DISTINCT在嵌套查询中不去重是因执行路径失控,而非语法错误;应优先用EXISTS替代IN、GROUP BY预聚合代替外层DISTINCT、必要时用临时表物化中间结果。

DISTINCT 在嵌套查询里根本不去重,是因为它被“包在中间”了
你写了个 SELECT COUNT(*) FROM (SELECT DISTINCT ... FROM ...) t,以为外层 COUNT 就能拿到去重后的行数——但实际执行时,Oracle 很可能把 DISTINCT 推入内层物化(尤其在复杂 JOIN 场景下),而外层只是对已去重结果再 COUNT。问题在于:如果内层子查询本身因 JOIN 膨胀出 10 万行,DISTINCT 就得对这 10 万行做全字段比对和哈希去重,I/O 和内存开销巨大。这不是语法错,是执行路径失控。
用 EXISTS 替代 IN 子查询避免隐式膨胀
常见写法 WHERE id IN (SELECT user_id FROM events) 看似简洁,但 Oracle 可能将其重写为 FILTER + NESTED LOOPS,对主表每行都执行一次子查询,且子查询若返回重复 user_id,还可能触发多次匹配。换成 EXISTS 后,语义明确为“半连接”,只要找到一条匹配就短路退出,不拉数据、不拼行。
- 必须确保
events.user_id有索引,否则EXISTS可能比IN更慢 - 子查询里写
SELECT 1或SELECT NULL,别用SELECT *—— Oracle 不会优化掉字段投影,徒增解析和传输开销 - 如果子查询带
ORDER BY或LIMIT(如 MySQL),Oracle 会直接报错;Oracle 中只需保证关联条件清晰即可
用 GROUP BY 预聚合代替外层 DISTINCT
当你要统计“每个用户最近一次登录的设备类型数量”,写成 SELECT COUNT(DISTINCT user_id, device_type) FROM (... JOIN ... ORDER BY login_time DESC) 是错的:DISTINCT 对整行生效,而 login_time 天然唯一,导致永远不重复。正确做法是先用 GROUP BY user_id 拿到每个用户的最新记录,再对外层结果去重计数。
- 预聚合 SQL 示例:
SELECT COUNT(DISTINCT device_type) FROM ( SELECT user_id, device_type, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_time DESC) rn FROM login_logs ) t WHERE rn = 1 - 比
DISTINCT嵌套快,因为ROW_NUMBER()只需一次排序 + 窗口扫描,而DISTINCT需二次哈希 - 注意
PARTITION BY字段必须有索引,否则窗口函数排序成本飙升
临时表物化中间结果比反复跑子查询更稳
当嵌套层级 ≥3,或子查询被多个外层引用(比如同时用于 SELECT、WHERE、JOIN),Oracle 的 CBO 很可能对同一子查询重复执行三次。这时显式建临时表,把去重逻辑固化下来,反而更快。
- 用
CREATE GLOBAL TEMPORARY TABLE t_user_dist AS SELECT DISTINCT user_id FROM events,然后所有后续查询都基于t_user_dist - 临时表默认
ON COMMIT DELETE ROWS,不用手动清理;如需跨事务保留,加ON COMMIT PRESERVE ROWS - 别忘了给临时表的关联字段(如
user_id)建索引,否则 JOIN 时还是走全表扫描
真正卡性能的从来不是 DISTINCT 这个关键字,而是你没控制住数据膨胀的源头——JOIN 条件漏写、子查询没收敛、索引缺失。先看执行计划里的 VIEW 步骤是否物化,再决定要不要上临时表或窗口函数。











