distinct子查询常被误用为“去重万能解”,因其仅按整行值去重,不理解业务语义(如取最新记录),且在join、in等场景易引发笛卡尔积或性能爆炸;应优先用group by替代,并将过滤、分页下推至子查询内。

为什么DISTINCT子查询常被误用为“去重万能解”
很多人写 SELECT * FROM (SELECT DISTINCT a, b FROM t1 JOIN t2 ON ...) 是想先去重再查,但实际效果往往和直接写 COUNT(DISTINCT a) 差不多,甚至更慢。根本问题在于:DISTINCT 作用的是整个子查询输出行,它不理解业务语义——比如“每个用户最新一条记录”,DISTINCT 只认字段值完全一致才去重,不会按时间选最大值。
常见错误现象:
- 子查询带 JOIN 后加
DISTINCT,结果行数仍爆炸,因为重复是连接产生的,不是数据本身重复 - 外层 SELECT 多加了字段(如
ROW_NUMBER()或计算列),导致DISTINCT失效 - 子查询结果用于
IN或JOIN,但没做显式去重,引发笛卡尔积放大
用 GROUP BY 替代子查询里的 DISTINCT 更可靠
当目标只是取唯一组合(如维度枚举值),GROUP BY 比 DISTINCT 更可控,且 MySQL 8.0+ 对 GROUP BY 的物化支持更好。关键是要让优化器明确知道“这组键天然唯一”。
实操建议:
- 把
SELECT DISTINCT category FROM products改成SELECT category FROM products GROUP BY category,两者语义等价,但后者在有索引时可能跳过排序 - 如果字段上有唯一索引或主键,MySQL 可能自动启用“松散索引扫描”,避免临时表
- 别在子查询里混用
DISTINCT和聚合函数(如MAX(created_at)),该用GROUP BY就用到底
子查询结果用于 JOIN 时必须显式去重
LEFT JOIN 或 INNER JOIN 时,若子查询未去重,匹配会放大主表行数。例如 SELECT u.* FROM users u JOIN (SELECT user_id FROM orders) o ON u.id = o.user_id,如果 orders 表里一个 user_id 出现 10 次,u 行就复制 10 份。
正确做法:
- 子查询必须带
DISTINCT或GROUP BY,例如(SELECT DISTINCT user_id FROM orders) - JOIN 别名字段判空必须写成
o.user_id IS NULL,不能写o.* IS NULL(语法错误) - 如果子查询含聚合(如统计订单数),就把聚合提前:
(SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id)
WHERE 条件必须下推到子查询内部
子查询没加过滤条件,等于让数据库先算出全量去重结果,再在外层筛选——这是性能杀手。尤其当子查询涉及百万级表时,临时表一落地就是磁盘 IO 爆炸。
典型反例:SELECT * FROM users WHERE id IN (SELECT user_id FROM orders) —— 即使你只想查最近 7 天的活跃用户,子查询仍扫全表。
优化要点:
- 把时间、状态等过滤条件直接写进子查询:
(SELECT DISTINCT user_id FROM orders WHERE create_time > '2026-07-14') - 确保子查询过滤字段上有索引,否则
DISTINCT或GROUP BY仍会触发 filesort - 如果子查询结果要分页,
LIMIT必须放在子查询里,否则外层仍要处理全部去重结果
真正卡住性能的,往往不是 DISTINCT 本身,而是它被套在错误层级、没配合过滤、又没对齐业务语义。把去重意图拆清楚:是取唯一键?是统计基数?还是选最新一行?选错手段,优化就南辕北辙。











