count(distinct)触发大规模排序是因为无法利用索引有序性,被迫进入sort aggregate模式,需全量拉取、排序或建哈希表去重;核心优化是建立匹配的最左前缀联合索引以启用松散索引扫描。

为什么COUNT(DISTINCT)会触发大规模排序
MySQL 执行 COUNT(DISTINCT) 时,若无法利用索引的有序性,就会进入 Sort Aggregate 模式——它必须把所有匹配行拉出来,按去重字段排序(或建哈希表),再逐个归并计数。一旦去重后基数超过几十万,Using filesort 和 Using temporary 就必然出现,磁盘临时表随之而来。
常见诱因包括:去重字段无索引、WHERE 条件没走索引、联合去重字段顺序与索引不匹配、字段类型导致索引失效(如对 TEXT 或函数结果用 DISTINCT)。
如何让COUNT(DISTINCT)跳过排序阶段
核心是让 MySQL 走“松散索引扫描(Loose Index Scan)”——即直接遍历索引树的每个分支,不用读全叶子节点。这要求:去重字段必须是索引最左前缀,且查询不带非范围 WHERE 条件干扰。
- 对单列去重,建普通索引即可:
CREATE INDEX idx_user_id ON orders(user_id); - 对多列组合去重(如
COUNT(DISTINCT user_id, product)),必须建联合索引且顺序一致:CREATE INDEX idx_user_product ON orders(user_id, product); - 若已有
WHERE created_at > '2024-01-01',索引应前置过滤字段:CREATE INDEX idx_created_user ON orders(created_at, user_id); - 避免在索引字段上用函数:
WHERE UPPER(email) = 'A@B.COM'会让idx_email失效
当排序已不可避免时,怎么减少伤害
如果去重后结果集预估超 10 万(即“中集合”以上),硬扛排序代价太高,得换策略:
- 改用子查询 +
GROUP BY:写成SELECT COUNT(*) FROM (SELECT user_id FROM orders WHERE ... GROUP BY user_id) t,有时能更好复用索引,尤其当WHERE条件和分组字段共用联合索引时 - 调大内存参数治标:临时提高
tmp_table_size和max_heap_table_size,避免内存临时表溢出到磁盘;但别设过高,否则挤占 buffer pool - 采样估算保实时性:业务允许误差时,用
LIMIT随机采样 1% 数据,再按比例放大,例如:SELECT COUNT(DISTINCT user_id) * 100 FROM (SELECT user_id FROM orders TABLESAMPLE SYSTEM (1)) t - 拒绝实时算:把去重逻辑搬到写入侧,比如用 Flink 实时维护 HyperLogLog 结构,查时直取
PFCOUNT值
最容易被忽略的性能信号
不是慢才要优化,而是看到 EXPLAIN FORMAT=TREE 输出里出现 Using temporary; Using filesort 就该警觉——哪怕当前只耗时 200ms,数据翻倍后很可能变成秒级甚至分钟级。
另一个盲区是:COUNT(DISTINCT) 在 GROUP BY 子句里嵌套使用(如统计每个渠道的去重用户数),此时每组都可能触发独立排序,压力呈线性放大。这种场景下,预计算汇总表或物化视图比任何 SQL 技巧都管用。











