Skip to content

Commit afbc7e2

Browse files
author
xiongcc
committed
添加第70章:How to add a foreign key
1 parent 25df0da commit afbc7e2

File tree

1 file changed

+96
-0
lines changed

1 file changed

+96
-0
lines changed

How to add a foreign key.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# How to add a foreign key
2+
3+
>我每天都会发布一篇新的 PostgreSQL "howto" 文章。加入我的旅程吧 — 订阅、提供反馈、分享!
4+
5+
添加外键 (FK) 很简单:
6+
7+
```sql
8+
alter table messages
9+
add constraint fk_messages_users
10+
foreign key (user_id)
11+
references users(id);
12+
```
13+
14+
然而,此操作需要锁定涉及的两个表:
15+
16+
- 被引用表上的 `ShareRowExclusiveLock``RowShareLock``AccessShareLock` 锁,在本例中为 `users` (包括该表主键上的 `AccessShareLock`)。这会阻止对 `users` 表的任何数据修改 (`UPDATE``DELETE``INSERT`),以及 DDL 操作。
17+
- 引用表上的 `ShareRowExclusiveLock``AccessShareLock` ,在本例中为 `messages` (包括其主键的`AccessShareLock`)。同样,这也会阻止对该表的写入以及 DDL 操作。
18+
19+
为了确保现有数据不违反约束,需要对表进行全表扫描 — 因此,表中的数据越多,隐式扫描的时间越长。在此期间,锁会阻塞所有写入和 DDL 操作。
20+
21+
要避免停机,我们需要分三步创建 FK:
22+
23+
1. 快速定义带有 `NOT VALID` 标志的约束。
24+
2. 对于现有数据,如果需要,修复会破坏 FK 的行。
25+
3. 在单独的事务中,验证现有行是否满足约束。
26+
27+
## 第1步:使用NOT VALID添加FK
28+
29+
```sql
30+
alter table messages
31+
add constraint fk_messages_users
32+
foreign key (user_id)
33+
references users(id)
34+
not valid;
35+
```
36+
37+
此操作仅需短暂的 `ShareRowExclusiveLock``AccessShareLock`,所以在负载较大的系统上,建议设置较低的 `lock_timeout` 并进行重试 (参照 [Zero-downtime database schema migrations](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries)),以避免锁队列阻塞对表的写入。
38+
39+
🖋️ **重要**:一旦带有 `NOT VALID` 的约束生效,新的写入会立即进行约束检查 (而旧数据尚未验证,可能会违反约束):
40+
41+
```sql
42+
nik=# \d messages
43+
Table "public.messages"
44+
Column | Type | Collation | Nullable | Default
45+
---------+--------+-----------+----------+---------
46+
id | bigint | | not null |
47+
user_id | bigint | | |
48+
Indexes:
49+
"messages_pkey" PRIMARY KEY, btree (id)
50+
Foreign-key constraints:
51+
"fk_messages_users" FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID
52+
53+
nik=# insert into messages(id, user_id) select 1, -1;
54+
ERROR: insert or update on table `messages` violates foreign key constraint "fk_messages_users"
55+
DETAIL: Key (user_id)=(-1) is not present in table `users`.
56+
```
57+
58+
## 第2步:如有需要,修复现有数据
59+
60+
添加了 `NOT VALID` 标志的 FK 后,Postgres 已经根据新约束检查了所有新数据,但旧数据可能仍有部分行违反该约束。在进行下一步操作之前,有必要确保没有违反新 FK 的旧行。可以使用以下查询来完成:
61+
62+
```sql
63+
select id
64+
from messages
65+
where
66+
user_id not in (
67+
select id from users
68+
);
69+
```
70+
71+
该查询会扫描整个 `messages` 表,因此可能需要较长时间。确保 `users` 通过主键访问以提高性能 (这取决于数据量和规划器设置)。
72+
73+
找到的行将阻止下一步操作,因此需要删除或进行更改,以避免 FK 冲突。
74+
75+
## 第3步:验证
76+
77+
完成后,需要在单独的事务中验证旧行:
78+
79+
```sql
80+
alter table messages
81+
validate constraint fk_messages_users;
82+
```
83+
84+
如果表较大,此 `ALTER` 操作可能需要较长时间。然而,它仅需要获取引用表 (本例中为 `messages`) 上的 `ShareUpdateExclusiveLock``AccessShareLock`
85+
86+
因此并不会阻塞 `UPDATE` / `DELETE` / `INSERT`,但会与 DDL 和 `VACUUM` 冲突。对于被引用的表 (本例中为 `users`),需要获取 `AccessShareLock``RowShareLock`
87+
88+
与往常一样,如果 `autovacuum` 在预防事务 ID 回卷的模式下处理该表,它将不会"妥协"— 因此在运行此操作之前,请确保没有 `autovacuum` 在该模式下运行,也没有 DDL 操作在进行。
89+
90+
## 我见
91+
92+
📒 TODO:包括前一篇文章,都提到了在
93+
94+
>processes this table in the transaction ID wraparound prevention mode
95+
96+
即使不在冻结,也需要获取 `ShareUpdateExclusiveLock`,尚不明白作者为何需要特别提及?待验证。

0 commit comments

Comments
 (0)