关于 select count(*)的一些总结

关于 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 数量。

上面有几点需要注意:

  1. mysql会根据查询来选择不同的索引,有些查询可以直接在二级索引中进行搜索,这样不会回表,直接返回二级索引中的数据,会更快些。像上面示例中的查询,需要在驱动表 d 中进行全表扫描,然后进行 join 关联,最终把一条记录送往 serve层进行判断,整体的过程就比较长。

  2. 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 不会实际读取记录,只返回结果。

  1. 为什么 mysql 不直接维护一个 count 变量呢?
    因为 mysql 是支持事务操作的,每个事务看见的表内容可能是不一样的,因此无法维护一个统一的count 变量来应对。同时,搜索的 where 条件也是不统一的,不可能为所有的查询条件来维护数量。

三、一些解决方案

针对 select count(*) 超时的问题,有一些应对的方式:

  1. 使用估算的值

    对于比较简单的查询(不涉及到联表的), explain可以估算出大概表的数量。前端页面显示大约有多少条数据。但是这种方式有可能不太准,仅仅只是个估算。

  2. 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 条。 这也是从产品逻辑上做的一种妥协。

  1. 如果非要精确的 count数量,还有一种方式就是使用另外的存储(如 redis)来辅助记录 所需查询的结果数量。

  2. 最后一种最粗暴的,直接不查询。 页面不显示总共多少条,只提供向前翻页和向后翻页的功能。


参考

【1】关于一次 mysql join 慢查询想到的几点

【2】count(*) 和 count(1) 有什么区别?哪个性能最好?

相关推荐
三小河12 分钟前
SWC 凭什么比 Babel 快 ?项目优化迁移踩坑记录
前端·后端
鶴哥只手遮天30 分钟前
《Redis 8.10.3 深度剖析》系列 ·第 17 章. 一个迟到十六年的类型
redis·后端·开源
知守观1 小时前
同一个团队两套分包:DDD 四层落地后,domain 层最大的类还是 5885 行
java·后端·领域驱动设计
Anymous1 小时前
我手写的 Redis 锁,输给了 10 秒的 TTL
后端·架构
Rain的Java大神之路2 小时前
设计一套生产级 API 接口:从契约、安全、幂等到限流,我踩过的坑都在这了
java·后端·面试
SimonKing2 小时前
零配置也能白嫖前沿大模型!dsh-our-free-model插件真香警告
java·后端·程序员
nice_lcj5202 小时前
【从零理解 Spring AI · 15】让检索更准:先宽召回,再精排序
java·后端·rag·spring ai·ai应用开发·检索优化·重排序
回家路上绕了弯2 小时前
要人确认的那一步,在确认什么?
后端
IT_陈寒3 小时前
JavaScript项目实战经验分享:从内存泄漏到性能优化的血泪史
前端·人工智能·后端
进击的荆棘3 小时前
MySQL——库的操作
数据库·mysql