为什么SQL查询中用COUNT(DISTINCT)去重计数性能那么糟糕?
- 内容介绍
- 相关推荐
本文共计649个文字,预计阅读时间需要3分钟。
基本原因在于执行路径的不同:
子查询写法必须满足的三个前提条件
不是所有 SELECT COUNT(*) FROM (SELECT DISTINCT ...) 都快,它只在以下情况真正生效:
- WHERE 条件足够强,能大幅减少输入行数(比如
dt >= '2026-04-15',而不是status IN ('a','b','c')这种低选择率条件) - 去重字段
col有单列索引或作为联合索引最左前缀(否则子查询仍要全表扫) - 子查询结果集行数远小于原始表(理想是 COUNT(*) 反而多一次遍历
MySQL中容易被忽略的索引陷阱
即使你给 user_id 加了索引,COUNT(DISTINCT user_id) 仍可能不走索引——因为 MySQL 优化器发现去重逻辑无法跳过 NULL 值判断或需要回表取其他字段时,会主动放弃索引扫描。但子查询方式可以绕过这个限制:
✅ 正确建索引示例:CREATE INDEX idx_dt_uid ON log (dt, user_id),配合查询 SELECT COUNT(*) FROM (SELECT DISTINCT user_id FROM log WHERE dt BETWEEN '2026-04-18' AND '2026-04-22') t
⚠️ 错误用法:如果子查询里写了 SELECT DISTINCT user_id, name FROM ...,哪怕只为了去重 user_id,也会因 name 不在索引中触发回表,性能反而更差
Spark SQL里用子查询替代COUNT(DISTINCT)要防Expand膨胀
Spark 中多个 COUNT(DISTINCT) 会触发 Expand 节点,把 1 行变成 N 行(N=去重字段数),导致 shuffle 数据量暴增。而子查询方式天然规避该问题:
❌ 慢写法:SELECT dt, COUNT(DISTINCT uid), COUNT(DISTINCT aid) FROM events GROUP BY dt
✅ 快写法(拆开):SELECT a.dt, a.uv, b.av FROM (SELECT dt, COUNT(*) AS uv FROM (SELECT DISTINCT dt, uid FROM events WHERE dt >= '2026-04-18') GROUP BY dt) a JOIN (SELECT dt, COUNT(*) AS av FROM (SELECT DISTINCT dt, aid FROM events WHERE dt >= '2026-04-18') GROUP BY dt) b ON a.dt = b.dt
注意:这种拆分在 Spark 中实际是分两轮 job 执行,虽然总耗时未必减半,但避免了单 job 因 Expand 导致的严重数据倾斜和 OOM
真正卡住性能的,往往不是语法本身,而是去重发生在哪一层——是在扫描 10 亿行时边读边去重,还是先用索引筛出 10 万行再丢进哈希表。选错层级,优化就从根上失效。
本文共计649个文字,预计阅读时间需要3分钟。
基本原因在于执行路径的不同:
子查询写法必须满足的三个前提条件
不是所有 SELECT COUNT(*) FROM (SELECT DISTINCT ...) 都快,它只在以下情况真正生效:
- WHERE 条件足够强,能大幅减少输入行数(比如
dt >= '2026-04-15',而不是status IN ('a','b','c')这种低选择率条件) - 去重字段
col有单列索引或作为联合索引最左前缀(否则子查询仍要全表扫) - 子查询结果集行数远小于原始表(理想是 COUNT(*) 反而多一次遍历
MySQL中容易被忽略的索引陷阱
即使你给 user_id 加了索引,COUNT(DISTINCT user_id) 仍可能不走索引——因为 MySQL 优化器发现去重逻辑无法跳过 NULL 值判断或需要回表取其他字段时,会主动放弃索引扫描。但子查询方式可以绕过这个限制:
✅ 正确建索引示例:CREATE INDEX idx_dt_uid ON log (dt, user_id),配合查询 SELECT COUNT(*) FROM (SELECT DISTINCT user_id FROM log WHERE dt BETWEEN '2026-04-18' AND '2026-04-22') t
⚠️ 错误用法:如果子查询里写了 SELECT DISTINCT user_id, name FROM ...,哪怕只为了去重 user_id,也会因 name 不在索引中触发回表,性能反而更差
Spark SQL里用子查询替代COUNT(DISTINCT)要防Expand膨胀
Spark 中多个 COUNT(DISTINCT) 会触发 Expand 节点,把 1 行变成 N 行(N=去重字段数),导致 shuffle 数据量暴增。而子查询方式天然规避该问题:
❌ 慢写法:SELECT dt, COUNT(DISTINCT uid), COUNT(DISTINCT aid) FROM events GROUP BY dt
✅ 快写法(拆开):SELECT a.dt, a.uv, b.av FROM (SELECT dt, COUNT(*) AS uv FROM (SELECT DISTINCT dt, uid FROM events WHERE dt >= '2026-04-18') GROUP BY dt) a JOIN (SELECT dt, COUNT(*) AS av FROM (SELECT DISTINCT dt, aid FROM events WHERE dt >= '2026-04-18') GROUP BY dt) b ON a.dt = b.dt
注意:这种拆分在 Spark 中实际是分两轮 job 执行,虽然总耗时未必减半,但避免了单 job 因 Expand 导致的严重数据倾斜和 OOM
真正卡住性能的,往往不是语法本身,而是去重发生在哪一层——是在扫描 10 亿行时边读边去重,还是先用索引筛出 10 万行再丢进哈希表。选错层级,优化就从根上失效。

