PostgreSQL: PostgreSQL分区表与表继承

最后更新:2026-08-26

1. 你将学到


2. 故事

Alice 的电商平台 orders 表每天新增 100K 行。三年后单表超过 1 亿行,即使有索引,按月查询一次也要扫描整张表。DBA 建议按月做 RANGE 分区——查询最近 30 天数据只需扫描 1 个分区,而非 3 年的全表。分区上线后,月度报表查询从 12 秒降到 0.8 秒,磁盘 I/O 降低 95%。


3. Concept:声明式分区概述

(1) 为什么需要分区

当单表行数超过千万级,索引的 B-Tree 深度增加、VACUUM 耗时暴涨、查询规划器可能选择次优计划。分区将逻辑大表拆为多个物理小表,每个小表独立索引与维护。

指标 未分区(1 亿行) 按月分区(36 个分区)
单分区行数 100M ~2.8M
索引深度 5-6 层 3-4 层
VACUUM 时间 30+ min < 2 min/分区
按月查询 I/O 全表扫描 仅扫 1 个分区

(2) PostgreSQL 分区演进

版本 特性
PG 9.x 表继承 + 触发器手动分区
PG 10 声明式 RANGE / LIST 分区
PG 11 HASH 分区、分区裁剪增强、UPDATE 跨分区迁移
PG 12 ATTACH/DETACH 分区
PG 13 多级分区裁剪优化
PG 14+ 分区裁剪性能进一步提升

(3) 四种分区策略

策略 适用场景 分区键要求
RANGE 时间序列、数值区间 可排序类型
LIST 枚举值分类(地区、状态) 离散值
HASH 均匀分布、无明确范围 任意可哈希类型
MULTI-LEVEL RANGE + LIST/HASH 组合 多列组合

4. 操作:RANGE 分区

▶ 示例:创建按月 RANGE 分区的订单表

SQL
-- Parent table: partition key only
CREATE TABLE orders (
    id          BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id     BIGINT NOT NULL,
    order_date  DATE NOT NULL,
    total_amount NUMERIC(12,2),
    status      TEXT DEFAULT 'pending'
) PARTITION BY RANGE (order_date);

-- Monthly partitions for 2024
CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

CREATE TABLE orders_2024_03 PARTITION OF orders
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');

-- Default partition catches out-of-range rows
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

输出:

TEXT 📖 仅展示
CREATE TABLE

▶ 示例:自动生成分区的函数

SQL
-- Generate monthly partitions for a given year
CREATE OR REPLACE FUNCTION create_monthly_partitions(
    p_parent  REGCLASS,
    p_year    INT
) RETURNS VOID AS $$
DECLARE
    m      INT;
    p_name TEXT;
    s_date TEXT;
    e_date TEXT;
BEGIN
    FOR m IN 1..12 LOOP
        p_name := format('%s_%s_%02s', p_parent::text, p_year, m);
        s_date := format('%s-%02s-01', p_year, m);
        e_date := format('%s-%02s-01', p_year, m + 1);
        EXECUTE format(
            'CREATE TABLE IF NOT EXISTS %I PARTITION OF %s
             FOR VALUES FROM (%L) TO (%L)',
            p_name, p_parent::text, s_date, e_date
        );
    END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT create_monthly_partitions('orders', 2025);

输出:

TEXT 📖 仅展示
CREATE TABLE

▶ 示例:RANGE 分区插入与查询

SQL
INSERT INTO orders (user_id, order_date, total_amount, status)
VALUES
    (1001, '2024-01-15', 299.99, 'completed'),
    (1002, '2024-02-20', 159.50, 'shipped'),
    (1003, '2024-03-10', 89.00,  'pending');

-- Query with partition pruning
SELECT * FROM orders
WHERE order_date BETWEEN '2024-02-01' AND '2024-02-29';
TEXT 📖 仅展示
  id  | user_id | order_date | total_amount | status
------+---------+------------+--------------+--------
 1002 |    1002 | 2024-02-20 |       159.50 | shipped
(1 row)

5. 操作:LIST 与 HASH 分区

▶ 示例:LIST 分区按地区拆分用户表

SQL
CREATE TABLE users_by_region (
    id       BIGINT GENERATED ALWAYS AS IDENTITY,
    name     TEXT NOT NULL,
    region   TEXT NOT NULL,
    email    TEXT
) PARTITION BY LIST (region);

