关于 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) 有什么区别?哪个性能最好?

相关推荐
神经蛙199614 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
二月龙14 小时前
Spring 事务失效的 8 种场景,很多老手依然频繁踩雷
后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
长大198815 小时前
MyBatis 常见性能陷阱:N+1 查询、一级缓存踩坑解决方案
后端
用户18615580086015 小时前
MinIO Java 对接试用:从连接、上传到下载的完整示例
后端
爱勇宝15 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
极客悟道15 小时前
SDKMAN vs jEnv vs JetTUI,JDK 版本管理到底选哪个
后端
长大198815 小时前
Java8 新特性到底要不要吃透?工作中高频使用的 5 个功能总结
后端
二月龙15 小时前
Spring Bean 生命周期 & 循环依赖:90% 开发者只知结论不懂原理
后端