count() 慢因默认全表扫描,需通过建立有效索引、显式指定非空索引列(如count('id'))、避免函数/模糊查询等使索引生效;伪优化sql_calc_found_rows已废弃;高并发场景应采用缓存+分片或采样估算。

count() 为什么慢?先看执行计划
ThinkPHP 的 count() 默认走全表扫描,哪怕加了 where 条件,只要没命中有效索引,MySQL 就会扫完整个聚簇索引。用 EXPLAIN 一看,type 是 ALL 或 index,rows 接近表总行数,基本就坐实了。
常见诱因包括:条件字段没索引、用了函数(如 DATE(create_time))、OR 连接多个非索引字段、LIKE '%xxx' 左模糊。
优化核心不是换写法,而是让数据库能用上索引快速定位行数 —— 后面所有技巧都绕不开这点。
用 field 指定主键或非空索引列
ThinkPHP 的 count() 默认等价于 COUNT(*),但 MySQL 对 COUNT(主键) 和 COUNT(非空索引列) 的优化程度更高,尤其在覆盖索引场景下可避免回表。
实操建议:
- 确保
where条件中的字段已建联合索引,且该索引包含主键或一个NOT NULL字段 - 显式调用
count('id')而非count(),前提是id是主键且不允许为NULL - 如果主键是自增
INT,比BIGINT或字符串主键更利于索引扫描效率
示例:
$count = Db::name('user')->where('status', 1)->where('create_time', '>=', '2024-01-01')->count('id');比 count() 更可能触发索引覆盖。
用 SQL_CALC_FOUND_ROWS + LIMIT 是伪优化
有些教程推荐先查带 LIMIT 的数据再用 SELECT FOUND_ROWS(),但在 ThinkPHP 中这需要手动拼 SQL,且 MySQL 8.0+ 已移除 SQL_CALC_FOUND_ROWS 支持,5.7 也明确标记为废弃。
它的问题不止是兼容性:
- 执行两遍查询(带 LIMIT 的结果集 + FOUND_ROWS),IO 和 CPU 开销不比一次
COUNT小 - 若查询含
ORDER BY或复杂 JOIN,FOUND_ROWS()返回的是未加 LIMIT 前的总行数,但实际扫描量可能更大 - TP 的
paginate()内部虽曾用此机制,但新版默认已切回独立COUNT查询
结论:别碰,尤其新项目直接排除。
真正有效的替代方案:缓存 + 条件分片
对高并发、低频变更的统计场景(比如后台用户总数、某类订单日统计),硬扛实时 COUNT 不现实。可行路径是「降精度换性能」:
- 用 Redis 的
INCRBY/DECRBY维护预计算值,增删数据时同步更新,count变成GET操作 - 按时间分片缓存:例如把「昨日订单数」存在
order_count:20240510,每天零点用定时任务刷一次,接口只读缓存 - 对「模糊总数」需求(如「约 2.3 万条」),可用采样估算:查 1% 的随机页(
LIMIT 10000, 1),反推总量,误差可控且极快
注意:缓存方案必须配套幂等更新逻辑,否则数据不一致比慢更致命 —— 比如用数据库事务包住业务操作和 Redis 更新,或监听 binlog 异步修正。
索引是否生效、缓存是否穿透、分片粒度是否合理,这三个点漏掉任何一个,优化就容易翻车。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