CREATE TABLE users_north_america PARTITION OF users_by_region
    FOR VALUES IN ('US', 'CA', 'MX');

CREATE TABLE users_europe PARTITION OF users_by_region
    FOR VALUES IN ('UK', 'DE', 'FR', 'ES');

CREATE TABLE users_asia PARTITION OF users_by_region
    FOR VALUES IN ('CN', 'JP', 'KR', 'SG');

CREATE TABLE users_other PARTITION OF users_by_region DEFAULT;

输出:

TEXT 📖 仅展示
CREATE TABLE

▶ 示例:HASH 分区均匀分布日志表

SQL
CREATE TABLE access_logs (
    id         BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id    BIGINT,
    action     TEXT,
    log_time   TIMESTAMPTZ DEFAULT now()
) PARTITION BY HASH (user_id);

-- Create 8 hash partitions
CREATE TABLE access_logs_p0 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE access_logs_p1 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE access_logs_p2 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE access_logs_p3 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE access_logs_p4 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE access_logs_p5 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE access_logs_p6 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE access_logs_p7 PARTITION OF access_logs
    FOR VALUES WITH (MODULUS 8, REMAINDER 7);

输出:

TEXT 📖 仅展示
CREATE TABLE

▶ 示例:多级分区(RANGE + LIST)

SQL
CREATE TABLE order_details (
    id          BIGINT,
    order_id    BIGINT,
    order_date  DATE NOT NULL,
    region      TEXT NOT NULL,
    product_id  INT,
    quantity    INT
) PARTITION BY RANGE (order_date);

-- First level: by month
CREATE TABLE order_details_2024q1 PARTITION OF order_details
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01')
    PARTITION BY LIST (region);

-- Second level: by region within each month range
CREATE TABLE order_details_2024q1_na PARTITION OF order_details_2024q1
    FOR VALUES IN ('US', 'CA', 'MX');

CREATE TABLE order_details_2024q1_eu PARTITION OF order_details_2024q1
    FOR VALUES IN ('UK', 'DE', 'FR');

输出:

TEXT 📖 仅展示
CREATE TABLE

6. Concept:分区裁剪

(1) 裁剪原理

分区裁剪(Partition Pruning)让查询规划器在规划阶段排除不相关分区,避免运行时扫描。

100%
flowchart TD
    A["SELECT * FROM orders<br/>WHERE order_date = '2024-02-15'"] --> B["Query Planner"]
    B --> C{"Partition Pruning"}
    C -->|"order_date in [2024-02-01, 2024-03-01)"| D["orders_2024_02 ✓"]
    C -->|"out of range"| E["orders_2024_01 ✗"]
    C -->|"out of range"| F["orders_2024_03 ✗"]
    C -->|"out of range"| G["orders_default ✗"]
    D --> H["Scan 1 partition only"]

(2) 验证裁剪效果

SQL
-- Enable partition pruning (default ON)
SET enable_partition_pruning = on;

-- Check which partitions are scanned
EXPLAIN (COSTS OFF) SELECT * FROM orders
WHERE order_date = '2024-02-15';
TEXT 📖 仅展示
Append
  -> Seq Scan on orders_2024_02
       Filter: (order_date = '2024-02-15'::date)

(3) 裁剪失效的常见原因

场景 是否裁剪 原因
WHERE order_date = '2024-02-15' 常量可裁剪
WHERE order_date = $1 (prepared stmt) ✅ PG 11+ 通用参数裁剪
WHERE order_date = now() 稳定函数可裁剪
WHERE order_date = random_func() volatile 函数不可裁剪
WHERE to_char(order_date, 'YYYY-MM') = '2024-02' 函数包裹分区键

7. 操作:分区维护

▶ 示例:DETACH 旧分区归档

SQL
-- Detach January 2023 partition (no data loss)
ALTER TABLE orders DETACH PARTITION orders_2023_01;

-- Now it's a standalone table, can be moved to cheaper storage
ALTER TABLE orders_2023_01 SET TABLESPACE archive_tbs;

-- Or export and drop
COPY orders_2023_01 TO '/archive/orders_2023_01.csv';
DROP TABLE orders_2023_01;

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

▶ 示例:ATTACH 新分区

SQL
-- Create new table first
CREATE TABLE orders_2025_01 (LIKE orders INCLUDING DEFAULTS);

