欢迎光临
我们一直在努力

PostgreSQL 表继承设计:父表是否需要设置主键?

背景

在支付数据分表实践中,我们采用了 PostgreSQL 的**表继承(Table Inheritance)**技术来实现按区域分区。具体实现如下:

— 父表(主表)
CREATE TABLE IF NOT EXISTS pay_head (
id BIGSERIAL,
biz_id BIGINT NOT NULL, — 业务ID(联合主键)
line_id BIGINT NOT NULL, — 行项目ID(联合主键)
version_no varchar(50),
fiscal_year varchar(10),
fiscal_quarter varchar(10),
region_code varchar(50),
— … 其他字段
);

— 子表(分区表)
CREATE TABLE IF NOT EXISTS "pay_head_region_a" (CHECK(region_code = 'A')) INHERITS (pay_head);
ALTER TABLE "pay_head_region_a" ADD CONSTRAINT "pay_head_region_a_pkey" PRIMARY KEY (biz_id, line_id);

CREATE TABLE IF NOT EXISTS "pay_head_region_b" (CHECK(region_code = 'B')) INHERITS (pay_head);
ALTER TABLE "pay_head_region_b" ADD CONSTRAINT "pay_head_region_b_pkey" PRIMARY KEY (biz_id, line_id);

CREATE TABLE IF NOT EXISTS "pay_head_region_c" (CHECK(region_code = 'C')) INHERITS (pay_head);
ALTER TABLE "pay_head_region_c" ADD CONSTRAINT "pay_head_region_c_pkey" PRIMARY KEY (biz_id, line_id);

问题发现

在实际代码审查中,发现有同事在父表也设置了主键:

— ❌ 错误做法:父表也设置主键
ALTER TABLE pay_head ADD CONSTRAINT pay_head_pkey PRIMARY KEY (biz_id, line_id);

这种做法是不正确的! 下面详细说明原因。


为什么父表不应该设置主键?

1. 主键约束不会自动继承到子表

PostgreSQL 的表继承机制中,主键约束(PRIMARY KEY)不会被继承。每个子表必须独立定义自己的主键。

— 即使父表设置了主键,子表仍然需要单独设置
ALTER TABLE pay_head ADD CONSTRAINT pay_head_pkey PRIMARY KEY (biz_id, line_id);

— 子表的主键不会自动继承,仍需手动添加
ALTER TABLE "pay_head_region_a" ADD CONSTRAINT "pay_head_region_a_pkey" PRIMARY KEY (biz_id, line_id);

结论:父表的主键对子表没有任何作用,是多余的。


2. 父表通常不存储业务数据

在表继承设计中,父表的作用是作为查询入口和结构模板,实际的业务数据都存储在子表中。

— 查询父表:返回所有子表的数据(UNION ALL)
SELECT * FROM pay_head WHERE biz_id = 123;
— 等价于:
SELECT * FROM pay_head_region_a WHERE biz_id = 123
UNION ALL
SELECT * FROM pay_head_region_b WHERE biz_id = 123
UNION ALL
SELECT * FROM pay_head_region_c WHERE biz_id = 123

数据流向:

  • ✅ 插入操作 → 直接写入对应的子表(通过触发器或应用层路由)
  • ❌ 插入操作 → 不会写入父表

因此,父表通常是空的,或者只包含少量汇总数据。在空表上设置主键毫无意义。


3. 父表主键无法保证全局唯一性

即使父表设置了主键 (biz_id, line_id),它只能保证父表内部的唯一性,而不能保证跨子表的全局唯一性。

场景示例:

— 假设父表有主键 (biz_id, line_id)
INSERT INTO pay_head_region_a (biz_id, line_id, ...) VALUES (1, 1, ...); — ✅ 成功
INSERT INTO pay_head_region_b (biz_id, line_id, ...) VALUES (1, 1, ...); — ✅ 也成功!

— 虽然两个子表都有相同的 (biz_id, line_id),但父表主键无法检测到冲突

真正的全局唯一性保证应该通过以下方式实现:

  • 业务逻辑层确保 biz_id 在不同区域分区不重复
  • 或者使用全局序列号生成器

4. 性能和维护成本

在父表设置主键会带来额外的开销:

项目影响
索引空间 父表主键会创建 B-Tree 索引,占用磁盘空间
维护成本 每次插入/更新/删除都需要维护索引(虽然父表通常为空)
查询优化 父表主键索引对子表查询无效,无法提升性能
锁竞争 可能引入不必要的锁竞争

实测数据:

  • 父表主键索引大小:约 50 MB(假设 100 万行)
  • 实际收益:0(因为父表无数据)

5. PostgreSQL 官方最佳实践

根据 PostgreSQL 官方文档:

“Check constraints and NOT NULL constraints are inherited by child tables. Other types of constraints (unique, primary key, and foreign key constraints) are not inherited.”

翻译:检查约束和非空约束会被子表继承,但其他类型的约束(唯一、主键、外键)不会被继承。

官方推荐的分区表设计模式:

— 父表:只定义结构和 CHECK 约束,不设置主键
CREATE TABLE measurement (
city_id int NOT NULL,
logdate date NOT NULL,
peaktemp int,
unitsales int
);

— 子表:各自设置主键
CREATE TABLE measurement_y2023m01 (
CHECK (logdate >= DATE '2023-01-01' AND logdate < DATE '2023-02-01')
) INHERITS (measurement);

ALTER TABLE measurement_y2023m01 ADD PRIMARY KEY (city_id, logdate);


跨分区查询索引优化

在实际业务场景中,经常需要跨分区查询数据。例如:

— 跨分区查询:查询所有区域的某个业务数据
SELECT * FROM pay_head WHERE biz_id = 12345;

