distinct在join后执行是性能杀手,因其需对join产生的巨大中间结果集去重,导致内存和cpu压力陡增;应优先在join前对主表去重、用子查询提前去重、优化join顺序、或改用group by/exists等轻量方案。

Distinct 在 Join 后执行是性能杀手
SQL 中 DISTINCT 耗时高,根本原因不是去重本身慢,而是它被迫在 JOIN 产生的巨大中间结果集上运行。比如两个各 10 万行的表左连接,若平均匹配 5 条,中间结果可能达 50 万行——DISTINCT 就得对这 50 万行做哈希或排序去重,内存和 CPU 压力陡增。
数据库优化器通常不会主动把 DISTINCT 下推到 Join 前,尤其当字段来自多表、或存在 GROUP BY / ORDER BY 时,语义约束让它只能“等所有 Join 完再动手”。
- 用
EXPLAIN或执行计划看Distinct节点是否出现在Hash Join或Merge Join之后 - 如果
Distinct的目标仅是主表某几列(如SELECT DISTINCT t1.id, t1.name),优先考虑在 Join 前对主表去重 - 避免
SELECT DISTINCT *—— 字段越多,哈希键越宽,缓存局部性越差,去重成本指数上升
用子查询提前去重,但要注意关联字段依赖
把 DISTINCT 搬到子查询里,常能砍掉 70%+ 的执行时间,前提是去重逻辑不破坏业务语义。例如查“有订单的用户信息”,别写:
SELECT DISTINCT u.id, u.name FROM users u JOIN orders o ON u.id = o.user_id;
而应写:
SELECT u.id, u.name FROM users u WHERE u.id IN (SELECT DISTINCT user_id FROM orders);
-
IN子查询中DISTINCT只作用于单表orders.user_id,数据量小、索引可用、几乎无排序开销 - 若需返回订单侧字段(如最新下单时间),改用
EXISTS+ 窗口函数或聚合子查询,避免把订单全量拉进来 - 注意 MySQL 5.7 及更早版本对
IN (SELECT ...)有半连接优化限制,必要时改用JOIN关联派生表
Join 顺序影响 Distinct 的输入规模
即使语义等价,FROM a JOIN b ON ... JOIN c ON ... 的顺序不同,中间结果集大小可能差一个数量级。而 DISTINCT 总是在最终 FROM 链末端执行,所以 Join 顺序直接决定它要处理多少行。
- 让最小的结果集(如带强过滤条件的表)做驱动表,尽早收缩行数
- 避免先
JOIN一对多的大表(如users → user_logs),再连其他表;可先用聚合或去重预处理user_logs - PostgreSQL 支持
LATERAL,可把去重逻辑下推到右表:FROM users u, LATERAL (SELECT DISTINCT category FROM products WHERE user_id = u.id LIMIT 1) p
替代 DISTINCT 的轻量方案:GROUP BY 和 EXISTS
GROUP BY 在某些场景比 DISTINCT 更高效,尤其当字段上有索引且分组键能命中索引前缀时,优化器可能走索引跳扫(index skip scan);而 EXISTS 则完全规避去重,只判断存在性。
- 想查“每个部门是否有高级员工”,用
EXISTS:SELECT dept_id FROM departments d WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id AND e.level = 'senior') - 若必须返回聚合值(如每个用户的订单数),直接
GROUP BY user_id,比DISTINCT+ 子查询更可控 - 注意
GROUP BY隐含排序行为(尤其 MySQL),加ORDER BY NULL可显式禁用
真正棘手的是那些既要多表字段、又要严格去重、还无法提前过滤的场景——这时得靠物化中间结果(如临时表 + 索引)或应用层分页去重,数据库层已逼近优化极限。










