查询数据条数详解。
比如你维护着一张电商订单表,业务的需求是查找所有订单数,开发很快能写出对应的 SQL :
select count(*) from order_01;
但你是否会发现,如果这张表很大后,这条 SQL 会非常耗时。
今天我们就一起重新认识下 count(),并想办法去优化这类 SQL。
老规矩,先创建测试表并写入数据。
use muke; /* 使用muke这个database */
drop table if exists t1; /* 如果表t1存在则删除表t1 */
CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`),
KEY `idx_b` (`b`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
drop procedure if exists insert_t1; /* 如果存在存储过程insert_t1,则删除 */
delimiter ;;
create procedure insert_t1() /* 创建存储过程insert_t1 */
begin
declare i int; /* 声明变量i */
set i=1; /* 设置i的初始值为1 */
while(i<=10000)do /* 对满足i<=10000的值进行while循环 */
insert into t1(a,b,c,d) values(i,i,i,i); /* 写入表t1中a、b两个字段,值都为i当前的值 */
set i=i+1; /* 将i加1 */
end while;
end;;
delimiter ; /* 创建批量写入10000条数据到表t1的存储过程insert_t1 */
call insert_t1(); /* 运行存储过程insert_t1 */
insert into t1(a,b,c,d) values (null,10001,10001,10001),(10002,10002,10002,10002);
drop table if exists t2; /* 如果表t2存在则删除表t2 */
create table t2 like t1; /* 创建表t2,表结构与t1一致 */
alter table t2 engine =myisam; /* 把t2表改为MyISAM存储引擎 */
insert into t2 select * from t1; /* 把t1表的数据转到t2表 */
CREATE TABLE `t3` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4;
insert into t3 select * from t1; /* 把t1表的数据转到t3表 */
count(a) 和 count(*) 的区别
当 count() 统计某一列时,比如 count(a),a 表示列名,是不统计 null 的。
比如测试表 t1,我们插入了字段 a 为 null 的数据,我们来对 a 做一次 count():
select count(a) from t1;
实际在数据写入时,写入了 10002 行数据。因此,对 a 字段为 null 的这一行不做统计。
而 count(*) 无论是否包含空值,都会统计。
我们对测试表 t1 执行一次 count(*):
select count(*) from t1;
显然,统计的是所有的行。因此,如果希望知道结果集的行数,最好使用 count(*)。
对于 MyISAM 引擎,如果没有 where 子句,也没检索其它列,那么 count(*) 将会非常快。因为 MyISAM 引擎会把表的总行数存在磁盘上。
首先我们看下对 t2 表(存储引擎为 MyISAM)不带 where 子句做 count(*) 的执行计划:
explain select count(*) from t2;
在 Extra 字段发现 “Select tables optimized away” 关键字,表示是从 MyISAM 引擎维护的准确行数上获取到的统计值。
而 InnoDB 并不会保留表中的行数,因为并发事务可能同时读取到不同的行数。所以执行 count(*) 时都是临时去计算的,会比 MyISAM 引擎慢很多。
我们看下对 t1 表(存储引擎为 InnoDB)执行 count(*) 的执行计划:
发现使用的是 b 字段的索引 idx_b,并且扫描行数是10109,表示会遍历 b 字段的索引树去计算表的总量。
对比 MyISAM 引擎和 InnoDB 引擎 count(*) 的区别,可以知道:
1 MyISAM 会维护表的总行数,放在磁盘中,如果有 count(*) 的需求,直接返回这个数据
2 但是 InnoDB 就会去遍历普通索引树,计算表数据总量
在上面这个例子,InnoDB 表 t1 在执行 count(*) 时,为什么会走 b 字段的索引而不是走主键索引呢?下面我们分析下:
用 Redis 做计数器
首先初始化时,执行一次精确计数:
select count(*) from t1;
表此时的总数是 10002,把这个值赋给 Redis 中一个 key,命令如下:
set t1_count 10002
insert into t1(a,b,c,d) values (10003,10003,10003,10003);
把 Redis 中 t1_count 这个 key 的值加 1,命令如下:
INCR t1_count
delete from t1 where id=10003;
把 Redis 中 t1_count 这个 key 的值减 1,命令如下:
get t1_count
这里对 Redis 的计数做一些补充:
INCR t1_count 表示为键 t1_count 存储的数字值加 1
DECR t1_count 表示为键 t1_count存储的数字值减 1如果一次需要增加或者删除多行,用法如下:
INCRBY t1_count 10 表示一次为键 t1_count 存储的数字值加 10。
DECRBY t1_count 10 表示一次为键 t1_count 存储的数字值减 10。
但是这种方法还是有缺点的,试想,在表 t1 写入数据到 Redis ,再到把 t1_count 加 1,总会存在一个时间差,如果这中间另外一个 session 去读取 Redis 中 t1_count 的值,此时 t1_count 的值没增加,但是表的实际数据行已经增加了,所以就会不准确。
这一步操作我们用 MySQL 中一张 InnoDB 表来代替,而数据写入操作和计数操作都放在一个事务中,就可以避免 出现计数不准确的情况。
因为放在同一个事务里,在图中 1 这个位置点,因为事务还没提交,所以表 t1 写入一条记录本身就对其它 session 不可见,此时其它 session 去执行 select count(*) from t1 和查计数表 count_t1 的记录都是一样的,为 101 。不会出现用 Redis 计数时,表实际总数与计数器的值不一致的情况。
1 用 Redis 做计数器:能快速获取结果,比 show table status 结果准确,但是并发场景计数可能不准确;
2 增加 InnoDB 计数表:能快速获取结果,利用了事务特性确保了计数的准确,也是比较推荐的方法。
该文为本人学习的笔记,方便以后自己复习。参考
《高性能 MySQL》(第三版):6.7.1 优化 COUNT() 查询《MySQL 5.7 Reference Manual》:14.6.1.6 Limits on InnoDB Tables
《MySQL 5.7 Reference Manual》:12.20.1 Aggregate (GROUP BY) Function
Descriptions《MySQL 5.7 Reference Manual》:13.7.5.36 SHOW TABLE STATUS Syntax
慕课网专栏:https://www.imooc.com/read/43取其精华整合而成。