一、count(*) 的实现方式
不同引擎,不同实现方式。InnoDB,事务支持、并发能力、安全优于 MyISAM
MyISAM 总行存磁盘,直接返回,效率高;加where 不能返回这么快
InnoDB :一行行从引擎读出,然后累积。
二、为什么 InnoDB 不把数字存起来?
多版本并发控制(MVCC)原因,InnoDB 不确定“返回多少行”。表 t 10000 条记录,三用户并行会话:
A 启动事务,查询表总行;
B 启动事务,插入一行,查询表总行;
C 启动单独语句,插入一行,查询表总行。
上到下按照时间顺序执行,同一行语句是在同一时刻执行
InnoDB默认可重复读。对于 count(*) InnoDB 一行行地读出判断,可见行用于计算总行数。
如对 MVCC 记忆模糊,回顾《事务隔离:为什么你改了我还看不见?》和第 8 篇文章《事务到底是隔离的还是不隔离的?》
InnoDB 索引组织表,主键索引树的叶子节点是数据,普通索引树叶子节点是主键值。普通比主键索引树小。 count(*)遍历哪个结果都一样。MySQL 优化器找最小遍历。
show table status 结果里面也有一个 TABLE_ROWS 用于显示这个表当前有多少行,这个命令执行挺快的,误差 40% 到 50%。显示的行数也不能直接使用。
1)MyISAM count(*) 快,不支持事务;
2)show table status 快,不准确;
3)InnoDB count(*) 遍历全表,准确,性能问题。
三、自己计数方法,优缺点
场景:显示交易系统操作总数。思路:找地方存操作记录行数
3.1用缓存系统保存计数
更新频繁库,插入Redis 加 1,删减 1。读和更新都快。
可能会丢失。异常重启,计数操作丢失,到库里执行 count(*) 获取 。异常重启不经常可接受
显示操作总数和最近操作100 条记录。先到 Redis ,再到数据表里取数据记录。不精确:
1. 100 行结果最新,Redis 计数没加 1;
2. 另一种是,100 行没最新,Redis 计数加1。
插入 R,加 1; B 就查询页面显示时需要的数据。不一致。
先写数据表,再改 Redis 计数。读的时候是先读 Redis,再读数据表,顺序相反的。顺序一样的话,是不是就没问题了?换一下,再看结果。
反过来,会话 B 在 T3 时刻查询的时,Redis加 1 ,查不到新插入行
3.2 数据库保存计数
计数放到数据库表 C ,用“事务”解决
B 读仍在 T3 执行,更新还没提交,加 1对B 不可见,一致。
四、不同count 用法
count() 聚合函数,返回结果集,一行行判断,参数不是 NULL,累计值加 1。
count(*)、count(主键 id) 和 count(1)
性能差别原则:
1. server 层要什么就给什么;
2. InnoDB 只给必要值;
3. 优化器只优化 count(*) 语义为“取行数”,其他“显而易见”的优化并没有做。
1)count(主键 id):遍历整表,每行id 值都取出,返回给 server 层。判断非空,按行累加。
2)count(1) :遍历整表,不取值。server 层每行放“1”,判断非空,按行累加。
比 count(主键 id) 快。不涉及到解析数据行,拷贝字。
3)count(字段):第一条原则,server 层要什么,InnoDB 就返回什么。
1. “字段”not null,一行行读,判断非null,按行累加;
2. 允许为 null,值取判断,不是 null 累加。
4)count(*) 是例外,不取值,肯定不是 null,按行累加。
效率排序的话,count(字段)
小结
获表行数两种方法:不同引擎count(*) 方式不一样,缓存存储计数值问题。
计数放在 Redis 里面,两个不同的存储构成的系统,不支持分布式事务,无法拿到一致视图。计数值也放MySQL 中,解决一致性视图问题。
InnoDB 支持事务,用好事务原子性和隔离性,简化开发逻辑。
思考题
刚刚方案中,用事务确保计数准确。中间结果不被读到,因此修改计数值和插入新记录的顺序不影响结果。并发系统性能考虑,这个事务,先插入,还是先更新计数表?
更新涉及行锁竞争,先插入减少锁等待。计数表热点行,加锁时间要足够短
一个事务,两个语句:insert into 数据表;update 计数表,加 1。知识点在《行锁功过:怎么减少行锁对性能的影响?》
计数表记录多个业务表行数,表名字段加唯一索引。
CREATE TABLE `rows_stat` (
`table_name` varchar(64) NOT NULL,
`row_count` int(10) unsigned NOT NULL,
PRIMARY KEY (`table_name`)
) ENGINE=InnoDB;
更新时传 where table_name=$table_name,主键索引,更新加行锁只会锁在一行上。
业务表插入数据,计数表更新不同行,不会有行锁。
评论1
字段上有索引,且非空,count(字段)效率还最差。
count(id)也可用普通索引
评论2
库里总共 30w 数据 。 第一次用 count(*) 是 120多ms , 第二次就是 60多 ms 。 第三次用了 count(1) ,也是60多ms 。 请问 count(*) 这两次的前后时间差是什么原因,也会走缓存 ?
进了Buffer pool