为何统计总数的COUNT查询耗时11秒,结果查询仅需1秒?
嘿,这个问题我见过好多次了——明明拉数据的查询秒出,套个count(*)就突然卡成狗,咱们来一步步拆解原因:
核心差异:执行计划的本质不同
先看你这两个SQL的核心区别:展示结果的SQL是直接返回去重后的title, version,而统计总数的SQL是把这个结果套了两层子查询去计数。这里的关键是数据库对「返回数据」和「统计总数」的执行逻辑完全不一样:
1. 子查询的物化开销
你的统计SQL里套了两层子查询:
(SELECT count(*) as id FROM ( SELECT DISTINCT title, version FROM book AS b ... UNION ALL ... ) c )
数据库必须先执行内层的UNION ALL+DISTINCT,把所有符合条件的title, version行全部生成出来并存储到临时表(也就是「物化」),然后再扫描这个临时表逐行计数。而展示结果的SQL呢?数据库可以做流式返回——哪怕是全量数据,它查到一部分就可以给客户端发一部分,不需要把所有结果都先攒到临时表里,内存和IO开销小太多。
2. 索引利用的天差地别
对于展示结果的SQL,数据库大概率会用book(title, version, column1)或者book2(title, version, column1)这类覆盖索引——直接从索引里取需要的字段,根本不用回表查主数据。但套了count(*)之后,优化器可能会犯“糊涂”:它可能放弃覆盖索引,转而扫描主表;或者因为子查询的嵌套结构,没法利用索引快速计数,必须先物化所有行再统计。
3. DISTINCT的重复计算
你内层两个分支都加了DISTINCT title, version,然后用UNION ALL拼接(这个是对的,因为两个分支的条件互斥,不会有重复)。但统计的时候,数据库还是要把所有去重后的行都收集起来再计数;而展示结果的SQL,数据库在扫描过程中就能完成去重,不需要额外的物化步骤。
4. 笔误可能雪上加霜
哦对了,你第二个分支的WHERE条件里写的是b.title IS NOT NULL——但这里的表别名是b2啊!这不仅可能导致错误的结果,还可能让数据库没法正确利用索引,进一步拖慢查询速度。
优化方案:让count(*)快起来
试试这几个方法,应该能大幅提升统计速度:
- 去掉多余的子查询嵌套:直接把
count(*)和内层查询合并,减少不必要的物化:
SELECT COUNT(*) FROM ( SELECT DISTINCT title, version FROM book AS b WHERE b.title IS NOT NULL AND NOT EXISTS (SELECT * FROM user AS u WHERE u.column1 = b.column1) UNION ALL SELECT DISTINCT title, version FROM book2 AS b2 WHERE b2.title IS NOT NULL AND EXISTS (SELECT * FROM user AS u WHERE u.column2 = b2.column1) ) AS c;
- 分开计数再求和:因为两个分支的条件是互斥的(一个
NOT EXISTS,一个EXISTS),可以分别计算每个分支的去重数量,再相加。这样数据库能对每个分支用最优执行计划,避免临时表开销:
SELECT (SELECT COUNT(DISTINCT title, version) FROM book AS b WHERE b.title IS NOT NULL AND NOT EXISTS (SELECT * FROM user AS u WHERE u.column1 = b.column1)) + (SELECT COUNT(DISTINCT title, version) FROM book2 AS b2 WHERE b2.title IS NOT NULL AND EXISTS (SELECT * FROM user AS u WHERE u.column2 = b2.column1)) AS total_count;
- 添加覆盖索引:给
book和book2分别创建(title, version, column1)的覆盖索引,让数据库可以直接通过索引计算去重后的数量,完全不用回表。
最后,一定要用EXPLAIN命令看看两个SQL的执行计划差异——这是定位问题最直接的方法,能清楚看到数据库到底是用了索引还是扫了全表,有没有生成临时表。
内容的提问来源于stack exchange,提问作者Kivo

