DISTINCT ON (PostgreSQL) 与 ROW_NUMBER() 的性能对比

9次阅读

多数情况下 DISTINCT ON 更快——它在扫描时就去重,无需额外排序或窗口计算;而 ROW_NUMBER() 需先生成全序列再过滤,开销更大,但前提是索引覆盖 DISTINCT ON 和 ORDER BY 的列顺序。

DISTINCT ON (PostgreSQL) 与 ROW_NUMBER() 的性能对比

postgresql 里 DISTINCT ON 和 ROW_NUMBER() 哪个更快?

多数情况下 DISTINCT ON 更快——它在扫描时就做去重,不需额外排序或窗口计算;而 ROW_NUMBER() 必须先生成完整序号列,再过滤,多一次逻辑读和内存开销。但这个结论有前提:查询能利用索引支持 DISTINCT ON 的排序字段。

DISTINCT ON 能用上索引的关键条件

DISTINCT ON 的性能优势依赖索引能否覆盖其排序逻辑。它等价于“对每组取排序后第一行”,所以 PostgreSQL 会尝试用索引跳过重复扫描。

  • 必须有索引以 DISTINCT ON (col1) 的列开头,且后续包含 ORDER BY col1, col2 中的列(顺序一致)
  • 例如:DISTINCT ON (user_id) ORDER BY user_id, created_at DESC 需要索引 ON posts (user_id, created_at DESC)
  • 如果 ORDER BYDISTINCT ON 列顺序不一致(如 DISTINCT ON (a) ORDER BY b),就无法用索引加速,退化为全表扫描 + 排序
  • 复合条件中带非前导列过滤(如 WHERE status = 'active')时,除非该列也在索引前缀中,否则可能跳过索引

ROW_NUMBER() 在什么场景下反而更合适?

当你要取“每组第 N 行”(不只是第一行),或需要基于复杂条件动态决定排序优先级时,ROW_NUMBER() 是唯一选择;DISTINCT ON 只能固定取第一行。

  • 需要分页:比如“每个用户最新两条帖子”,ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) + WHERE rn
  • 排序逻辑无法静态表达:比如按“有图片优先、再按时间”,需在 ORDER BY 里写 CASE WHEN image_url IS NOT NULL THEN 0 ELSE 1 END, created_at DESC,这种 DISTINCT ON 也能写,但可读性和维护性差
  • 需要后续对序号做计算:比如只取奇数行、或跨组累计计数,ROW_NUMBER() 提供的是稳定可复用的数值,DISTINCT ON 没有中间值
  • 注意:若仅为了取第一行却用了 ROW_NUMBER(),且没加合适的索引,执行计划常出现 WindowAgg + Sort,比 Unique + Index Scan 多出 2–5 倍执行时间

实测性能差异的典型表现

在千万级 posts 表(含 user_id 索引)上测试“每个用户最新一篇”:

EXPLAIN ANALYZE SELECT DISTINCT ON (user_id) * FROM posts ORDER BY user_id, created_at DESC;

Index Scan Backward using idx_user_created,耗时约 120ms;

EXPLAIN ANALYZE SELECT * FROM (   SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn   FROM posts ) t WHERE rn = 1;

WindowAgg → Sort → Seq Scan,耗时约 480ms(无索引优化时可能超 2s)。

真正容易被忽略的是:即使你写了 ROW_NUMBER(),只要外层只取 rn = 1,PostgreSQL 9.6+ 其实能自动优化成类似 DISTINCT ON 的执行路径——但前提是 PARTITION BYORDER BY 列完全匹配索引,且没有其他干扰条件(如函数包裹、隐式类型转换)。一旦触发不了这个优化,性能落差就实实在在落在慢查询日志里了。

text=ZqhQzanResources