PostgreSQL 20 让 UUID 也能直接 min() / max()

2026-07-09 36 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

PostgreSQL 20 合入了一个小而实用的补齐:uuid 类型将支持 min()max() 聚合。这个变化不算炫目,但会减少很多查询里的绕路写法。因为 uuid 本来已经有完整比较操作符,也有 btree operator class,换句话说,它在 PostgreSQL 里早就是可排序的;缺的只是常用聚合函数这块拼图。

为什么 UUID 可以有“最小”和“最大”

很多人下意识觉得 UUID 是随机标识,不该谈大小。但数据库里的“大小”不是业务含义,而是类型的排序规则。

PostgreSQL 的 uuid 类型已经支持比较操作,例如:

SELECT '00000000-0000-0000-0000-000000000001'::uuid
     < 'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid AS is_smaller;

它也能建 btree 索引,说明 PostgreSQL 已经知道如何对 uuid 排序:

CREATE TABLE events (
  id uuid PRIMARY KEY,
  payload text NOT NULL
);

CREATE INDEX events_id_btree_idx ON events USING btree (id);

既然能比较、能排序、能走 btree,min(id)max(id) 缺席就显得不自然。PostgreSQL 20 的补丁补上了这点,并加入了内部用于比较的 uuid_larger()uuid_smaller()

以前怎么写,之后怎么写

在 PostgreSQL 20 之前,如果你想拿一列 UUID 的最小值或最大值,常见做法是通过 ORDER BY ... LIMIT 1

SELECT id
FROM events
ORDER BY id ASC
LIMIT 1;

SELECT id
FROM events
ORDER BY id DESC
LIMIT 1;

这当然能工作,而且在有合适索引时也很高效。但它不是聚合表达式,放进分组、窗口、报表 SQL 时会变得别扭。

PostgreSQL 20 之后,可以直接写成:

SELECT min(id) AS first_uuid,
       max(id) AS last_uuid
FROM events;

分组场景也更自然:

SELECT tenant_id,
       min(id) AS min_event_id,
       max(id) AS max_event_id,
       count(*) AS event_count
FROM events
GROUP BY tenant_id;

这类查询读起来更像“按租户统计 ID 范围”,而不是“每组再找排序后的第一条和最后一条”。

可以这样实践:用 SQL 观察差异

下面的例子可以在 PostgreSQL 20 或更新版本上直接运行。若你还在 PostgreSQL 19 或更早版本,最后的 min(id) / max(id) 查询会报错,这正好能说明这个变化解决了什么。

DROP TABLE IF EXISTS demo_uuid_events;

CREATE TABLE demo_uuid_events (
  tenant_id text NOT NULL,
  id uuid NOT NULL,
  payload text NOT NULL
);

INSERT INTO demo_uuid_events (tenant_id, id, payload) VALUES
  ('acme', '00000000-0000-0000-0000-000000000010', 'created'),
  ('acme', '00000000-0000-0000-0000-000000000003', 'updated'),
  ('acme', 'ffffffff-ffff-ffff-ffff-ffffffffffff', 'closed'),
  ('beta', '11111111-1111-1111-1111-111111111111', 'created'),
  ('beta', '22222222-2222-2222-2222-222222222222', 'updated');

SELECT tenant_id,
       min(id) AS min_id,
       max(id) AS max_id,
       count(*) AS rows
FROM demo_uuid_events
GROUP BY tenant_id
ORDER BY tenant_id;

预期结果会类似:

 tenant_id |                min_id                |                max_id                | rows
-----------+--------------------------------------+--------------------------------------+------
 acme      | 00000000-0000-0000-0000-000000000003 | ffffffff-ffff-ffff-ffff-ffffffffffff |    3
 beta      | 11111111-1111-1111-1111-111111111111 | 22222222-2222-2222-2222-222222222222 |    2

如果你想顺手确认索引排序能力,可以加一个 btree 索引,然后看 ORDER BY ... LIMIT 1 的计划:

CREATE INDEX demo_uuid_events_id_idx ON demo_uuid_events (id);

EXPLAIN
SELECT id
FROM demo_uuid_events
ORDER BY id
LIMIT 1;

注意:这个例子只是说明 uuid 本身有确定排序规则,不等于承诺 min(uuid) 在所有数据量和统计信息状态下都会选择同一种执行计划。实际计划仍然取决于表大小、索引、统计信息和查询形状。

这不是“按时间排序”的替代品

min(uuid)max(uuid) 表示 UUID 排序规则下的边界值,不表示最早创建或最新创建。

这点尤其重要。很多系统用 UUID v4 作为随机主键,此时最大 UUID 没有“最新”的含义。即使使用带时间信息的 UUID 变体,也要确认数据库排序规则是否与你想表达的时间顺序一致。业务上要查最新事件,仍然应该用明确的时间列:

SELECT tenant_id,
       max(created_at) AS latest_event_time
FROM events
GROUP BY tenant_id;

如果你需要“每个租户最新事件的 UUID”,可以把时间排序写清楚:

SELECT DISTINCT ON (tenant_id)
       tenant_id,
       id,
       created_at
FROM events
ORDER BY tenant_id, created_at DESC, id DESC;

UUID 聚合解决的是类型能力和 SQL 表达一致性,不会替你推断业务语义。

升级时怎么用

这项变化适合逐步吸收,不需要为了它大改 SQL。

可以优先改这些地方:

  • 报表 SQL 里已经按实体分组,并且需要展示 UUID 边界值。
  • 应用代码里为了规避 min(uuid) 缺失而写了子查询或 ORDER BY ... LIMIT 1
  • 通用查询生成器对不同可排序类型统一生成 min() / max(),但之前必须给 uuid 加例外分支。

需要保留边界意识:

  • 如果代码必须兼容 PostgreSQL 19 或更早版本,不要直接生成 min(uuid) / max(uuid)
  • 不要把 UUID 最大值当作“最新记录”。
  • 性能判断仍要看 EXPLAIN,尤其是大表、分区表和复杂分组查询。

这是一个典型的 PostgreSQL 小步增强:不改变你的数据模型,却让类型系统和 SQL 体验更完整。uuid 已经能排序,现在终于也能像其他常见可排序类型一样参与 min()max() 了。


相关推荐