概述
时间序列表有两个随数据变老而恶化的问题:查询必须跳过大量历史行,删除又必须重写整张表。按范围分区可以同时解决两者。查询规划器会剪掉不可能命中的分区,一周的查询只访问一个分区;删除数据改为 DROP TABLE 分区,立即释放文件,而 DELETE 会留下死元组、膨胀索引并和前台流量争抢资源。
步骤
- 创建分区父表,主键必须包含分区键:
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY,
tenant_id uuid NOT NULL,
occurred_at timestamptz NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (id, occurred_at)
) PARTITION BY RANGE (occurred_at);
- 按月建立分区,并保留一个 default 分区兜底:
CREATE TABLE events_2026_09 PARTITION OF events
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE events_2026_10 PARTITION OF events
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
CREATE TABLE events_default PARTITION OF events DEFAULT;
-
只在父表上建索引,PostgreSQL 11 起会自动为已有分区与后续新分区创建对应索引。
-
用
EXPLAIN (ANALYZE, BUFFERS)验证带时间范围的查询只扫描目标分区。
验证
对 2026 年 10 月 1 日至 8 日的查询应只在计划中看到 events_2026_10,旧分区与 events_default 均被剪掉。再用 \d+ events_2026_10 确认索引存在。删除一个历史月份用 DROP TABLE events_2025_01;,应在毫秒级完成且没有 vacuum 积压。
注意事项
- 主键必须包含分区键,
PRIMARY KEY (id)会变成PRIMARY KEY (id, occurred_at),指向该表的外键也要同步调整。 - 若必须保证
id全局唯一,需要额外的序列表或 UUID 方案。 - 过滤条件不能包裹分区键,
date_trunc、转成date等写法会让分区剪枝失效。 - default 分区只作兜底:挂载新分区时需要扫描它,且它可能藏住本应进入其他分区的数据。
- 分区数量过多会增加规划时间,建议按月并归档历史分区,同时监控 default 分区的行数。