SQL DISTINCT 去重优化实践

1次阅读

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

SQL DISTINCT 去重优化实践

为什么 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 BYDISTINCT 共享索引优化: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 和类型隐式转换陷阱

DISTINCTNULL 当作相同值处理,这没错;但一旦字段存在隐式类型转换(比如字符串字段存数字、或 JSON 字段直接参与比较),结果就不可控。

  • 常见错误现象:明明看到两行都是 '123'DISTINCT 却没去重——其实是其中一行是 123整型),另一行是 '123'(字符串),数据库按类型区分
  • PostgreSQL 中 jsonb 字段的 DISTINCT 比较是深比较,但 json 是文本比较,字段类型选错会导致去重失效
  • MySQL 严格模式下,VARCHAR(10)VARCHAR(20)DISTINCT 中视为不同类型;非严格模式可能自动补空格,造成误去重

真正麻烦的从来不是语法会不会写,而是你根本没意识到字段里混着 NULL、空字符串、Unicode 空格、JSON 格式不一致这些“安静的脏数据”——它们会让 DISTINCT 表现出发散行为,而且很难一眼看出来。

text=ZqhQzanResources