|
| 1 | +Originally from:[tweet](https://twitter.com/samokhvalov/status/1732056107483636188),[LinkedIn post](). |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +#How to add a foreign key |
| 6 | + |
| 7 | +>I post a new PostgreSQL "howto" article every day. Join me in this |
| 8 | +>journey –[subscribe](https://twitter.com/samokhvalov/), provide feedback, share! |
| 9 | +
|
| 10 | +Adding a foreign key (FK) is straightforward: |
| 11 | + |
| 12 | +```sql |
| 13 | +altertable messages |
| 14 | +addconstraint fk_messages_users |
| 15 | +foreign key (user_id) |
| 16 | +references users(id); |
| 17 | +``` |
| 18 | + |
| 19 | +However, this operation requires locks on both tables involved: |
| 20 | + |
| 21 | +1.`ShareRowExclusiveLock`,`RowShareLock`, and`AccessShareLock` on the referenced table, in this example it's |
| 22 | +`users` (plus`AccessShareLock` on its primary key, PK). This blocks any data modifications to`users` (`UPDATE`, |
| 23 | +`DELETE`,`INSERT`), as well as DDL. |
| 24 | + |
| 25 | +2.`ShareRowExclusiveLock` and`AccessShareLock` to the referencing table, in this example`messages` |
| 26 | + (plus,`AccessShareLock` to its PK). Again, this blocks writes to this table, and DDL. |
| 27 | + |
| 28 | +And to ensure that the existing data doesn't violate the constraint, full table scans are needed – so the more data the |
| 29 | +tables have, the longer this implicit scan is going to take. During which, the locks are going to block all writes and |
| 30 | +DDL to the table. |
| 31 | + |
| 32 | +To avoid downtime, we need to create the FK in three steps: |
| 33 | + |
| 34 | +1. Quickly define a constraint with the flag`NOT VALID`. |
| 35 | +2. For the existing data, if needed, fix rows that would break the FK. |
| 36 | +3. In a separate transaction,`validate` the constraint for existing rows. |
| 37 | + |
| 38 | +##Step 1: Add FK with NOT VALID |
| 39 | + |
| 40 | +Example: |
| 41 | + |
| 42 | +```sql |
| 43 | +altertable messages |
| 44 | +addconstraint fk_messages_users |
| 45 | +foreign key (user_id) |
| 46 | +references users(id) |
| 47 | +not valid; |
| 48 | +``` |
| 49 | + |
| 50 | +This requires a very brief`ShareRowExclusiveLock` and`AccessShareLock` on both tables, so on loaded systems, it is |
| 51 | +still recommended to execute this with low`lock_timeout` and retries (read: |
| 52 | +[Zero-downtime database schema migrations](https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries)), |
| 53 | +to avoid lock queue blocking writes to the tables. |
| 54 | + |
| 55 | +🖋️**Important:** once the constraint with`NOT VALID` is in place, new writes are checked (while old rows have not |
| 56 | +been yet verified and some of them might violate the constraint): |
| 57 | + |
| 58 | +```sql |
| 59 | +nik=# \d messages |
| 60 | + Table"public.messages" |
| 61 | + Column | Type | Collation | Nullable | Default |
| 62 | +---------+--------+-----------+----------+--------- |
| 63 | + id |bigint | |not null | |
| 64 | + user_id |bigint | | | |
| 65 | +Indexes: |
| 66 | +"messages_pkey"PRIMARY KEY, btree (id) |
| 67 | +Foreign-key constraints: |
| 68 | +"fk_messages_users"FOREIGN KEY (user_id)REFERENCES users(id) NOT VALID |
| 69 | + |
| 70 | +nik=# insert into messages(id, user_id) select 1, -1; |
| 71 | +ERROR: insertorupdateon table`messages` violatesforeign keyconstraint"fk_messages_users" |
| 72 | +DETAIL: Key (user_id)=(-1) is not presentin table`users`. |
| 73 | +``` |
| 74 | + |
| 75 | +##Step 2: Fix existing data if needed |
| 76 | + |
| 77 | +Now, with the FK created with`NOT VALID`, we know that Postgres already checks all the new data against the new |
| 78 | +constraint, but for the old data, some rows might still be violating it. Before the next step, it makes sense to ensure |
| 79 | +there are no old rows violating our new FK. It can be done using this query: |
| 80 | + |
| 81 | +```sql |
| 82 | +select id |
| 83 | +from messages |
| 84 | +where |
| 85 | + user_id notin ( |
| 86 | +select idfrom users |
| 87 | + ); |
| 88 | +``` |
| 89 | + |
| 90 | +This query scans the whole`messages` table, so it will take significant time. It is worth ensuring that`users` is |
| 91 | +accessed via PK here (depends on the data volumes and planner settings). |
| 92 | + |
| 93 | +The rows identified by this query will block the next step, so they need to either be deleted or changed to avoid the FK |
| 94 | +violation. |
| 95 | + |
| 96 | +##Step 3: Validation |
| 97 | + |
| 98 | +To complete the process, we need to`validate` the old rows in a separate transaction: |
| 99 | + |
| 100 | +```sql |
| 101 | +altertable messages |
| 102 | +validateconstraint fk_messages_users; |
| 103 | +``` |
| 104 | + |
| 105 | +If the tables are large, this`ALTER` is going to take significant time. However, it only acquires |
| 106 | +`ShareUpdateExclusiveLock` and`AccessShareLock` on the referencing table (`messages` in this example). |
| 107 | + |
| 108 | +Therefore, it doesn't block`UPDATE` /`DELETE` /`INSERT`, but it conflicts with DDL and`VACUUM` runs. On the |
| 109 | +referenced table (`users` here),`AccessShareLock` and`RowShareLock` are acquired. |
| 110 | + |
| 111 | +As usual, if`autovacuum` processes this table in the transaction ID wraparound prevention mode, it won't yield – so |
| 112 | +before running this, make sure there is no`autovacuum` running in this mode or DDL in progress. |