distinct变慢是因为它本质是隐式group by,需全量排序或哈希去重,中间结果可能膨胀;多字段、大类型、无索引覆盖时性能骤降,优化需匹配联合索引最左前缀且含全部select字段。

为什么 DISTINCT 一加就变慢?
因为 DISTINCT 不是“过滤器”,它本质是隐式 GROUP BY —— 数据库必须把所有目标列值全拉出来、排序或建哈希表去重,中间结果集可能比原表还大。尤其当 SELECT 后跟了多个字段、或字段含 TEXT/json 类型时,内存和临时磁盘压力会陡增。
- 常见错误现象:
EXPLAIN显示using temporary; Using filesort,查询耗时从毫秒跳到秒级 - 典型场景:报表导出、后台搜索下拉列表、聚合前的唯一 ID 提取
- 参数差异:mysql 8.0+ 对
DISTINCT加了哈希优化,但 postgresql 仍默认走排序;sqlite 则对单列DISTINCT有索引短路优化,多列无效 - 性能影响:加了
DISTINCT后无法利用覆盖索引(除非索引包含全部 SELECT 字段)
DISTINCT 能不能用索引加速?
能,但条件很具体:必须是 SELECT 的所有字段,都出现在同一个联合索引的最左前缀中,且查询没带 WHERE 条件或 WHERE 条件也命中该索引。
- 例如表
orders(user_id, status, created_at),执行SELECT DISTINCT user_id, status FROM orders可走索引;但SELECT DISTINCT status, user_id就不行(顺序不匹配) - 如果加了
WHERE created_at > '2024-01-01',而索引是(user_id, status),那依然用不上——created_at不在索引里 - 别指望
ORDER BY和DISTINCT共享索引优化:MySQL 5.7 不支持,8.0+ 仅当ORDER BY字段完全被DISTINCT字段包含时才可能复用
替代 DISTINCT 的三种实操方案
不是所有去重要求都非得靠 DISTINCT。根据数据语义,常可换更轻量的方式。
- 用
GROUP BY显式替代:语义等价,但方便后续加MIN()/count()等聚合,且某些引擎(如 clickhouse)对GROUP BY优化更好 - 用子查询 +
ROW_NUMBER()去重:适合需要“每个分组取最新一条”的场景,比如SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) rn FROM logs) t WHERE rn = 1 - 用
EXISTS替代多表DISTINCT:比如查“买过商品 A 的用户”,别写SELECT DISTINCT u.id FROM users u JOIN orders o ON u.id = o.user_id JOIN items i ON o.item_id = i.id WHERE i.name = 'A',改用SELECT u.id FROM users u WHERE EXISTS (SELECT 1 FROM orders o JOIN items i ON o.item_id = i.id WHERE o.user_id = u.id AND i.name = 'A'),避免笛卡尔积放大
容易被忽略的 NULL 和类型隐式转换陷阱
DISTINCT 把 NULL 当作相同值处理,这没错;但一旦字段存在隐式类型转换(比如字符串字段存数字、或 JSON 字段直接参与比较),结果就不可控。
- 常见错误现象:明明看到两行都是
'123',DISTINCT却没去重——其实是其中一行是123(整型),另一行是'123'(字符串),数据库按类型区分 - PostgreSQL 中
jsonb字段的DISTINCT比较是深比较,但json是文本比较,字段类型选错会导致去重失效 - MySQL 严格模式下,
VARCHAR(10)和VARCHAR(20)在DISTINCT中视为不同类型;非严格模式可能自动补空格,造成误去重
真正麻烦的从来不是语法会不会写,而是你根本没意识到字段里混着 NULL、空字符串、Unicode 空格、JSON 格式不一致这些“安静的脏数据”——它们会让 DISTINCT 表现出发散行为,而且很难一眼看出来。