PostgreSQL表分区

Eave 2025.11.13

PostgreSQL支持声明式表分区功能。分区的意思是把逻辑上的一个大表分割成物理上的几块。分区可以提供若干好处:

查询性能飞跃 — 分区修剪(Partition Pruning)只扫描相关分区,索引更小,高命中部分可常驻内存。

连续扫描优势 — 当查询或更新分区的大部分记录时,顺序扫描分区远快于离散索引扫描全表。

极速数据销毁 — 删除历史数据直接 DROP TABLEDETACH PARTITION,毫秒级完成,无 VACUUM 灾难。

冷热数据分层 — 将低频访问分区迁移至廉价慢速存储(不同表空间),优化成本。


这种好处通常只有在表可能会变得非常大的情况下才有价值。到底多大的表会从分区中收益取决于具体的应用,不过有个基本的拇指规则就是表的大小超过了数据库服务器的物理内存大小。

目前,PostgreSQL 10+支持声明式分区,通过PARTITION BY子句创建分区表。你不需要手动处理表继承和约束,数据库会自动管理数据路由。


PostgreSQL可以实现下面形式的分区

范围分区:基于列值的范围划分,无重叠。常用:时间序列(年月日)、数字范围。PARTITION BY RANGE (created_at)

列表分区:显式列出分区键的值。适用:地区、状态码、类别等离散值。PARTITION BY LIST (region)

哈希分区:哈希函数均匀分布数据,消除热点,适合均匀写入/查询负载。PARTITION BY HASH (user_id)


设置分区表的步骤

1、创建主表

使用 PARTITION BY 子句指定分区键和分区方法

主表定义了整个逻辑表的结构,但本身不存储数据。你需要指定分区键,例如按日期范围分区:

CREATE TABLE measurement (
    city_id     int not null,
    logdate     date not null,
    peaktemp    int,
    unitsales   int
) PARTITION BY RANGE (logdate);

2、创建分区

为每个分区定义数据范围

分区是独立的物理表,通过 PARTITION OF 子句与主表关联:

CREATE TABLE measurement_202602 PARTITION OF measurement
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

注意:范围分区的边界遵循下包含,上不包含的原则。例如 '2026-02-01' ≤ value < '2026-03-01''2026-03-01'属于下一个分区。

3、数据自动路由

当向主表插入数据时,PostgreSQL 会根据分区键的值自动将数据路由到正确的分区,无需手动编写触发器或规则:

INSERT INTO measurement (city_id, logdate, peaktemp, unitsales) 
VALUES (1, '2026-02-15', 33, 120);

4、数据查询

查询时直接操作主表,PostgreSQL 会自动只扫描相关分区:

-- 这个查询只会扫描 measurement_202602 分区
SELECT * FROM measurement 
WHERE logdate BETWEEN '2026-02-01' AND '2026-02-28';

注意:声明式分区要求分区键必须包含在主键或唯一约束中。

5、删除历史数据

直接丢弃分区(无需数据恢复):DROP TABLE IF EXISTS measurement_202602; 毫秒级完成,空间立即释放。

归档后分离:ALTER TABLE measurement DETACH PARTITION measurement_202602; 分区变成独立表,可压缩或迁移。

6、PARTITION BY和INHERITS的区别

INHERITS是一种表结构继承机制,子表会继承父表的所有列,同时可以添加自己的列。这与声明式分区(PARTITION BY)有本质区别。

INHERITS的适用场景

多租户数据隔离(典型场景)

需求:多个客户(租户)共享相同的数据结构,但要求物理隔离,且查询时需要能统一访问。

-- 父表:统一的查询接口
CREATE TABLE tenant_data (
    id serial,
    data text,
    created_at timestamptz
);

-- 每个租户独立子表
CREATE TABLE tenant_001_data (
    tenant_specific_col text,  -- 租户特有字段
    CHECK (tenant_id = 001)
) INHERITS (tenant_data);

CREATE TABLE tenant_002_data (
    another_col int,           -- 不同租户可以有不同扩展字段
    CHECK (tenant_id = 002)
) INHERITS (tenant_data);

-- 查询所有租户数据(统一接口)
SELECT * FROM tenant_data;

-- 查询特定租户(只扫描对应子表)
SELECT * FROM ONLY tenant_001_data;  -- ONLY 排除父表数据

注意:当向子表tenant_002_data插入数据后,查询父表tenant_data时,这些数据会被包含在结果中;父表查询时,只能看到子表中继承自父表的列。