-- Add check constraint for validation (speeds up ATTACH)
ALTER TABLE orders_2025_01
    ADD CONSTRAINT orders_2025_01_check
    CHECK (order_date >= '2025-01-01' AND order_date < '2025-02-01');

-- Attach to parent (takes exclusive lock briefly)
ALTER TABLE orders ATTACH PARTITION orders_2025_01
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

输出:

TEXT 📖 仅展示
CREATE TABLE

▶ 示例:并发 DETACH(PG 14+)

SQL
-- DETACH without blocking concurrent reads
ALTER TABLE orders DETACH PARTITION orders_2023_01 CONCURRENTLY;

输出:

TEXT 📖 仅展示
-- SQL 语句执行成功

▶ 示例:分区索引策略

SQL
-- Index on parent propagates to all partitions
CREATE INDEX idx_orders_user_id ON orders (user_id);

-- Each partition gets its own index
\d orders_2024_01
TEXT 📖 仅展示
Indexes:
    "orders_2024_01_user_id_idx" btree (user_id)
索引策略 说明
父表创建索引 自动传播到所有现有及未来分区
分区单独创建索引 仅影响该分区,ATTACH 时需手动同步
唯一索引 必须包含分区键(跨分区唯一性由分区键保证)

▶ 示例:唯一索引必须包含分区键

SQL
-- This FAILS: unique without partition key
CREATE UNIQUE INDEX idx_orders_id ON orders (id);
-- ERROR: unique constraint must contain partition key

-- This WORKS: unique with partition key
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);

输出:

TEXT 📖 仅展示
CREATE TABLE

8. Concept:表继承(INHERITS)

(1) 继承语法与特点

表继承是 PostgreSQL 的独有特性,比声明式分区更灵活——子表可以有额外列,不需要覆盖父表所有值域。

SQL
-- Parent table
CREATE TABLE people (
    id    SERIAL PRIMARY KEY,
    name  TEXT NOT NULL,
    email TEXT
);

-- Child table inherits all columns + adds extras
CREATE TABLE employees (
    salary    NUMERIC(10,2),
    dept      TEXT,
    hire_date DATE
) INHERITS (people);

-- Add primary key to child
ALTER TABLE employees ADD PRIMARY KEY (id);

(2) 继承 vs 声明式分区对比

特性 表继承(INHERITS) 声明式分区
子表额外列 ✅ 允许 ❌ 必须同结构
父表存数据 ✅ 可以 ❌ 父表为空壳
自动路由 INSERT ❌ 需触发器 ✅ 自动
分区裁剪 ❌ 需 CHECK 约束 ✅ 自动
唯一约束跨表 ❌ 仅单表 ✅ 含分区键可跨表
外键引用父表 ❌ 不支持 ✅ 支持
灵活性
推荐场景 异构子类型 大表拆分优化

(3) 继承查询:ONLY 关键字

SQL
-- Query parent + all children
SELECT * FROM people;

-- Query parent ONLY (no children)
SELECT * FROM ONLY people;

-- Check which table each row comes from
SELECT tableoid::regclass, * FROM people;

9. 操作:跨分区查询优化

▶ 示例:分区统计信息收集

SQL
-- Analyze specific partition
ANALYZE orders_2024_02;

-- Analyze all partitions via parent
ANALYZE orders;

-- Check partition stats
SELECT relname, n_live_tup, last_analyze
FROM pg_stat_user_tables
WHERE relname LIKE 'orders_%'
ORDER BY relname;

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ 示例:跨分区 JOIN 优化

SQL
-- Partition-wise join (PG 12+)
SET enable_partitionwise_join = on;

SELECT o.id, o.total_amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-03-31';

输出:

TEXT 📖 仅展示
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
参数 默认值 说明
enable_partition_pruning on 分区裁剪
enable_partitionwise_join off 分区级 JOIN
enable_partitionwise_aggregate off 分区级聚合
constraint_exclusion partition 约束排除(继承用)

▶ 示例:分区与继承混合场景