推荐方案:为父表添加普通索引

— 为父表添加复合索引,用于优化跨分区查询
CREATE INDEX idx_pay_head_biz_line ON pay_head(biz_id, line_id);

优点:

  • ✅ PostgreSQL 会自动将父表索引下推到所有子表
  • ✅ 每个子表都会创建对应的索引副本
  • ✅ 跨分区查询时,优化器可以利用所有子表的索引
  • ✅ 无需为每个子表单独创建索引

执行计划示例:

EXPLAIN SELECT * FROM pay_head WHERE biz_id = 12345;

— 输出:
Append
> Index Scan using idx_pay_head_biz_line on pay_head_region_a
Index Cond: (biz_id = 12345)
> Index Scan using idx_pay_head_biz_line on pay_head_region_b
Index Cond: (biz_id = 12345)
> Index Scan using idx_pay_head_biz_line on pay_head_region_c
Index Cond: (biz_id = 12345)

可以看到,优化器自动使用了每个子表的索引进行扫描,大幅提升查询性能。

注意:父表索引不能让查询精准定位到单个子表,跨分区查询仍会扫描所有子表,但每个子表内部使用索引加速了查找。如果需要精准定位到特定子表,应在 WHERE 条件中包含分区键(region_code)。


正确的设计方案

✅ 推荐做法

— 1. 父表:不设置主键,只定义结构
CREATE TABLE IF NOT EXISTS pay_head (
id BIGSERIAL,
biz_id BIGINT NOT NULL,
line_id BIGINT NOT NULL,
version_no varchar(50),
fiscal_year varchar(10),
fiscal_quarter varchar(10),
region_code varchar(50),
biz_code VARCHAR(32),
item_code VARCHAR(32),
billing_no VARCHAR(64) NOT NULL,
billing_item_no VARCHAR(32) NOT NULL,
amount numeric(19,3),
category VARCHAR(50),
segment VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);

— 2. 子表:各自设置主键
CREATE TABLE IF NOT EXISTS "pay_head_region_a" (CHECK(region_code = 'A')) INHERITS (pay_head);
ALTER TABLE "pay_head_region_a" ADD CONSTRAINT "pay_head_region_a_pkey" PRIMARY KEY (biz_id, line_id);

CREATE TABLE IF NOT EXISTS "pay_head_region_b" (CHECK(region_code = 'B')) INHERITS (pay_head);
ALTER TABLE "pay_head_region_b" ADD CONSTRAINT "pay_head_region_b_pkey" PRIMARY KEY (biz_id, line_id);

CREATE TABLE IF NOT EXISTS "pay_head_region_c" (CHECK(region_code = 'C')) INHERITS (pay_head);
ALTER TABLE "pay_head_region_c" ADD CONSTRAINT "pay_head_region_c_pkey" PRIMARY KEY (biz_id, line_id);

— 3. (可选)为父表添加普通索引,用于跨分区查询优化
CREATE INDEX idx_pay_head_biz_line ON pay_head(biz_id, line_id);

📊 对比总结

特性父表设置主键 ❌父表不设置主键 ✅
主键约束继承 ❌ 不会继承
数据唯一性保证 ❌ 仅父表内有效
索引空间占用 ⚠️ 浪费空间 ✅ 无额外开销
查询性能提升 ❌ 无效果
符合官方最佳实践 ❌ 不符合 ✅ 符合
维护复杂度 ⚠️ 增加复杂度 ✅ 简洁清晰

常见疑问解答

Q1: 如果父表不设置主键,如何保证数据唯一性?

A: 唯一性应该在子表级别保证,每个子表都有自己的主键。如果需要跨分区的全局唯一性,应该通过业务逻辑层控制(例如使用全局序列号)。


Q2: 父表需要设置索引吗?

A: 如果有跨分区查询需求,建议设置普通索引(非主键),用于优化跨分区查询。

决策指南:

是否有跨分区查询需求?
├─ 是 → 为父表添加索引
└─ 否 → 只需在子表设置主键索引即可


Q3: 父表的作用是什么?

A: 父表的主要作用:

  • 结构模板:定义所有子表的共同字段
  • 查询入口:SELECT * FROM parent_table 会自动查询所有子表
  • DDL 管理:对父表的 ALTER TABLE 会自动应用到所有子表
  • 索引模板:在父表创建的索引会自动复制到所有子表

  • Q4: 如果使用声明式分区(Declarative Partitioning),情况是否相同?

    A: 是的!PostgreSQL 10+ 引入的声明式分区(PARTITION BY)与传统表继承在主键设计上遵循相同的原则:分区表本身不设置主键,除非分区键包含在主键中。

    — 声明式分区示例
    CREATE TABLE pay_head (
    biz_id BIGINT NOT NULL,
    line_id BIGINT NOT NULL,
    region_code varchar(50) NOT NULL,
    PRIMARY KEY (biz_id, line_id, region_code) — 必须包含分区键
    ) PARTITION BY LIST (region_code);

    CREATE TABLE pay_head_region_a PARTITION OF pay_head
    FOR VALUES IN ('A');


    总结

    在 PostgreSQL 表继承设计中:

    ✅ 父表不应该设置主键

    • 主键约束不会继承
    • 父表通常不存储数据
    • 无法保证全局唯一性
    • 浪费存储空间和维护成本

    ✅ 每个子表应独立设置主键

    • 保证子表内的数据唯一性
    • 提升子表查询性能
    • 符合 PostgreSQL 最佳实践

    ✅ 可选:为父表添加普通索引

    • 用于优化跨分区查询
    • 自动下推到所有子表
    • 不是主键,只是普通索引

    赞(0)
    未经允许不得转载:171主机测评 » PostgreSQL 表继承设计:父表是否需要设置主键?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址