一、limit优化
之前的member会员表,联合索引为KEY `idx_name_age_address` (`name`,`age`,`address`),表里插入了十万条数据,一般情况下分页查询的sql语句:
select * from member limit 90000,10;
explain select * from member limit 90000,10;
执行计划是全表扫描,底层执行过程:首先,如果没有排序字段,默认按照id进行排序,查询前90010条件数据出来,然后去掉前面90000条件数据,最后剩下的10条就是查询结果,其实这种随着数据量越来越大,越往后面翻页,一次查询的数据只会多不会少,这样查询就会存在问题
1、根据自增主键排序
select * from member where id > 90000 limit 10;
explain select * from member where id > 90000 limit 10;
执行计划显示出使用了主键id索引的范围查询,这种情况,如果中间id有断层,查询的数据就会有问题;比如id为1的被删除了,理论上就会是id为90002的开始到90011结束,但是如果使用id筛选过滤的话就会有问题,具体体现如下(把id为1的数据删了):
select * from member limit 90000,10;
select * from member where id > 90000 limit 10;
2、根据普通索引字段排序分页查询
这个时候就需要对sql再做一些优化,根据二级索引b+树的结构,先根据字段排好序(一般情况下不会根据id排序),分页查出前10条数据的id,这样就可以避免回表而使用到了覆盖索引,接着再根据这些id关联查询出来的就是结果集
select * from member t1 inner join (select id from member order by name limit 90000,10) t2 on t1.id = t2.id;
explain select * from member t1 inner join (select id from member order by name limit 90000,10) t2 on t1.id = t2.id;
根据最左前缀的字段name进行排序分页查询的结果如下:
查询计划首先是id为2的先执行,类型为衍生查询,使用了覆盖索引,三个索引字段都用到了,并且使用了索引排序;第二执行是table为<derived2>执行,虽然是全表扫描,但是只有10个id结果集而已;第三执行的是table为t1的执行,查询类型为eq_ref,使用了主键关联,所以整个过程是相当快的
二、join优化
#创建表
CREATE TABLE `table1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
#创建一个和table1一样的表
create table table2 like table1;
drop procedure if exists insert_table1;
CREATE PROCEDURE insert_table1 () BEGIN
DECLARE
i INT;
SET i = 1;
WHILE
( i <= 100000 ) DO
INSERT INTO table1 ( a, b )
VALUES
( i, i );
SET i = i + 1;
END WHILE;
END;
drop procedure if exists insert_table2;
CREATE PROCEDURE insert_table2 () BEGIN
DECLARE
i INT;
SET i = 1;
WHILE
( i <= 100 ) DO
INSERT INTO table2 ( a, b )
VALUES
( i, i );
SET i = i + 1;
END WHILE;
END;
call insert_table1();
call insert_table2();
有两个表table1和table2,table1有100000条数据,table2有100条数据,索引字段也都是a,b就是普通字段
1、嵌套循环连接Nested-Loop Join(NLJ)算法
EXPLAIN select * from table1 t1 inner join table2 t2 on t1.a = t2.a;
执行过程:根据执行计划,首先执行的是table列为t2的,进行的是全表扫描;然后执行的是table为t1,是索引进行关联,table2是驱动表,table1是被驱动表;t2是索引关联查询,只是在索引树上查找到主键,然后回表一次,table2全表扫描一行数据,table1跟着全表扫描一行数据,执行过程我画了张草图
2、基于块的嵌套循环Block Nested-Loop Join(BNL)算法
EXPLAIN select * from table1 t1 inner join table2 t2 on t1.b = t2.b;
mysql8.0之前执行的结果图
mysql8.0执行结果图
执行过程:table列值为t2的先执行,t2作为驱动表用的全表扫描,扫描了100行放到join_buffer中(放得下的情况,如放不下就分批),接着table1全表扫描100000行去join_buffer中进行数据匹配,匹配成功返回,下面的图是对这段话的说明:
show variables like '%join_buffer_size%';#查看join_buffer大小
对于驱动表的定义:join连接数据量较少的表,也可以使用straight_join指定驱动表,但不适用于left join和right join,因为它们已经指定了驱动表,straight_join用法:
select * from table1 t1 straight_join table2 t2 on t1.b = t2.b;
假设字段无索引还使用了NLJ算法,总共就会扫描100000*100行,table2扫描一行数据到table1表遍历查询,因为没有走索引,这个过程是非常慢的,table2表每扫描一行就要去table1去遍历扫描,单次最坏的情况要十万次才找到结果集,所以就是100000*100次行扫描
一般情况下如果对性能有比较高的要求的系统,都不推荐使用join关联,如果非要用最好不要超过三个,优化起来比较困难,优化原理,小表驱动大表,join连接字段大表最好建有索引
三、in和exists
1、in的查询优化
小表驱动大表
#table1十万条数据 table2一百条数据
EXPLAIN select * from table1 where id in (select id from table2);#正确示例
EXPLAIN select * from table2 where id in (select id from table1);#错误示例
table2是驱动表,走的是普通索引,因为id在二级索引树上就可以找到,所以选择普通索引;table1使用的是主键关联查询,直接就可以在主键索引树上获取数据;
第二种可能是mysql做的优化,table2数据量比较少,虽然位置放的不合理,还是用table2作为驱动表,先进性table2的全表扫描,比之前的慢一点,之后的差不多
2、exists查询优化
如果能用in就不要用exists
explain select * from table2 t2 where exists (select * from table1 t1 where t1.id = t2.id);#正确示例
explain select * from table1 t1 where exists (select * from table2 t2 where t2.id = t1.id);#错误示例
第一条sql,t2作为驱动表,虽然是全表扫描,但是数据量没有t1那么多,然后再用t1主键关联,这个和in位置写错了差不多
第二条sql,t1全表扫描,t1十万条数据,显然这个是很慢的,然后主键关联肯定时间也长
四、count查询优化
count(*),count(1),count(id),count(字段)
count的不同写法,要说快慢其实差不多,具体要分两种情况,一种是字段有索引,一种是字段无索引
1、有索引
count(*)≈count(1)>count(字段)>count(id)
count(*)的执行过程,底层不取值,按行累加
count(1)和count(字段)差不多,count(1)不需要取出字段值,就按照常量1做统计,count(字段还需要取值),count(1)要比count(字段要快)
count(字段)比count(id)要快,因为字段有索引情况下,二级索引比主键索引树数据少,扫描效率肯定快
2、无索引
count(*)≈count(1)>count(id)>count(字段)
上面已经说明count(*)和count(1),而count(id)和count(字段),没有索引自然count(id)要快,毕竟是在索引树上进行的内存操作
EXPLAIN select count(*) from member;
EXPLAIN select count(1) from member;
EXPLAIN select count(id) from member;
EXPLAIN select count(name) from member;
为什么count(id)也用到了二级索引,因为二级索引树的叶子节点数据量少,可能mysql底层也做了优化
3、优化方法
- myisam类型表的总行数
myisam存储引擎的表会把总记录数维护在磁盘上
EXPLAIN select count(*) from test_myisam;
innodb为什么不也设置一个总行数呢,因为myisam不支持事务,innodb需要支持事务,如果维护了,代价高
- show table status
show table status like 'member';
对member表的总行的估值
- 将总行数维护到redis中
在数据删除和新增时,往redis中进行表key值加减,但是无法保证一致性
- 增加表维护总行数
新增一个表table_count,存表明和这个表的行数,新增和删除时进行表table_count数据的更改,这样就可以事务的一致性了
五、mysql数据类型选择
1、数值类型
类型 | 大小 |
TINYINT | 1字节 |
SMALLINT | 2字节 |
INT | 4字节 |
BIGINT | 8字节 |
FLOAT | 4字节 |
DOUBLE | 8字节 |
DECIMAL | DECIMAL(M,D) ,如果M>D,为 M+2否则为D+2 |
如果整型没有负数,可以指定字段为无符号类型
对精度有要求,字段要设置成DECIMAL
2、时间类型
类型 | 大小 |
DATE | 3字节 |
TIME | 3字节 |
YEAR | 1字节 |
DATETIME | 8字节 |
TIMESTAMP | 4字节 |
如果只是日期,那就选date类型,这样可以减小筛选的粒度
datetime类型最大值是9999-12-31 23:59:59,而timestamp类型最大值是2038-01-19 03:14:07,衡量之后再做选择
3、字符串类型
类型 | 大小 |
CHAR | 0-255字节 |
VARCHAR | 0-65535 字节 |
TINYBLOB | 0-255字节 |
TINYTEXT | 0-255字节 |
BLOB | 0-65535字节 |
TEXT | 0-65535字节 |
如果已经确定了字符长度最好用char类型,比如char(4),而只存了一个a,就会在后面补空格,查询出来,mysql会帮你去空格,这样查询也会耗费性能;
如果是tinyint(4)就会进行0填充,加入存了一个2,结果就是0002,查询会去零,括号里的只是显示宽度
blob字段分开一个表存,只存id和blob内容就行,保证两个表的的双写一直就可以,这样可以减少主键索引的空间,提高查询速度