在PostgreSQL中,主键和外键不仅是数据完整性的基石,也直接影响表的存储结构、写入性能以及多表关联时的锁行为。合理的设计能够在保证约束的同时降低维护成本,而不当的选型则会在业务增长后带来难以察觉的隐患。

一、主键的底层机制与选型
PostgreSQL的主键本质上是一个唯一索引加上NOT NULL约束。当我们声明PRIMARY KEY时,系统会自动创建一个名为表名_pkey的唯一B树索引。在MVCC机制下,每一次更新行版本都会涉及索引项的变更,因此主键的宽度和类型会直接决定索引页的膨胀速度。
常见的选型有代理主键(如BIGSERIAL)与业务主键(如自然唯一的订单号)。代理主键写入简单、长度固定,但对业务语义无表达;业务主键可读性强,却可能因业务规则变化导致重建主键。以下示例展示两种定义方式:
-- 代理主键
CREATE TABLE user_account (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL
);
-- 业务主键
CREATE TABLE order_main (
order_no CHAR(20) PRIMARY KEY,
buyer_id BIGINT NOT NULL
);
从性能角度看,BIGINT类型的代理主键在B树中占用8字节,插入呈单调递增,能减少页分裂;而较长的业务主键会让每个二级索引都携带这份键值,磁盘占用成倍上升。若业务后续需要做分库分表,代理主键配合雪花算法比单纯数据库序列更容易规避跨节点冲突。
1.1 主键与NULL及唯一性
主键列隐式禁止NULL,这与唯一索引允许单个NULL不同。如果在迁移旧数据时发现空值,必须先清洗再建主键,否则ALTER TABLE ADD PRIMARY KEY会直接报错。此外,复合主键的顺序应遵循区分度高的列在前,以提升索引裁剪效率。
例如用户与角色的关联表,将user_id放在复合主键首位,比反过来更能利用索引范围扫描。设计阶段用EXPLAIN观察执行计划,能提前发现主键顺序带来的性能偏差。
二、外键约束的运行原理
外键在PostgreSQL中通过触发器实现。当子表插入或更新时,会去检查父表是否存在对应键值;当父表删除或更新被引用键时,会根据ON DELETE规则锁定或级联子表。如果没有在父表被引用列上建立索引,子表的外键检查仍可用主键,但父表做更新操作时会对子表加更重的锁。
很多人在子表的外键列上忘记建索引,导致父表删除一行时,数据库必须全表扫描子表确认无引用,这在千万级数据下会直接阻塞写入。正确做法如下:
CREATE TABLE order_item (
id BIGSERIAL PRIMARY KEY,
order_no CHAR(20) REFERENCES order_main(order_no),
sku TEXT NOT NULL
);
CREATE INDEX idx_item_order ON order_item(order_no);
上面的idx_item_order能加速父表order_main删除订单时的反向校验。若业务只插入不删除历史订单,也可以评估用ON DELETE NO ACTION配合定时离线校验来替代实时外键,从而换取写入吞吐。
2.1 外键的级联策略
ON DELETE CASCADE适合强归属关系,如订单与订单明细;RESTRICT则防止误删主数据。需要注意的是,级联删除会触发子表行级触发器,若子表还有自身外键,可能形成递归锁,设计时需梳理引用闭环。
在报表类系统中,常把明细表外键设为DEFERRABLE INITIALLY DEFERRED,让事务提交前才校验,便于先插子表再插父表,避免人为控制顺序。但这种设置会延长约束校验到提交点,增加回滚概率。
三、大规模场景下的实践建议
当单表超过亿级,或采用分片架构时,跨分片外键几乎无法用数据库原生约束保证。此时可保留逻辑外键命名规范,用应用层校验或异步对账任务替代。如下代码展示用触发器模拟软外键检查:
CREATE OR REPLACE FUNCTION check_parent() RETURNS TRIGGER AS $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM order_main WHERE order_no = NEW.order_no) THEN
RAISE EXCEPTION 'order_no not found';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_check_before_insert
BEFORE INSERT ON order_item
FOR EACH ROW EXECUTE FUNCTION check_parent();
这种方式把校验收缩在数据库内但脱离了分布式事务,适合父表极少变更的场景。若父表也频繁写,应改为在消息队列中做最终一致性校对,防止触发器成为写入瓶颈。
3.1 主键外键的运维注意
使用pg_dump迁移时,外键会在数据装载后统一创建,因此大表导入阶段不会受约束影响。但恢复索引时若内存不足,可先建主键再建普通索引,利用并行选项提升速度。生产环境修改主键类型,如从INT改为BIGINT,需用ALTE TABLE重排表,建议在低峰期配合逻辑复制完成。
定期用pg_stat_user_tables与pg_indexes_size监控主键和外键索引的膨胀率,对长期更新的宽主键表安排REINDEX。只有把约束设计与存储引擎特性结合起来,才能让PostgreSQL在业务演进中保持稳健。
PostgreSQLprimary_keyforeign_key修改时间:2026-08-11 20:00:35