数据库监控与调优【十四】—— COUNT语句优化

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不同版本的特点进行调优,活学活用调优方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值