COUNT语句优化
有关COUNT的几个实验与结论
准备工作
create table user_test_count
(
id int primary key not null auto_increment,
name varchar(45),
age int,
email varchar(60),
birthday date
) engine 'innodb';
insert into user_test_count (id, name, age, email, birthday)
values (1, '张三', 20, 'zhangsan@imooc.com', '2000-01-01');
insert into user_test_count (id, name, age, email, birthday)
values (2, '李四', 30, 'lisi@imooc.com', '1990-01-01');
insert into user_test_count (id, name, age, email, birthday)
values (3, '王五', 40, null, null);
insert into user_test_count (id, name, age, email, birthday)
values (4, '柯柯', 18, null, null);
count(*)
只有一个主键索引
EXPLAIN SELECT count( * ) FROM user_test_count;
可以发现这条sql语句使用了主键索引
给email字段添加普通索引
ALTER TABLE user_test_count ADD INDEX user_test_count_email_index ( email );
再次执行上面得count查询语句
通过结果,可以发现使用了user_test_count_email_index这个普通索引
故得出结论:
1.count(*)当没有非主键索引时,会使用主键索引
2.count(*)如果存在非主键索引,会使用非主键索引
再次给birthday字段添加普通索引
ALTER TABLE user_test_count ADD INDEX user_test_count_birthday_index ( birthday );
再次执行上面得count查询语句
通过结果,可以发现使用了user_test_count_birthday_index这个普通索引
通过观察两个普通索引的长度可以得出结论:
3.count(*)如果存在多个非主键索引,会使用一个最小的非主键索引
为什么会出现上述情况
该表存储引擎是innodb,而innodb的非主键索引,叶子节点存储的是索引+主键;innodb的主键索引,叶子节点存储的是主键+表数据
而mysql是以页为单位的,一个页的默认大小是1024K。
于是,在一个页里面,非主键索引可以存储更多的条目。
对于一张表,假设存在1000000条数据,使用非主键索引扫描的页数可能只有100个,而使用主键索引可能需要500个
于是,在count查询语句,使用非主键索引的性能比使用主键索引的性能要好,如果存在多个非主键索引,会使用一个最小的非主键索引,也是为了在一个页里面存储更多的数据,从而节省扫描的次数
count(字段)
count的字段存在索引
EXPLAIN SELECT count( email ) FROM user_test_count;
通过结果,可以发现使用了user_test_count_email_index这个普通索引
count的字段不存在索引
EXPLAIN SELECT count( age ) FROM user_test_count;
通过结果,可以发现使用了全表扫描
对比count(*)和count(email)的结果
-- 结果为4
SELECT count( * ) FROM user_test_count;
-- 结果为2
SELECT count( email ) FROM user_test_count;
故得出结论:
1.count(字段)如果这个字段上面存在索引,走这个字段的索引
2.count(字段)如果这个字段上面不存在索引,走全表扫描
3.count(字段)只会对该字段统计,会排除掉该字段值为null的行,count(*)不会排除
count(1)
当存在三个索引,主键索引、普通索引user_test_count_email_index(长度为243字节)和普通索引user_test_count_birthday_index(长度为4字节),执行下面的sql
EXPLAIN SELECT count( 1 ) FROM user_test_count;
可以看到count(1)也是走了长度较小的索引user_test_count_birthday_index,和count(*)一样
详情见官方文档:https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html
具体结论:
1.count(*)和count(1)没有区别
2.MyISAM引擎中,如果count(*)没有where条件(形如
select count(*) from 表名
)查询会非常快。因为mysql会为此存储引擎存储精确的行数,并且可以非常快速地访问3.从MySQL 8.0.13开始,InnoDB引擎中,如果count(*)没有where条件(形如
select count(*) from 表名
),查询也会被优化,性能有所提升
总结
- count(*)和count(1)一样
- count(*)会选择最小的非主键索引,如果不存在任何非主键索引,则会使用主键
- count(*)不会排除为null的行,而count(字段)会排除
- 对于不带查询条件的count(*)语句,MyISAM引擎以及InnoDB引擎(MySQL >= 8.0.13),都作了优化
- 如果没有特殊需求,尽量使用count(*)
COUNT语句具体优化
准备工作
准备两个版本的MySQL,分别是8.0.18和5.6
都执行以下sql创建表,并尽可能多的添加数据,这里添加了2844047条数据
CREATE TABLE `salaries` (
`emp_no` int(0) NOT NULL,
`salary` int(0) NOT NULL,
`from_date` date NOT NULL,
`to_date` date NOT NULL,
PRIMARY KEY (`emp_no`, `from_date`) USING BTREE,
INDEX `salaries_from_date_to_date_index`(`from_date`, `to_date`) USING BTREE,
CONSTRAINT `salaries_ibfk_1` FOREIGN KEY (`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
MySQL 8.0.18版本
执行结果
SELECT count( * ) FROM salaries; -- 执行时间120ms
SHOW CREATE TABLE salaries; -- innodb
SELECT version( ); -- 版本8.0.18 > 8.0.13,可以针对无条件的count(*)语句去优化
MySQL 5.6版本
执行结果
SELECT count( * ) FROM salaries; -- 执行时间841ms
SHOW CREATE TABLE salaries; -- innodb
SELECT version( ); -- 版本5.6
任意版本查看执行计划
EXPLAIN SELECT count( * ) FROM salaries;
通过结果可以发现走了index全索引扫描,且走salaries_from_date_to_date_index索引,长度为6字节
所以,可以降低索引长度进行优化
方案一
创建一个更小的非主键索引,因为效果不明显就不演示了
方案二
把数据库表的存储引擎换成MyISAM,因为MyISAM引擎针对没有条件的count(*)语句会直接返回。
这种做法在实际项目中用得很少,一般不会修改数据库表的存储引擎。而且即使修改了数据库表的存储引擎,也只能提升不带查询条件的count(*)语句
方案三
新增一个汇总表,table[table_name, count] => 如:salaries, 2844047
表数据的新增和删除会通过触发器自动的修改汇总表的值
好处:结果比较准确,且比较灵活,可以根据需求设计汇总表。例如:需求不是简单的无条件查询count,而是有条件查询,比如某个薪资范围的用户数,就可以将汇总表设计成[salary范围, count]
坏处:增加了维护的成本
方案四
sql_calc_found_rows
实际项目中的count语句往往是和分页查询一同使用,如下
SELECT * FROM salaries LIMIT 0, 10;
SELECT COUNT(*) FROM salaries;
使用sql_calc_found_rows,如下
SELECT sql_calc_found_rows * FROM salaries LIMIT 0, 10;
在做完以上查询之后,mysql会自动地执行count
再在执行完上述语句之后,执行一条sql,获取count
SELECT FOUND_ROWS() AS salary_count;
好处:效率很高
坏处:MySQL 8.0.17已经废弃这种用法, 未来会被删除
注意:测试这种方式,需要在mysql终端执行,idea无法正常返回结果
在mybatis中使用sql_calc_found_rows参考文章:https://blog.csdn.net/myth_g/article/details/89672722
方案五
缓存
将SELECT count( * ) FROM salaries;
的结果存入缓存,后续用定时任务实时更新缓存的数据
好处:性能比较高,结果比较准确,有误差但是比较小,除非在缓存更新期间,新增或者删除了大量数据
坏处:引入了额外的组件,增加了架构的复杂度
方案六
information_schema.tables
SELECT * FROM information_schema.`TABLES` WHERE table_schema = 'employees' AND table_name = 'salaries';
返回结果TABLE_ROWS列即为想要的结果
好处:不操作salaries表,不论salaries表有多少条数据,都可以迅速返回结果
坏处:估算值,并不是准确值,所以需要业务对count的结果准确性要求不高
方案七
SHOW TABLE STATUS WHERE NAME = 'salaries';
返回结果Rows列即为想要的结果
好处:不操作salaries表,不论salaries表有多少条数据,都可以迅速返回结果
坏处:估算值,并不是准确值,所以需要业务对count的结果准确性要求不高
方案八
EXPLAIN SELECT * FROM salaries;
返回结果rows列即为想要的结果
好处:不操作salaries表,不论salaries表有多少条数据,都可以迅速返回结果
坏处:估算值,并不是准确值,所以需要业务对count的结果准确性要求不高
count语句优化实战
存在一条count语句
SELECT count( * ) FROM salaries WHERE emp_no > 10010; -- 执行时间799ms,较慢
使用explain查看执行计划
EXPLAIN SELECT count( * ) FROM salaries WHERE emp_no > 10010;
通过执行结果,可以看到type是range,表示范围查询,使用了主键,预计扫描1419213行,行数很多,导致性能低下
现在用的MySQL版本是8.0.18,这个版本的MySQL不带条件的count(*)查询是很快的
通过以下sql查询数据库最小的emp_no
SELECT MIN(emp_no) FROM salaries;
得到结果是10001,非常接近10010,故可以优化sql语句如下
SELECT count( * ) - ( SELECT count( * ) FROM salaries WHERE emp_no <= 10010 ) FROM salaries; -- 执行时间444ms,优化效果明显
使用explain查看执行计划
EXPLAIN SELECT count( * ) - ( SELECT count( * ) FROM salaries WHERE emp_no <= 10010 ) FROM salaries;
通过执行结果,可以看到首先执行了type=index的查询,扫描了2838426行,本质上就是SELECT count( * ) FROM salaries;
查询,但是由于MySQL版本是8.0.18,这个版本的MySQL不带条件的count(*)查询是很快的,故这个查询花费时间较少。接着,再执行SELECT count( * ) FROM salaries WHERE emp_no <= 10010;
扫描的是主键,扫描了112行,花费的时间比之前小很多。
如果MySQL版本低于8.0.13,该如何优化?
可以采用方案三(汇总表记录salaries表总数,再查询emp_no <= 10010的数量,通过总数-查询的数量即可)或者方案五
从这个实战例子,我们可以得出以下启发
1.尽量减少sql扫描的行
2.根据MySQL不同版本的特点进行调优,活学活用调优方案