关于 select count 的一些总结
前言
在做一个接口分页查询的时候,由于数据量太多(需要一个亿级别的表join 一个百万级别的表),select count(*)发生了超时,简单探究了下,在此做个总结,希望可以便人便己。
一、关于背景与环境
基本的背景和 sql 环境如【1】 中所介绍的,需要分页查询在线服务记录。【1】中讨论的是分页问题,这里讨论的获取记录总数的问题。基本的 SQL 如下:
sql
SELECT count(*)
FROM t_chat_detail d
JOIN t_service_record r ON r.session_id = d.session_id
WHERE d.end_time BETWEEN FROM_UNIXTIME(1778984821) AND FROM_UNIXTIME(1784294202);
查询时间范围为2026-05-17(1778984821)到2026-07-17(1784294202)时间范围内的在线服务记录。由于t_service_record的记录总数有一亿多条数据,t_chat_detail总数也有两百多万条,因此 join 查询非常慢。如下所示, 查询了70 多万条记录,耗时23秒。

看下 explain, 由于没有命中索引(其实这里如果使用了索引也非常慢), mysql采用了全表扫的方式。

所以对于一些分页查询来说,由于有 limit 限制(细节可以查看【1】),即使数据可以查得到,但是如果想要返回完整的total, 也有可能会超时。
二、select count(*)的执行过程
sql
select count(col) from t_chat_detail;
先看看count(col)的定义,它的含义是符合查询条件的记录中,函数指定的参数col不为 NULL 的记录有多少个。
所以,上面的 sql 执行就是需要查询t_chat_detail中所有符合条件(上面的示例 sql 没有where 条件)的记录 中,col不为 NULL的有多少个。
基本的执行流程是 mysql server 层会为每个 sql 查询维护一个 count 变量,然后循环向引擎层 InnoDB 读取一条记录,如果记录对应的 col 不为空,则 count+1。 最终返回的count 数量。
上面有几点需要注意:
-
mysql会根据查询来选择不同的索引,有些查询可以直接在二级索引中进行搜索,这样不会回表,直接返回二级索引中的数据,会更快些。像上面示例中的查询,需要在驱动表 d 中进行全表扫描,然后进行 join 关联,最终把一条记录送往 serve层进行判断,整体的过程就比较长。
-
select count() 在内部会转化为 select count(1), **所以他们的性能是一样的。**如下面官网文中所示。
InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.
顺便说一点:他们在执行的时候性能都稍微比 select count(col) 要好一丢丢,因为select count(col) 中 InnoDB 层需要读取 col, server 层判断是否为空;而 select count(1) 一定是不为空的,InnoDB 不会实际读取记录,只返回结果。
- 为什么 mysql 不直接维护一个 count 变量呢?
因为 mysql 是支持事务操作的,每个事务看见的表内容可能是不一样的,因此无法维护一个统一的count 变量来应对。同时,搜索的 where 条件也是不统一的,不可能为所有的查询条件来维护数量。
三、一些解决方案
针对 select count(*) 超时的问题,有一些应对的方式:
-
使用估算的值
对于比较简单的查询(不涉及到联表的), explain可以估算出大概表的数量。前端页面显示大约有多少条数据。但是这种方式有可能不太准,仅仅只是个估算。
-
select count(*) 也使用 limit 提前终止查询。 比如说下面的查询,限制了 10001 条。
sql
SELECT COUNT(*)
FROM (
SELECT d.id
FROM t_chat_detail d
JOIN t_service_record r ON r.session_id = d.session_id
WHERE d.end_time BETWEEN FROM_UNIXTIME(1778984821) AND FROM_UNIXTIME(1784294202)
LIMIT 10001
) u;
那么10001 以内的查询是准的,超过 10001 的可以只显示超过 10000 条。 这也是从产品逻辑上做的一种妥协。
-
如果非要精确的 count数量,还有一种方式就是使用另外的存储(如 redis)来辅助记录 所需查询的结果数量。
-
最后一种最粗暴的,直接不查询。 页面不显示总共多少条,只提供向前翻页和向后翻页的功能。