数据库运维8 min read次阅读

PostgreSQL 分区表实战:从分区裁剪到在线运维

什么时候该分区

一张表涨到几千万、上亿行,即使有索引也开始"变肉":

  • 索引膨胀:B+ 树层级加深,单点查询也要多次 IO。
  • VACUUM/ANALYZE 慢:整张大表清理、统计一遍成本极高。
  • 热点数据被冷数据稀释:80% 查询只看最近一个月,却要扫全表索引。
  • 维护锁表ALTER 大表、REINDEX 动辄锁很久。

分区表把一张逻辑大表按规则拆成多个物理子表,查询只命中相关子表——这就是分区裁剪(partition pruning)。PostgreSQL 10+ 支持声明式分区,已经很好用。

三种分区策略

类型 按什么分 典型场景
Range 范围(时间、ID 区间) 按月份/年份归档日志、订单
List 枚举值 按地区、租户、业务线
Hash 哈希取模 均匀打散、无天然范围键

时间 Range 分区是运维场景的绝对主力(日志、监控、订单)。

Range 分区建表(声明式)

-- 父表(不存数据,只定义分区键)
CREATE TABLE orders (
    id          bigint,
    user_id     bigint,
    amount      numeric(12,2),
    created_at  timestamp NOT NULL,
    PRIMARY KEY (id, created_at)        -- 分区键必须进唯一约束/主键
) PARTITION BY RANGE (created_at);

-- 子表(每个子表一个区间)
CREATE TABLE orders_2026_06 PARTITION OF orders
    FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');

CREATE TABLE orders_2026_07 PARTITION OF orders
    FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');

CREATE TABLE orders_2026_08 PARTITION OF orders
    FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');

要点:

  • 分区键 created_at 必须出现在主键/唯一约束里(否则无法保证全局唯一)。
  • 子表区间左闭右开 [from, to),不要重叠、不要留缝。
  • 子表可单独建索引:CREATE INDEX ON orders_2026_07 (user_id);

插入数据时无需指定子表,PG 按分区键自动路由:

INSERT INTO orders(id, user_id, amount, created_at)
VALUES (1, 100, 9.9, '2026-07-15 10:00:00');  -- 自动进 orders_2026_07

List 与 Hash 分区

-- List:按租户
CREATE TABLE metrics (
    tenant_id   int,
    ts          timestamp,
    val         numeric
) PARTITION BY LIST (tenant_id);

CREATE TABLE metrics_t1 PARTITION OF metrics FOR VALUES IN (1,2,3);
CREATE TABLE metrics_t2 PARTITION OF metrics FOR VALUES IN (4,5,6);

-- Hash:均匀打散(PG 11+)
CREATE TABLE events (
    id   bigint,
    payload jsonb
) PARTITION BY HASH (id);

CREATE TABLE events_p0 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE events_p1 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE events_p2 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE events_p3 PARTITION OF events FOR VALUES WITH (MODULUS 4, REMAINDER 3);

Hash 分区不支持分区裁剪按范围查询(只知道取模),适合纯均匀散列;要做时间范围查询还是用 Range。

分区裁剪:性能的来源

EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-07-01' AND created_at < '2026-08-01';
-- Append  ->  orders_2026_07
-- 只扫一个子表,其余子表被"裁剪"掉,不参与扫描

裁剪靠约束排除(constraint exclusion) 和分区元数据。注意:

  • 查询条件必须能静态推断出分区范围,才能裁剪。
  • 分区键做范围过滤才有效;用 created_at::date 之类函数包裹、或用非分区键,裁剪失效。
  • 参数化查询(PREPARE)也能裁剪,PG 会用实际参数值判断。

在线归档:ATTACH / DETACH

分区表最大的运维优势:归档=挪走一个子表,不影响其他数据,几乎无锁

-- 把历史子表 detach 出来(变成独立表,脱离子表关系)
ALTER TABLE orders DETACH PARTITION orders_2025_12;

-- 独立后的表可以慢速导出、压缩、转存冷存储
COPY orders_2025_12 TO '/archive/orders_2025_12.csv' WITH (FORMAT csv);

-- 新建未来分区(建议用 cron/事件提前建,避免插入时"无匹配分区"报错)
CREATE TABLE orders_2026_09 PARTITION OF orders
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

-- 把外部已准备好的表直接 attach 进来(比边插边建快得多)
CREATE TABLE orders_staging (LIKE orders INCLUDING ALL);
-- ... 灌数据 ...
ALTER TABLE orders ATTACH PARTITION orders_staging
    FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');

ATTACH 时会校验数据确实落在声明的区间内,否则报错——保证数据不串区。DETACH 默认会拿 ACCESS EXCLUSIVE 锁,但窗口极短(只改元数据)。

默认分区与防止插入落空

-- 没匹配任何子表的插入会报错;可设 DEFAULT 分区兜底
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

⚠️ DEFAULT 分区是"垃圾抽屉":一旦用了它,未来按该区间建正式子表前必须先 DETACH DEFAULT 并清理数据,否则 ATTACH 冲突。能精确建子表就别依赖 DEFAULT。

监控与维护

-- 看每个子表大小
SELECT inhrelid::regclass AS partition,
       pg_size_pretty(pg_table_size(inhrelid))
FROM pg_inherits
WHERE inhparent = 'orders'::regclass;

-- 看分区裁剪是否生效
EXPLAIN (COSTS OFF) SELECT * FROM orders WHERE created_at < '2026-06-01';
-- 应该只出现被命中的子表

-- 每个子表单独 vacuum(比整表快)
VACUUM ANALYZE orders_2026_07;

10 条实战底线

  1. 分区键必须进主键/唯一约束,否则建表失败或唯一性无法保证。
  2. 高频过滤字段做分区键(几乎总是时间),裁剪才生效。
  3. 子表区间左闭右开、不重叠不留缝,重叠建表直接报错。
  4. 查询条件直接用到分区键,别用函数包裹,否则裁剪失效、全表扫。
  5. 提前建未来分区(cron 月初/年初自动建),避免插入无匹配分区报错。
  6. 归档用 DETACH + 导出,比 DELETE 大批量数据安全且快,不锁全表。
  7. 批量灌历史数据用 ATTACH 现成表,比逐行 INSERT 快几个数量级。
  8. 慎⽤ DEFAULT 分区,它是垃圾抽屉,会阻碍后续 ATTACH
  9. 每个子表可独立建索引、独立 VACUUM,善用这个粒度。
  10. 分区不是银弹:单子表也要合理索引;子表过多(上千)会让规划器开销上升,按业务粒度分(月级通常够)。

分区表的精髓是"化整为零":让 99% 的查询只碰那 1% 的数据。配合 Range 时间分区 + 自动建表 + DETACH 归档,亿级流水表的运维可以从"提心吊胆"变成"按月切西瓜"一样轻松。

分享:

相关文章

评论区