PostgreSQL DISTINCT ON 取每设备最新上报:索引与执行计划对比

举报
yd_229231784 发表于 2026/11/02 07:09:32 2026/11/02
【摘要】 本文用 PostgreSQL 的 DISTINCT ON 配合 ORDER BY 取每台设备最新一条上报记录,替代 row_number() 窗口子查询,给出建表、复合索引和 EXPLAIN ANALYZE 对比方法,并说明排序、扫描行数与边界限制。

问题场景

设备上报表通常持续写入,每个 device_id 有多条记录。业务要取每台设备最新一条上报,常见写法是用 row_number() 窗口函数按 device_id 分区、按 reported_at 降序编号,再筛 rn=1。这个写法语义清楚,但通常要为全部行计算窗口编号并排序。PostgreSQL 提供了 DISTINCT ON,它按指定表达式保留排序后的第一行,配合 (device_id, reported_at DESC) 复合索引,有机会让索引提供顺序,减少显式排序和窗口计算。本文给出最小可运行示例和 EXPLAIN ANALYZE 对比方法。

需要注意:DISTINCT ON 并不会自动减少需要读取的行数,它主要影响排序和去重方式;实际计划要以 EXPLAIN ANALYZE 为准。

建表、数据与索引

以下脚本可在测试库执行。设备号、时间范围和条数均为占位值,按需替换。

DROP TABLE IF EXISTS device_report;

CREATE TABLE device_report (
    id          bigserial PRIMARY KEY,
    device_id   integer       NOT NULL,
    reported_at timestamptz   NOT NULL,
    metric      numeric(10,2) NOT NULL
);

INSERT INTO device_report (device_id, reported_at, metric)
SELECT d,
       timestamptz '2025-01-01 00:00:00+00' - (g || ' minutes')::interval,
       (d * 100 + g % 100)::numeric(10,2)
FROM generate_series(1, 5) AS d
CROSS JOIN generate_series(1, 1000) AS g;

CREATE INDEX idx_device_report_device_time
    ON device_report (device_id, reported_at DESC, id DESC);

ANALYZE device_report;

索引列顺序与查询的 ORDER BY 对齐:先 device_id,再 reported_at DESC,最后 id DESC 作为同一时间的兜底排序。若业务不需要同一时间多条去重,可以去掉 id 列;但为了保证结果确定,建议保留 tie-breaker。

两种写法

写法 A:row_number() 窗口子查询。

EXPLAIN (ANALYZE, BUFFERS)
SELECT device_id, reported_at, id, metric
FROM (
    SELECT id,
           device_id,
           reported_at,
           metric,
           row_number() OVER (
               PARTITION BY device_id
               ORDER BY reported_at DESC, id DESC
           ) AS rn
    FROM device_report
) AS t
WHERE rn = 1
ORDER BY device_id;

写法 B:DISTINCT ON。

EXPLAIN (ANALYZE, BUFFERS)
SELECT DISTINCT ON (device_id)
       device_id, reported_at, id, metric
FROM device_report
ORDER BY device_id, reported_at DESC, id DESC;

DISTINCT ON 的表达式必须与 ORDER BY 最左侧表达式一致;这里都是 device_id。ORDER BY 后面的 reported_at DESC, id DESC 决定每组保留哪一行。若写成 ORDER BY reported_at DESC 而 DISTINCT ON (device_id),会报错或语义不符合预期。

运行与计划对比

在 psql 中执行:

psql -h <host> -U <user> -d <db> -f demo.sql

然后分别执行两条 EXPLAIN 语句。重点观察:

  • 节点结构:DISTINCT ON 常见为 Unique 上接 Index Scan;row_number 常见为 WindowAgg 上接 Sort 或 Incremental Sort。具体以实际计划为准。
  • 扫描行数:看 Seq Scan/Index Scan 的 rows 与 actual rows,以及两种写法是否读取了相同的行数。
  • 排序开销:看 Sort Method、Sort Key、Memory,确认是否出现显式 Sort。
  • 缓冲区:看 Buffers 的 shared hit/read,判断回表与缓存影响。

如果数据量很小,规划器可能对两种写法都选择 Seq Scan + Sort,这是成本估算的结果,不代表 DISTINCT ON 无效。可以先用 ANALYZE 更新统计信息,再在测试环境观察;不要在生产环境随意关闭 enable_seqscan 来“制造”索引计划。

另外,DISTINCT ON 仍要消费有序输入中的全部行才能输出所有设备,因此不要期待扫描行数一定下降。它的收益主要来自避免窗口函数计算和显式排序;是否真正更快,需要结合设备数、每设备记录数、索引回表成本和缓存命中情况判断。

边界与限制

  1. 同一设备同一 reported_at 可能有多条。必须加 id DESC 之类的 tie-breaker,并同步到索引,否则“最新一条”不唯一。
  2. 若只要全局最新一条,不要用 DISTINCT ON,直接 ORDER BY reported_at DESC LIMIT 1 更直接。
  3. 若每台设备要取前 N 条,DISTINCT ON 不适合,窗口函数或 LATERAL 子查询更合适。
  4. 带时间范围过滤时,索引仍可能被使用,但 ORDER BY 的全局有序性与过滤条件可能让规划器改选其他计划,需要用真实数据验证。
  5. 复合索引会增加写入和维护成本;只为高频查询创建。若查询列较少,可考虑 INCLUDE 覆盖列,但要权衡索引体积。
  6. EXPLAIN ANALYZE 会实际执行语句。生产环境只读查询也应评估影响;写语句请用事务回滚或仅用 EXPLAIN。

小结

DISTINCT ON 配合匹配的复合索引,是取每设备最新一条记录的简洁写法。它不能保证扫描行数更少,但通常能减少窗口计算和显式排序。用 EXPLAIN ANALYZE 对比节点、扫描行数、排序方法和缓冲区,再决定是否替换 row_number() 写法。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。