← 中文资源

简体中文版本

按时间范围对大表做分区

把持续增长的 events 表改为按月分区,保留索引继承,并验证查询只扫描所需分区。

阅读英文原文

概述

时间序列表有两个随数据变老而恶化的问题:查询必须跳过大量历史行,删除又必须重写整张表。按范围分区可以同时解决两者。查询规划器会剪掉不可能命中的分区,一周的查询只访问一个分区;删除数据改为 DROP TABLE 分区,立即释放文件,而 DELETE 会留下死元组、膨胀索引并和前台流量争抢资源。

步骤

  1. 创建分区父表,主键必须包含分区键:
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);
  1. 按月建立分区,并保留一个 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;
  1. 只在父表上建索引,PostgreSQL 11 起会自动为已有分区与后续新分区创建对应索引。

  2. 用 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 分区的行数。