DISTINCT本质是隐式GROUP BY,需全量加载SELECT字段值建哈希表或排序去重,无法跳过或索引短路;混用JOIN/子查询时退化为排序,触发临时表落盘、I/O激增及谓词下推失效。

DISTINCT 必须全量加载结果行进内存去重,无法跳过或索引短路
它不是过滤器,而是隐式 GROUP BY:数据库得先把所有 SELECT 字段的值完整拉出来,再建哈希表或排序缓冲区做逐行比对。哪怕你只想要前 10 行,只要没索引支撑,就得把几百万行全读进内存——临时表、排序、哈希结构全得占空间。
- MySQL 8.0+ 对单表 DISTINCT 可启用哈希聚合,但一旦混用 JOIN 或子查询,立刻退化为排序路径
- PostgreSQL 默认走排序,不自动哈希;SQLite 只对单列 DISTINCT 有索引短路可能,多列无效
- 字段含 TEXT、JSON 或多列组合时,每行内存开销成倍增长,极易触发
tmp_table_size溢出 → 落盘临时表 → I/O 暴增
DISTINCT 关闭谓词下推,中间结果被迫物化
子查询或视图里加了 DISTINCT,外层 WHERE 或 LIMIT 就基本失效。比如 SELECT * FROM (SELECT DISTINCT a, b FROM t JOIN s ON ...) WHERE a = 'x',MySQL 会先物化整个 JOIN 结果(50 万行),再过滤,完全绕过 a 上的索引。
- EXPLAIN 中出现
select_type = DERIVED且rows接近底层表总行数,就是物化信号 - 视图定义含 DISTINCT / GROUP BY / UNION,MySQL 强制走 TEMPTABLE 算法,不尝试 MERGE
- SHOW STATUS LIKE 'Created_tmp_disk_tables' 持续上涨,且与
Created_tmp_tables比值 >15%,说明已频繁落盘
高基数字段让哈希表直接爆内存
当去重字段本身唯一值极多(如 guid、order_id),哈希表无法压缩,内存占用线性膨胀。Hive 在 1G 内存下统计 1 亿 guid 直接 OOM;Spark SQL 每个 task 都要维护独立 HashSet,节点内存打满就 OOM。
- MySQL 的
innodb_buffer_pool_size不够大时,哈希表溢出写磁盘临时文件 - PostgreSQL 的
work_mem设太小,排序阶段变慢,且同样触发落盘 - ClickHouse 是例外:
uniq()底层是 HyperLogLog++,不膨胀;但uniqExact()仍会爆内存
真正卡住的从来不是语法怎么写,而是“要不要精确”。如果业务能接受 ±1% 误差,APPROX_COUNT_DISTINCT 或 uniq() 几乎不占内存;如果必须精确,就得提前物化——比如每天跑一次预聚合,查时直接读结果表。没人会在实时查询里硬扛亿级 DISTINCT,这不是调优问题,是架构选择问题。


















