详解聚合函数count()

语法

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 条记录
T4Redis 计数加 1

会话 A 是一个插入交易记录的逻辑,往数据表里插入一行 R,然后 Redis 计数加 1;会话 B 就是查询页面显示时需要的数据。
在 T3 时刻会话 B 来查询的时候,会显示出新插入的 R 这个记录,但是 Redis 的计数还没加 1。这时候,就会出现数据不一致。

  • 查到的 100 行结果里没有最新插入的记录,而 Redis 的计数里已经加了 1
时刻会话 A会话 B
T1
T2Redis 计数加 1
T3读取 Redis 计数。查询最新 100 条记录
T4插入一行数据 R

会话 B 在 T3 时刻查询的时候,Redis 计数加了 1 了,但还查不到新插入的 R 这一行,也是数据不一致的情况。

在数据库保存计数

把这个计数直接放到数据库里单独的一张计数表 C 中

时刻会话 A会话 B
T1
T2begin;表 C 计数加 1
T3begin;读取表 C 计数;查询最新 100 条记录;commint;
T4插入一行数据 R;commint;

虽然会话 B 的读操作仍然是在 T3 执行的,但是因为这时候会话 A 更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见。因此,会话 B 看到的结果里, 查计数值和最近100条记录看到的结果,逻辑上就是一致的。

小结

  • MyISAM表虽然count(*)很快,但是不支持事务
  • show table status命令虽然返回很快,但是不准确
  • InnoDB表直接 count(*)会遍历全表,虽然结果准确,但会导致性能问题

自己计数(需要自己找一个地方,把操作记录表的行数存起来)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值