PRIMARY KEY 主键
主键(Primary Key)是关系模型里最重要的约束:它同时保证了行标识唯一与值非空,也是外键引用的默认目标。本节把主键的语法形式、背后自动创建的唯一 B-tree 索引、大表加主键的在线做法,以及分区表与逻辑复制对主键的硬性要求一次讲清。
前置知识
阅读本节前,建议先了解:SECURITY DEFINER / INVOKER
基础概念
从约束的角度看,主键等价于两个约束的组合:
PRIMARY KEY (col) ≡ UNIQUE (col) + NOT NULL但它比「自己写 UNIQUE + NOT NULL」多做了一件事:PostgreSQL 会自动在背后创建一个唯一 B-tree 索引。这个索引不是可选项,而是主键实现的一部分——它既负责唯一性检查,也负责加速 WHERE id = ? 这类查找。
几个必须记住的事实:
| 事实 | 说明 |
|---|---|
| 一个表最多一个主键 | 语法层面直接禁止,第二个会报错 |
主键列自动 NOT NULL | 无需(也不允许)再写 NOT NULL |
| 主键自动建唯一索引 | 索引名默认 <表名>_pkey |
| 主键可被外键引用 | 这是它作为默认引用目标的原因 |
| 主键列可以为复合 | PRIMARY KEY (a, b),只能写在表约束位置 |
| 主键不继承 | 继承表(INHERITS)的父子主键互相独立 |
主键与其它约束在系统表里由 contype 字段区分:
contype | 含义 |
|---|---|
p | 主键(Primary Key) |
u | 唯一(Unique) |
n | 非空(Not Null,PostgreSQL 18 起才有独立条目) |
c | 检查(Check) |
f | 外键(Foreign Key) |
x | 排他(Exclude) |
主键与 UNIQUE 的唯一实质差别是「是否强制非空」和「能否有多个」,其余行为(自动建索引、可被引用、可延迟)二者一致。因此本节的很多结论同样适用于 UNIQUE 唯一约束。
语法与用法
-- 列级写法(单列主键)
column_name data_type PRIMARY KEY [ index_parameters ]
-- 表级写法(单列或复合)
[ CONSTRAINT constraint_name ] PRIMARY KEY ( column_name [, ... ] ) index_parameters
-- index_parameters
[ INCLUDE ( column_name [, ... ] ) ] -- 覆盖列(PostgreSQL 11+)
[ WITH ( storage_parameter = value [, ... ] ) ]
[ USING INDEX TABLESPACE tablespace_name ]
-- 已有表上增删
ALTER TABLE table_name ADD [ CONSTRAINT constraint_name ] PRIMARY KEY ( column_name [, ... ] )
ALTER TABLE table_name ADD CONSTRAINT constraint_name PRIMARY KEY USING INDEX index_name
ALTER TABLE table_name DROP CONSTRAINT constraint_name [ CASCADE ]最简可运行示例:
-- 列级写法:单列主键
CREATE TABLE demo (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);CREATE TABLE-- 表级写法:复合主键只能这样写
CREATE TABLE order_items (
order_id bigint NOT NULL,
line_no integer NOT NULL,
product_id bigint NOT NULL,
qty integer NOT NULL CHECK (qty > 0),
PRIMARY KEY (order_id, line_no) -- 组合唯一,且两列都自动 NOT NULL
);两种写法在单列时语义完全等价:
-- 写法 A:列级
CREATE TABLE t_a (id bigint PRIMARY KEY);
-- 写法 B:表级(结果与 A 完全一致)
CREATE TABLE t_b (id bigint, PRIMARY KEY (id));详细说明
主键背后一定有一个唯一 B-tree 索引
这是主键最容易被忽略的一点。建完主键后立刻看表结构,会发现多了一个索引:
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL,
name text NOT NULL
);\d users Table "public.users"
Column | Type | Collation | Nullable | Default
--------+--------+-----------+----------+------------------------------
id | bigint | | not null | generated always as identity
email | text | | not null |
name | text | | not null |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)users_pkey 这个索引在 pg_class 里是真实存在的对象:
-- 用 pg_index 证明:主键的索引就是这张表上的唯一索引
SELECT
c.relname AS index_name,
i.indisunique AS is_unique,
i.indisprimary AS is_primary,
pg_get_indexdef(i.indexrelid) AS index_def
FROM pg_index i
JOIN pg_class c ON c.oid = i.indexrelid
WHERE i.indrelid = 'users'::regclass; index_name | is_unique | is_primary | index_def
------------+-----------+------------+------------------------------
users_pkey | t | t | CREATE UNIQUE INDEX users_pkey ON public.users USING btree (id)
(1 row)is_unique = t 且 is_primary = t,索引定义是 CREATE UNIQUE INDEX ... USING btree。这解释了三个常见现象:
- 主键能加速查询,因为本质就是一个 B-tree 索引(原理见 B-Tree 索引原理)。
INSERT时主键要维护索引,不是「免费的」。DROP CONSTRAINT会连带删掉索引,不能只删约束留索引。
列级与表级:复合主键必须用表级
单列主键两种写法等价,但多列主键只能用表级:
-- 错误!列级写法无法表达组合
CREATE TABLE bad_pk (
order_id bigint PRIMARY KEY,
line_no integer PRIMARY KEY -- 语法上合法,但语义是「两个独立主键」
);ERROR: multiple primary keys for table "bad_pk" are not allowed注意这个错误同时揭示了另一条规则:一个表只能有一个主键。即使写成两个单列主键,也会被拒绝。
正确写法:
CREATE TABLE order_items (
order_id bigint NOT NULL,
line_no integer NOT NULL,
product_id bigint NOT NULL,
qty integer NOT NULL,
CONSTRAINT pk_order_items PRIMARY KEY (order_id, line_no)
);复合主键的语义是组合唯一,不是「各自唯一」:
INSERT INTO order_items VALUES (1, 1, 100, 2);
INSERT INTO order_items VALUES (1, 2, 100, 2); -- 允许:line_no 不同INSERT 0 1
INSERT 0 1-- 错误!组合重复
INSERT INTO order_items VALUES (1, 1, 999, 5);ERROR: duplicate key value violates unique constraint "pk_order_items"
DETAIL: Key (order_id, line_no)=(1, 1) already exists.一个表只能有一个主键
这条限制来自关系模型本身:主键是「行的唯一标识」,多一个就没有意义。想表达「另一组值也唯一」应该用 UNIQUE。
-- 错误!表里已经有一个主键
CREATE TABLE dup_pk (
id bigint NOT NULL PRIMARY KEY,
email text NOT NULL,
PRIMARY KEY (email)
);ERROR: multiple primary keys for table "dup_pk" are not allowed-- 错误!已有主键的表上再加一个
ALTER TABLE users ADD PRIMARY KEY (email);ERROR: multiple primary keys for table "users" are not allowed正确做法是加唯一约束:
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE (email);主键命名
不指定名字时,PostgreSQL 自动生成 <表名>_pkey:
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL
);
SELECT conname, contype, pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'products'::regclass
ORDER BY contype; conname | contype | definition
---------------+---------+------------
products_pkey | p | PRIMARY KEY (id)
(1 row)显式命名在跨环境比对、ALTER TABLE ... DROP CONSTRAINT 脚本、备份恢复时都更省事:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY,
order_no text NOT NULL,
CONSTRAINT pk_orders PRIMARY KEY (id) -- 自定义名字
);还有一种常见的迁移场景是改名(不重建索引):
ALTER TABLE orders RENAME CONSTRAINT pk_orders TO pk_orders_main;ALTER TABLE:加主键的代价与在线做法
ADD PRIMARY KEY 是一个「重活」:它要扫描整表校验唯一性,还要建一个唯一索引。代价与表大小成正比,且在操作期间持有 ACCESS EXCLUSIVE 锁(关于锁级别详见 修改表结构(ALTER TABLE))。
小表直接加即可:
ALTER TABLE orders ADD CONSTRAINT pk_orders PRIMARY KEY (id);大表的标准做法是先在线建唯一索引,再用 USING INDEX 挂上去,把耗时最长的部分从持锁阶段剥离出去:
-- 第 1 步:在线建唯一索引(不阻塞读写,耗时最长的一步)
CREATE UNIQUE INDEX CONCURRENTLY uq_orders_id ON orders (id);
-- 第 2 步:校验索引是否有效(CONCURRENTLY 失败时会留下 INVALID 索引)
SELECT indexrelid::regclass AS index_name, indisvalid, indisready
FROM pg_index
WHERE indrelid = 'orders'::regclass AND indexrelid = 'uq_orders_id'::regclass; index_name | indisvalid | indisready
-------------+------------+------------
uq_orders_id | t | t
(1 row)-- 第 3 步:把已有索引提升为主键约束(很快,但仍持 ACCESS EXCLUSIVE 锁)
ALTER TABLE orders
ADD CONSTRAINT pk_orders PRIMARY KEY USING INDEX uq_orders_id;
-- 说明:索引名不会自动改名,仍是 uq_orders_idUSING INDEX 有两个使用条件:
| 条件 | 说明 |
|---|---|
| 索引必须是唯一索引 | 否则报错 is not a unique index |
| 索引不能是表达式/部分索引 | 必须是普通列索引,不能带 WHERE |
| 索引列不能可空 | 需要先 SET NOT NULL,否则报错 |
-- 错误!部分唯一索引不能被提升为主键
CREATE UNIQUE INDEX uq_tmp ON orders (id) WHERE status <> 'cancelled';
ALTER TABLE orders ADD CONSTRAINT pk_x PRIMARY KEY USING INDEX uq_tmp;ERROR: index "uq_tmp" is a partial indexADD PRIMARY KEY 会默认把主键列设为 NOT NULL。如果列里已经有 NULL,加主键就直接失败:
CREATE TABLE legacy (id bigint, name text);
INSERT INTO legacy (name) VALUES ('a'), ('b'); -- 两行 id 都是 NULL
ALTER TABLE legacy ADD PRIMARY KEY (id);ERROR: column "id" of relation "legacy" contains null values处理流程是先补数据、再加约束:
UPDATE legacy SET id = nextval('legacy_id_seq') WHERE id IS NULL;
ALTER TABLE legacy ALTER COLUMN id SET NOT NULL;
ALTER TABLE legacy ADD PRIMARY KEY (id);删除主键与其索引是一步到位的:
ALTER TABLE orders DROP CONSTRAINT pk_orders;
-- 等价写法(按索引名删除)
ALTER TABLE orders DROP CONSTRAINT uq_orders_id;DROP CONSTRAINT 默认 RESTRICT,有外键引用时会失败,需要先处理引用方或显式 CASCADE:
-- 危险!会连带删除引用它的外键约束
ALTER TABLE orders DROP CONSTRAINT pk_orders CASCADE;怎么选主键:代理键 vs 自然键
PostgreSQL 没有「聚簇索引」概念,但主键索引的物理顺序仍会显著影响写入与查询(细节见 B-Tree 索引原理)。
| 维度 | 代理键(Surrogate Key) | 自然键(Natural Key) |
|---|---|---|
| 定义 | 无业务含义的自增/UUID 列 | 有业务含义的列(email、order_no) |
| 稳定性 | 永不变 | 业务规则变化时要改 |
| 宽度 | 8 字节(bigint) | 通常更宽(text) |
| 索引体积 | 小 | 大,级联到外键更明显 |
| 可读性 | 需要 JOIN 才看懂 | 直接可读 |
| 推荐 | 默认选它 | 仅当值确实永不变且短 |
如果确实要用自然键,请把它写成一个额外的 UNIQUE 约束,主键仍保留代理键:
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL,
CONSTRAINT uq_users_email UNIQUE (email) -- 业务唯一键
);单调性对插入的影响是一个容易被低估的点:
| 主键类型 | 插入模式 | 影响 |
|---|---|---|
自增 bigint | 始终追加到索引最右端 | 最好,页分裂少 |
时间有序 UUID(uuidv7()) | 基本追加 | 好,PostgreSQL 18 新增,见下 |
| 随机 UUID v4 | 随机落在索引任意位置 | 差,写放大 + 缓存命中率低 |
| 自然键(字符串) | 取决于分布 | 视分布而定,长度也是成本 |
PostgreSQL 18 新增了 uuidv7(),生成按时间戳有序的 UUID,兼顾了「不可预测/分布式唯一」与「索引友好」:
-- PostgreSQL 18+
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7(),
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
SELECT id FROM events LIMIT 1; id
--------------------------------------
0198f2a4-3c1e-7f2b-9d4a-6b8e0c12a345
(1 row)对比随机 UUID:
-- 不推荐:随机 UUID 主键,写入页随机跳转
CREATE TABLE events_bad (
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
);uuidv7() 有时间戳前缀,新行总是落在索引右端,因此插入局部性好得多。
主键与自增的搭配
PostgreSQL 提供三种自增方式,主键搭配推荐 GENERATED ... AS IDENTITY:
| 写法 | 标准性 | 序列归属 | 推荐度 |
|---|---|---|---|
GENERATED ALWAYS AS IDENTITY | SQL 标准 | 表,自动清理 | 推荐(严格) |
GENERATED BY DEFAULT AS IDENTITY | SQL 标准 | 表,自动清理 | 推荐(可显式插值) |
serial / bigserial | PostgreSQL 方言 | 独立对象,易误删 | 兼容老代码 |
CREATE TABLE users (
-- 推荐:BY DEFAULT 允许数据迁移时显式指定 id
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uq_users_email UNIQUE (email)
);ALWAYS 更严格,导入数据必须加 OVERRIDING SYSTEM VALUE:
CREATE TABLE users_strict (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL
);
-- 错误!ALWAYS 不允许显式赋值
INSERT INTO users_strict (id, email) VALUES (100, 'a@example.com');ERROR: cannot insert a non-DEFAULT value into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.-- 正确:迁移时显式覆盖
INSERT INTO users_strict (id, email) OVERRIDING SYSTEM VALUE VALUES (100, 'a@example.com');
-- 导入完成后把序列推到当前最大值,避免后续自增冲突
SELECT setval(pg_get_serial_sequence('users_strict', 'id'),
(SELECT max(id) FROM users_strict));分区表:分区键必须包含在主键中
这是分区表最常见的设计约束。PostgreSQL 要求唯一约束(含主键)的列必须是分区键的超集,因为唯一性只能在同一分区内保证,跨分区无法全局校验。
-- 正确:分区键就是主键
CREATE TABLE orders (
id bigint NOT NULL,
created_at date NOT NULL,
amount numeric(12,2) NOT NULL,
PRIMARY KEY (id, created_at) -- created_at 是分区键,被包含
) PARTITION BY RANGE (created_at);-- 错误!主键里没有分区键
CREATE TABLE orders_bad (
id bigint NOT NULL,
created_at date NOT NULL,
PRIMARY KEY (id)
) PARTITION BY RANGE (created_at);ERROR: unique constraint on partitioned table must include all partitioning columns
DETAIL: PRIMARY KEY constraint on table "orders_bad" lacks column "created_at" which is part of the partition key.因此「按时间分区 + 单列 id 主键」这两个需求不能同时满足。实际项目里的处理方式:
| 方案 | 说明 | 代价 |
|---|---|---|
| 主键含分区键 | PRIMARY KEY (id, created_at) | 外键引用变复杂,引用方要带上两列 |
| 用全局 ID 但不做数据库唯一约束 | 应用层保证 id 唯一 | 失去数据库兜底 |
| 不分区 | 保留单列主键 | 大表维护成本升高 |
还有个细节:分区表的 id 在全局并不唯一。上面的正确示例中,(1, '2026-01-01') 与 (1, '2026-06-01') 可以共存:
INSERT INTO orders VALUES (1, '2026-01-01', 10.00);
INSERT INTO orders VALUES (1, '2026-06-01', 20.00); -- 允许!不同分区
SELECT count(*) FROM orders WHERE id = 1; count
-------
2
(1 row)所以分区表若要求 id 全局唯一,必须额外用全局序列/UUID 生成来保证,而不能依赖主键。
主键与逻辑复制:REPLICA IDENTITY
逻辑复制(Logical Replication)在 UPDATE / DELETE 时,需要一种方式在订阅端定位到对应行。默认取的就是主键。
-- 查看表的 REPLICA IDENTITY 设置
SELECT relname, relreplident
FROM pg_class
WHERE relname IN ('users', 'logs'); relname | relreplident
---------+--------------
users | d
logs | f
(2 rows)relreplident 取值:
| 值 | 名称 | 含义 |
|---|---|---|
d | DEFAULT | 用主键;无主键则用第一个非空唯一索引 |
n | NOTHING | 不记录旧值,UPDATE/DELETE 无法复制 |
f | FULL | 记录整行旧值,代价最高但无主键也能复制 |
i | INDEX | 用指定的唯一索引 |
没有主键的表,UPDATE / DELETE 在逻辑复制中会失败(订阅端报 cannot delete from table ... because it does not have a replica identity),需要显式改成 FULL:
-- 无主键表的补救(代价:每行变更都要记录全部旧列,WAL 显著增大)
ALTER TABLE logs REPLICA IDENTITY FULL;这条与本节的关系是:主键不只服务于查询,也是逻辑复制的前提。设计新表时把主键定好,复制链路会省掉很多麻烦。逻辑复制的完整配置见后续「高可用与复制」阶段。
用系统表查询主键定义
pg_get_constraintdef() 直接把约束翻译成可读 SQL,是复核与比对环境的首选:
SELECT
c.conname AS constraint_name,
c.contype AS type,
pg_get_constraintdef(c.oid) AS definition,
ci.relname AS index_name
FROM pg_constraint c
LEFT JOIN pg_class ci ON ci.oid = c.conindid
WHERE c.conrelid = 'orders'::regclass
ORDER BY c.contype; constraint_name | type | definition | index_name
-----------------+------+---------------------------+--------------
pk_orders | p | PRIMARY KEY (id, created_at) | orders_pkey
(1 row)也可以用 information_schema(更标准,但不含 PostgreSQL 特有信息):
SELECT tc.constraint_name, tc.constraint_type, kcu.column_name, kcu.ordinal_position
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_name = tc.constraint_name
AND kcu.constraint_schema = tc.constraint_schema
WHERE tc.table_name = 'orders' AND tc.constraint_type = 'PRIMARY KEY'
ORDER BY kcu.ordinal_position;实战示例
场景:把一张已有 200 万行、原本没有主键的 orders 表在线加上主键,并验证过程中的索引与约束。
第 1 步:复现现状
-- 初始表:无主键、id 可空
CREATE TABLE orders (
id bigint,
user_id bigint NOT NULL,
order_no text NOT NULL,
amount numeric(12,2) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders (id, user_id, order_no, amount)
SELECT g, (g % 1000) + 1, 'SO-' || lpad(g::text, 10, '0'), (random() * 1000)::numeric(12,2)
FROM generate_series(1, 2000000) AS g;
-- 记录体积,便于对比
SELECT pg_size_pretty(pg_relation_size('orders')) AS table_size; table_size
------------
137 MB
(1 row)第 2 步:在线建唯一索引(不影响读写)
CREATE UNIQUE INDEX CONCURRENTLY uq_orders_id ON orders (id);CREATE INDEXCONCURRENTLY 不阻塞读写,但可能失败并留下无效索引,务必校验:
SELECT indexrelid::regclass AS index_name, indisvalid, indisready
FROM pg_index
WHERE indrelid = 'orders'::regclass AND indexrelid = 'uq_orders_id'::regclass; index_name | indisvalid | indisready
--------------+------------+------------
uq_orders_id | t | t
(1 row)若 indisvalid = f,需要先删除再重建:
-- 失败时的清理
DROP INDEX CONCURRENTLY uq_orders_id;第 3 步:确认无 NULL,再加主键
SELECT count(*) AS null_rows FROM orders WHERE id IS NULL; null_rows
-----------
0
(1 row)ALTER TABLE orders ADD CONSTRAINT pk_orders PRIMARY KEY USING INDEX uq_orders_id;ALTER TABLE第 4 步:验证结果
\d orders Table "public.orders"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+------------------------------
id | bigint | | not null |
user_id | bigint | | not null |
order_no | text | | not null |
amount | numeric(12,2) | | not null |
created_at | timestamp with time zone | | not null | now()
Indexes:
"uq_orders_id" PRIMARY KEY, btree (id)注意两点:id 自动变成了 not null;索引名仍是 uq_orders_id(USING INDEX 不会改名)。
-- 唯一性确实生效
INSERT INTO orders (id, user_id, order_no, amount) VALUES (1, 1, 'DUP', 1.00);ERROR: duplicate key value violates unique constraint "uq_orders_id"
DETAIL: Key (id)=(1) already exists.第 5 步:补一个外键列索引(主键索引不覆盖 user_id)
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);第 6 步:清理
DROP TABLE orders;注意事项
- 一个表只能有一个主键。 需要多组唯一性时,多余的写
UNIQUE。
-- 错误!
ALTER TABLE users ADD PRIMARY KEY (email);ADD PRIMARY KEY会扫描整表并建索引,且持ACCESS EXCLUSIVE锁。 大表必须走「CREATE UNIQUE INDEX CONCURRENTLY→ADD ... USING INDEX」。
-- 大表上不要这样直接执行:全程阻塞读写
ALTER TABLE orders ADD CONSTRAINT pk_orders PRIMARY KEY (id);ADD PRIMARY KEY会隐式SET NOT NULL。 列中已有NULL时直接失败。
-- 错误!column "id" of relation "legacy" contains null values
ALTER TABLE legacy ADD PRIMARY KEY (id);DROP CONSTRAINT会连带删掉背后的索引。 想保留索引需重建。
-- 删掉主键后,users_pkey 索引也随之消失
ALTER TABLE users DROP CONSTRAINT users_pkey;USING INDEX不能复用部分索引或表达式索引。
-- 错误!index "uq_tmp" is a partial index
ALTER TABLE orders ADD CONSTRAINT pk_x PRIMARY KEY USING INDEX uq_tmp;- 分区表的主键必须包含分区键。 这是硬性限制,不是建议。
-- 错误!unique constraint on partitioned table must include all partitioning columns
CREATE TABLE orders_bad (id bigint, created_at date, PRIMARY KEY (id)) PARTITION BY RANGE (created_at);- 分区表的「主键」不保证全局唯一。 不同分区可以有相同键值,需要全局唯一请用全局序列或 UUID 生成。
INSERT INTO orders VALUES (1, '2026-01-01', 10.00);
INSERT INTO orders VALUES (1, '2026-06-01', 20.00); -- 成功!- 无主键表在逻辑复制中无法
UPDATE/DELETE。 需要REPLICA IDENTITY FULL,但 WAL 体积会明显增大。
ALTER TABLE logs REPLICA IDENTITY FULL;- 随机 UUID 主键会带来写放大。 每次插入都落在索引的随机位置,缓存命中率低、页分裂多。
-- 不推荐:随机 UUID
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
-- 推荐(PostgreSQL 18+):时间有序 UUID
id uuid PRIMARY KEY DEFAULT uuidv7()主键列类型不要用
text/varchar拼接业务信息。 主键索引会级联到每个外键列,宽度成本会被放大数倍。继承表的主键不跨父子生效。 子表可以插入与父表重复的
id(详见 创建表(CREATE TABLE))。GENERATED ALWAYS AS IDENTITY的数据导入需要OVERRIDING SYSTEM VALUE,导入后别忘了setval。
INSERT INTO users_strict (id, email) OVERRIDING SYSTEM VALUE VALUES (100, 'a@example.com');
SELECT setval(pg_get_serial_sequence('users_strict', 'id'), (SELECT max(id) FROM users_strict));最佳实践
- 默认使用
bigint GENERATED BY DEFAULT AS IDENTITY作为代理主键。 不要用serial(序列是独立对象,容易被误删/漏迁),也不要用int(26 亿上限在日志类业务里很容易撞到)。
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY- 业务唯一键额外写成
NOT NULL UNIQUE,不要试图塞进主键。
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email text NOT NULL,
CONSTRAINT uq_users_email UNIQUE (email)- 给主键显式命名。 默认的
<表名>_pkey在跨环境比对和多版本迁移脚本里容易产生歧义。
CONSTRAINT pk_orders PRIMARY KEY (id, created_at)- 大表加主键一律用两步法,并把
lock_timeout设好,避免ALTER TABLE排队堵住后续所有查询。
CREATE UNIQUE INDEX CONCURRENTLY uq_orders_id ON orders (id);
SET lock_timeout = '3s';
ALTER TABLE orders ADD CONSTRAINT pk_orders PRIMARY KEY USING INDEX uq_orders_id;
RESET lock_timeout;- 建唯一索引后校验
indisvalid,CONCURRENTLY失败会静默留下无效索引。
SELECT indexrelid::regclass, indisvalid FROM pg_index
WHERE indrelid = 'orders'::regclass AND NOT indisvalid;- UUID 主键优先用
uuidv7()(PostgreSQL 18+),避免随机 UUID 的索引写放大。
-- PostgreSQL 18+
id uuid PRIMARY KEY DEFAULT uuidv7()分区表设计时先把主键和分区键一起规划。 一旦确定「按时间分区」,就要接受主键必须包含时间列。
新表务必有主键。 它同时决定了外键引用目标与逻辑复制的行标识能力,缺主键的表在复制链路里几乎是不可用的。
上线前用
pg_get_constraintdef()复核主键定义与列顺序。 复合主键的列顺序会直接影响索引可用性。
SELECT conname, pg_get_constraintdef(oid) AS def
FROM pg_constraint
WHERE conrelid = 'orders'::regclass AND contype = 'p';下一节
继续学习:FOREIGN KEY 外键