SQL
-- Base audit table
CREATE TABLE audit_log (
    id         BIGINT GENERATED ALWAYS AS IDENTITY,
    table_name TEXT NOT NULL,
    action     TEXT NOT NULL,
    changed_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (changed_at);

-- Monthly partitions, each inherited by type-specific tables
CREATE TABLE audit_log_2024_01 PARTITION OF audit_log
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- Additional columns for specific audit type via inheritance
CREATE TABLE audit_log_user_changes (
    old_email TEXT,
    new_email TEXT
) INHERITS (audit_log_2024_01);

输出:

TEXT 📖 仅展示
CREATE TABLE

10. 综合示例

SQL
-- Complete monthly partitioning setup for e-commerce orders
CREATE TABLE orders (
    id           BIGINT GENERATED ALWAYS AS IDENTITY,
    user_id      BIGINT NOT NULL,
    order_date   DATE NOT NULL,
    total_amount NUMERIC(12,2) DEFAULT 0,
    status       TEXT DEFAULT 'pending',
    created_at   TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (order_date);

-- Create partitions for 2024 Q1
CREATE TABLE orders_2024_01 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_2024_03 PARTITION OF orders
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

-- Unique index must include partition key
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);
CREATE INDEX idx_orders_user_date ON orders (user_id, order_date);

-- Insert test data
INSERT INTO orders (user_id, order_date, total_amount, status) VALUES
    (1001, '2024-01-05', 299.99, 'completed'),
    (1001, '2024-02-14', 159.50, 'shipped'),
    (1002, '2024-01-20', 450.00, 'completed'),
    (1002, '2024-03-01', 89.00,  'pending'),
    (1003, '2024-02-28', 1200.00,'completed');

-- Verify partition pruning
EXPLAIN (COSTS OFF) SELECT * FROM orders
    WHERE order_date BETWEEN '2024-02-01' AND '2024-02-28';

-- Detach old partition for archiving
ALTER TABLE orders DETACH PARTITION orders_2024_01;
COPY orders_2024_01 TO '/archive/orders_2024_01.csv';

-- Attach new partition for next quarter
CREATE TABLE orders_2024_04 (LIKE orders INCLUDING DEFAULTS);
ALTER TABLE orders_2024_04
    ADD CONSTRAINT chk_2024_04
    CHECK (order_date >= '2024-04-01' AND order_date < '2024-05-01');
ALTER TABLE orders ATTACH PARTITION orders_2024_04
    FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');

-- Monitor partition sizes
SELECT relname,
       pg_size_pretty(pg_total_relation_size(oid)) AS size,
       reltuples::bigint AS row_estimate
FROM pg_class
WHERE relname LIKE 'orders_2024%' ORDER BY relname;

❓ 常见问题

Q 分区表的主键必须包含分区键吗?
A 是的。PostgreSQL 要求分区表唯一约束(包括主键)必须包含分区键列,否则无法保证跨分区唯一性。
Q 已经存在的普通表可以转为分区表吗?
A 不能直接转换。需要创建新的分区父表,然后用 INSERT INTO...SELECT 或 ATTACH 方式迁移数据。
Q DEFAULT 分区有什么风险?
A DEFAULT 分区会接收所有不匹配的数据,如果新分区范围与 DEFAULT 已有数据重叠,ATTACH 新分区会失败。建议定期审查 DEFAULT 分区内容。
Q 分区数量有没有上限?
A PostgreSQL 无硬性上限,但每个分区在规划器中都有开销。建议单表分区数控制在数百以内,超过 1000 个分区时规划时间可能明显增加。
Q 表继承能替代声明式分区吗?
A 一般不推荐。继承缺少自动路由、裁剪和跨表约束,维护成本高。仅在需要异构子表(额外列)时使用继承。
Q HASH 分区能做分区裁剪吗?
A 可以。只要 WHERE 条件包含分区键的等值比较,PG 能计算出数据在哪个 hash bucket,从而裁剪其他分区。
Q UPDATE 导致行跨分区移动会锁表吗?
A PG 11+ 支持 UPDATE 跨分区移动,它是 DELETE + INSERT 内部操作,不会锁整表但会在两个分区上分别加行锁。

📖 小节


📝 作业

  1. ⭐ 创建一个按季度 RANGE 分区的 payments 表,包含 2024 年四个季度分区,并验证 WHERE payment_date BETWEEN '2024-Q2' 的裁剪效果。

  2. ⭐⭐ 为一个 SaaS 多租户系统设计 LIST 分区方案:按 tenant_id 哈希到 4 个分区,编写创建语句,并测试不同 tenant 的数据隔离效果。

  3. ⭐⭐⭐ 编写存储过程:输入年份,自动为 orders 表创建 12 个月分区、添加 CHECK 约束、创建索引,并 DETACH 上一年 1 月的分区到归档表空间。测试 INSERT、跨分区查询、裁剪验证全流程。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