语法
COUNT(expr)
返回 select 语句检索到的行中非NULL
值的数目。若找不到匹配的行,则 COUNT(expr) 返回0
。
mysql> select * from users;
+----+------+----------+
| id | name | password |
+----+------+----------+
| 1 | yhq | a1 |
| 2 | qhh | NULL |
+----+------+----------+
2 rows in set
mysql> select count(name) from users;
+-------------+
| count(name) |
+-------------+
| 2 |
+-------------+
1 row in set
mysql> select count(password
) from users;
+-----------------+
| count(password) |
+-----------------+
| 1 |
+-----------------+
1 row in set
COUNT(*)
返回检索行的数目, 不论其是否包含 NULL 值。
mysql> select * from users;
+----+------+----------+
| id | name | password |
+----+------+----------+
| 1 | yhq | a1 |
| 2 | qhh | NULL |
+----+------+----------+
2 rows in set
mysql> select count(*
) from users;
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set
COUNT(DISTINCT expr,[expr…])
返回不同的非NULL值
数目。若找不到匹配的项,则 COUNT(DISTINCT)返回0
。
mysql> select * from users;
+----+------+----------+
| id | name | password |
+----+------+----------+
| 1 | yhq | a1 |
| 9 | test | a2 |
| 8 | yqy | NULL |
| 2 | qhh | a1 |
+----+------+----------+
4 rows in set
mysql> select count(distinct(password)) from users;
+---------------------------+
| count(distinct(password)) |
+---------------------------+
| 2 |
+---------------------------+
1 row in set
不同的 count 用法(基于 InnoDB 引擎)
count(主键 id)
InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。
count(1)
InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
单看这两个用法的差别的话,你能对比出来,count(1)执行得要比 count(主键 id)快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。
count(字段)
如果这个字段
是定义为not null
的话,一行行地从记录里面读出这个字段,判断不能为null
,按行累加;
如果这个“字段”定义允许为null
,那么执行的时候,判断到有可能是null
,还要把值取出来再判断一下,不是null
才累加。
count(*)
并不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是 null,按行累加。
按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*),所以我建议你,尽量使用count(*)。
InnoDB
是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于count(*)
这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL 优化器会找到最小的那棵树来遍历。在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一
。
实现方式
在不同的 MySQL 引擎中,count(*)有不同的实现方式。
MyISAM引擎
把一个表的总行数存在了磁盘上,因此执行count(*)
在这里插入代码片的时候会直接返回这个数,效率很高。InnoDB引擎
执行count(*)
的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。
如果加了
where条件
的话,MyISAM 表也是不能返回得这么快的。
为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢?
因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表应该返回多少行
也是不确定的。
假设表中现在有 10000
条记录
- 会话 A 先启动事务并查询一次表的总行数;
- 会话 B 启动事务,插入一行后记录后,查询表的总行数;
- 会话 C 先启动一个单独的语句,插入一行记录后,查询表的总行数。
会话 A | 会话 B | 会话 C |
---|---|---|
begin; | ||
select count(*) from users; | ||
insert into users(插入一行); | ||
begin; | ||
insert into users(插入一行); | ||
select count(*) from users;(返回10000 ) | select count(*) from users;(返回10002 ) | select count(*) from users;(返回10001 ) |
在最后一个时刻,三个会话 A、B、C 会同时查询表 t 的总行数,但拿到的结果却不同。
这和InnoDB
的事务设计
有关系,可重复读
是它默认的隔离级别,在代码上就是通过多版本并发控制,也就是 MVCC 来实现的。每一行记录都要判断自己是否对这个会话可见,因此对于count(*)
请求来说,InnoDB
只好把数据一行一行地读出依次判断,可见的行才能够用于计算基于这个查询
的表的总行数。
table_rows 能代替 count(*)吗?
mysql> show table status \G
*************************** 1. row ***************************
Name: users
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 5
Avg_row_length: 3276
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 0
Auto_increment: 6
Create_time: 2020-07-12 23:51:19
Update_time: 2020-07-12 23:55:30
Check_time: NULL
Collation: utf8_unicode_ci
Checksum: NULL
Create_options:
Comment:
show table status
命令的输出结果里面也有一个Rows
用于显示这个表当前有多少行。但是Rows
是从采样估算得来的,因此它也很不准。有多不准呢,官方文档说误差可能达到40%
到50%
。所以,show table status命令显示的行数也不能直接使用
。
用缓存系统保存计数
Redis
要显示操作记录的总数,同时还要显示最近操作的 100 条记录。
- 查到的 100 行结果里面有最新插入记录,而 Redis 的计数里还没加 1
时刻 | 会话 A | 会话 B |
---|---|---|
T1 | ||
T2 | 插入一行数据 R | |
T3 | 读取 Redis 计数。查询最新 100 条记录 | |
T4 | Redis 计数加 1 |
会话 A 是一个插入交易记录的逻辑,往数据表里插入一行 R,然后 Redis 计数加 1;会话 B 就是查询页面显示时需要的数据。
在 T3 时刻会话 B 来查询的时候,会显示出新插入的 R 这个记录,但是 Redis 的计数还没加 1。这时候,就会出现数据不一致。
- 查到的 100 行结果里没有最新插入的记录,而 Redis 的计数里已经加了 1
时刻 | 会话 A | 会话 B |
---|---|---|
T1 | ||
T2 | Redis 计数加 1 | |
T3 | 读取 Redis 计数。查询最新 100 条记录 | |
T4 | 插入一行数据 R |
会话 B 在 T3 时刻查询的时候,Redis 计数加了 1 了,但还查不到新插入的 R 这一行,也是数据不一致的情况。
在数据库保存计数
把这个计数直接放到数据库里单独的一张计数表 C 中
时刻 | 会话 A | 会话 B |
---|---|---|
T1 | ||
T2 | begin;表 C 计数加 1 | |
T3 | begin;读取表 C 计数;查询最新 100 条记录;commint; | |
T4 | 插入一行数据 R;commint; |
虽然会话 B 的读操作仍然是在 T3 执行的,但是因为这时候会话 A 更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见。因此,会话 B 看到的结果里, 查计数值和最近100条记录
看到的结果,逻辑上就是一致的。
小结
MyISAM
表虽然count(*)
很快,但是不支持事务
。show table status
命令虽然返回很快,但是不准确
。InnoDB
表直接 count(*)会遍历全表,虽然结果准确
,但会导致性能问题
。
自己计数
(需要自己找一个地方,把操作记录表的行数存起来)