在 PostgreSQL 中,主键是表级约束,用来唯一标识表中的每一行数据。当单个列无法唯一确定一条记录时,可以把多个列组合起来,构成复合主键。例如订单明细表中,订单号与商品编号的组合才能唯一确定一条明细记录。复合主键既是一种数据完整性约束,也会在底层自动创建唯一索引,对查询性能和存储结构产生直接影响。

复合主键的定义与创建方式
在 PostgreSQL 中定义复合主键的语法非常直接。建表时可以把多个列名放入 PRIMARY KEY 后面的括号中,列之间用逗号分隔。例如创建一个订单明细表,同时以订单编号和商品编号作为主键:
CREATE TABLE order_items (
order_id integer NOT NULL,
product_id integer NOT NULL,
quantity integer NOT NULL,
PRIMARY KEY (order_id, product_id)
);
也可以在表创建之后通过 ALTER TABLE 添加复合主键约束。PostgreSQL 会先检查现有数据是否满足唯一性要求,如果存在重复的组合值,语句会执行失败并给出错误提示。下面是一个典型用法:
ALTER TABLE order_items ADD PRIMARY KEY (order_id, product_id);
从底层实现来看,PostgreSQL 会为这个主键约束自动创建一个唯一的 B-tree 索引,索引的键顺序与主键定义时的列顺序一致。这意味着复合主键的列顺序不仅仅影响数据完整性,还会直接影响索引扫描的效率。例如对于 PRIMARY KEY (order_id, product_id),如果查询条件只包含 product_id,PostgreSQL 通常无法高效利用这个索引,因为 B-tree 索引需要从左到右匹配列的顺序。理解这一点对后续分析复合主键的优缺点非常重要。
复合主键的主要优势
复合主键最突出的优势是能够从数据库层面保证多列组合的唯一性。在很多业务场景中,单列无法唯一标识一条记录,例如电商系统的购物车表,用户编号与商品编号的组合才能确定某位用户购物车中的某个商品。如果不使用复合主键,就需要额外创建一个唯一约束来防止重复数据,而复合主键本身就把唯一性约束和内建的索引一并完成,减少了额外约束的维护负担。
对于多对多关联表来说,复合主键几乎是最自然的设计。关联表通常只有两个外键列,分别指向两张主表。把这两列直接定义为复合主键,既保证了关联关系的唯一性,又避免了引入一个额外的自增主键列。这样做不仅能节省存储空间,还能让表结构更贴近业务语义。例如用户与角色的关联表可以这样定义:
CREATE TABLE user_roles (
user_id integer NOT NULL REFERENCES users(id),
role_id integer NOT NULL REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
复合主键在查询性能方面也有优势。如果业务查询经常同时使用复合主键中的多个列作为过滤条件,那么 PostgreSQL 可以直接利用主键的唯一索引完成精确查找,无需回表。比如查询某个订单的某个商品明细时,条件 WHERE order_id = 100 AND product_id = 200 正好命中复合主键索引,查询计划会走 Index Scan 或 Index Only Scan,效率很高。此外,复合主键还能减少表的总列数,避免为每张关联表都增加一个代理主键列,让数据库模式更加简洁。
复合主键的局限与潜在问题
复合主键带来的第一个问题是外键引用变得繁琐。假设订单明细表使用 (order_id, product_id) 作为主键,那么任何引用该表的表都必须同时包含这两个列,并在外键约束中列出完整的列组合。例如订单明细备注表必须这样设计:
CREATE TABLE order_item_notes (
note_id serial PRIMARY KEY,
order_id integer NOT NULL,
product_id integer NOT NULL,
note text,
FOREIGN KEY (order_id, product_id) REFERENCES order_items (order_id, product_id)
);
这会导致引用表的结构变宽,增加了冗余列和关联复杂度。如果后续业务扩展需要更多关联表,每一张表都要带着这两个列,维护成本会逐渐上升。而且一旦复合主键的列需要调整,例如增加或减少一个主键列,所有引用该主键的外键表都必须同步修改,影响面很大。
第二个明显的问题是 ORM 框架对复合主键的支持并不一致。很多主流 ORM 工具,如 Hibernate、Entity Framework、SQLAlchemy 等,虽然都支持复合主键,但配置方式比单列主键复杂得多。开发者需要手动声明多个标识属性,或使用额外的嵌入类来映射复合主键。这会增加代码的复杂度,也让一些自动生成的脚手架代码难以处理。对于快速迭代的业务系统,复合主键可能带来较高的开发成本。
还有存储和维护方面的代价。PostgreSQL 的主键索引本身会占用磁盘空间,复合主键的索引通常比单列主键索引更宽。更重要的是,PostgreSQL 中所有二级索引都会存储主键值作为指向行的指针。如果主键是宽大的复合主键,那么表上的每一个二级索引都会膨胀,导致索引体积增大、写入速度下降。对于写入频繁的表,这种额外开销可能非常可观。
复合主键与代理键的选择
代理键是人工引入的、与业务无关的唯一列,常见的有自增整数列和 UUID 列。使用代理键的优点是外键引用只需一个列,ORM 映射简单,主键值稳定且长度固定。但代理键也有代价:它不能自动防止业务重复数据,例如用户与角色的关联表使用自增代理键时,如果应用层不做唯一性控制,同一个用户可能会被重复赋予同一个角色。此时通常需要额外建立一个唯一约束来保证业务唯一性,反而增加了索引数量。
是否选择复合主键,可以重点考虑几个因素。如果表是多对多关联表,且只有两个外键列,复合主键是自然且高效的方案。如果业务主键本身非常稳定、不会发生变化,例如国家代码与省份代码的组合,也可以直接使用复合主键。但如果业务主键可能会调整,或者表会被大量其他表引用,或者开发团队大量依赖 ORM 自动生成持久层代码,那么使用单列代理键并配合唯一约束通常是更稳妥的选择。
还有一种折中方案:使用代理键作为主键,同时在建表时为业务列组合创建唯一约束。这样既保持了外键引用的简洁性,又能防止业务重复。例如订单明细表可以用自增的 id 作为主键,同时对 (order_id, product_id) 建立唯一索引。这种方案会多占用一个索引的存储空间,但在开发效率和业务完整性之间取得了较好的平衡。
使用复合主键时的注意事项
如果决定使用复合主键,列顺序是第一个需要仔细考虑的问题。应该把选择性高、经常出现在查询条件左侧的列放在前面。例如在订单明细表中,如果大多数查询都是先按订单编号过滤,再按商品编号定位,那么 PRIMARY KEY (order_id, product_id) 是合理的。反过来,如果经常按 product_id 单独查询,那么这样的复合主键索引就无法有效利用,需要为 product_id 额外创建索引。因此设计复合主键时不能只考虑唯一性,还要结合实际的查询模式。
其次要注意复合主键中的列不能包含 NULL 值。PostgreSQL 在建表时如果某列被包含在主键中,会自动为该列添加 NOT NULL 约束。但这并不意味着业务上可以忽略空值处理,因为如果应用层尝试插入 NULL 值,数据库会直接抛出错误。对于允许空值的业务字段,不应该将其纳入复合主键。
此外,复合主键与分区表结合使用时有一些限制。PostgreSQL 的分区表要求分区键必须是主键或唯一约束的一部分。如果一张分区表使用范围或列表分区,而复合主键中包含分区键,可以正常工作;但如果复合主键的列与分区键完全无关,则创建分区表时可能会遇到约束不满足的错误。因此在设计分区表的主键时,需要提前规划好分区键与主键列之间的关系。
最后,复合主键的索引同样需要纳入常规维护。宽大的 B-tree 索引容易产生页分裂和碎片,尤其是当主键列频繁更新时,索引维护成本会进一步上升。对于写入密集型的表,建议定期使用 REINDEX 或 VACUUM 来维护索引健康度。如果业务上线后发现复合主键带来的写入性能瓶颈,可以考虑迁移到代理键方案,并在迁移前充分评估外键和查询路径的影响。
PostgreSQL复合主键复合主键优缺点数据库主键设计修改时间:2026-08-26 08:51:24