mirror of https://github.com/postgres/postgres
Add a fast-path optimization for foreign key checks that bypasses SPI by directly probing the unique index on the referenced table. Benchmarking shows ~1.8x speedup for bulk FK inserts (int PK/int FK, 1M rows, where PK table and index are cached). The fast path applies when the referenced table is not partitioned and the constraint does not involve temporal semantics. Otherwise, the existing SPI path is used. This optimization covers only the referential check trigger (RI_FKey_check). The action triggers (CASCADE, SET NULL, SET DEFAULT, RESTRICT, NO ACTION) must find rows on the FK side to modify, which requires a table scan with no guaranteed index available, and then execute DML against those rows through the full executor path including any triggered actions. Replicating that without substantial code duplication is not feasible, so those triggers remain on the SPI path. Extending the fast path to action triggers remains possible as future work if the necessary infrastructure is built. The new ri_FastPathCheck() function extracts the FK values, builds scan keys, performs an index scan, and locks the matching tuple with LockTupleKeyShare via ri_LockPKTuple(), which handles the RI-specific subset of table_tuple_lock() results. If the locked tuple was reached by chasing an update chain (tmfd.traversed), recheck_matched_pk_tuple() verifies that the key is still the same, emulating EvalPlanQual. The scan uses GetTransactionSnapshot(), matching what the SPI path uses (via _SPI_execute_plan pushing GetTransactionSnapshot() as the active snapshot). Under READ COMMITTED this is a fresh snapshot; under REPEATABLE READ / SERIALIZABLE it is the frozen transaction- start snapshot, so PK rows committed after the transaction started are not visible. The ri_CheckPermissions() function performs schema USAGE and table SELECT checks, matching what the SPI path gets implicitly through the executor's permission checks. The fast path also switches to the PK table owner's security context (with SECURITY_NOFORCE_RLS) before the index probe, matching the SPI path where the query runs as the table owner. ri_HashCompareOp() is adjusted to handle cross-type equality operators (e.g. int48eq for int4 PK / int8 FK) which can appear in conpfeqop. The existing code asserted same-type operators only, which was correct for its existing callers (ri_KeysEqual compares same-type FK column values via ff_eq_oprs), but the fast path is the first caller to pass pf_eq_oprs, which can be cross-type. Per-key metadata (compare entries, operator procedures, strategy numbers) is cached in RI_ConstraintInfo via ri_populate_fastpath_metadata() on first use, eliminating repeated calls to ri_HashCompareOp() and get_op_opfamily_properties(). conindid and pk_is_partitioned are also cached at constraint load time, avoiding per-invocation syscache lookups and the need to open pk_rel before deciding whether the fast path applies. New regression tests cover RLS bypass and ACL enforcement for the fast-path permission checks. New isolation tests exercise concurrent PK updates under both READ COMMITTED and REPEATABLE READ. Author: Junwang Zhao <zhjwpku@gmail.com> Co-authored-by: Amit Langote <amitlangote09@gmail.com> Reviewed-by: Haibo Yan <tristan.yim@gmail.com> Tested-by: Tomas Vondra <tomas@vondra.me> Discussion: https://postgr.es/m/CA+HiwqF4C0ws3cO+z5cLkPuvwnAwkSp7sfvgGj3yQ=Li6KNMqA@mail.gmail.commaster
parent
5984ea868e
commit
2da86c1ef9
@ -0,0 +1,105 @@ |
||||
Parsed test spec with 3 sessions |
||||
|
||||
starting permutation: s2b s2ukey s1b s1i s2c s1c s2s s1s |
||||
step s2b: BEGIN; |
||||
step s2ukey: UPDATE parent SET parent_key = 2 WHERE parent_key = 1; |
||||
step s1b: BEGIN; |
||||
step s1i: INSERT INTO child VALUES (1, 1); <waiting ...> |
||||
step s2c: COMMIT; |
||||
step s1i: <... completed> |
||||
ERROR: insert or update on table "child" violates foreign key constraint "child_parent_key_fkey" |
||||
step s1c: COMMIT; |
||||
step s2s: SELECT * FROM parent; |
||||
parent_key|aux |
||||
----------+--- |
||||
2|foo |
||||
(1 row) |
||||
|
||||
step s1s: SELECT * FROM child; |
||||
child_key|parent_key |
||||
---------+---------- |
||||
(0 rows) |
||||
|
||||
|
||||
starting permutation: s2b s2uaux s1b s1i s2c s1c s2s s1s |
||||
step s2b: BEGIN; |
||||
step s2uaux: UPDATE parent SET aux = 'bar' WHERE parent_key = 1; |
||||
step s1b: BEGIN; |
||||
step s1i: INSERT INTO child VALUES (1, 1); |
||||
step s2c: COMMIT; |
||||
step s1c: COMMIT; |
||||
step s2s: SELECT * FROM parent; |
||||
parent_key|aux |
||||
----------+--- |
||||
1|bar |
||||
(1 row) |
||||
|
||||
step s1s: SELECT * FROM child; |
||||
child_key|parent_key |
||||
---------+---------- |
||||
1| 1 |
||||
(1 row) |
||||
|
||||
|
||||
starting permutation: s2b s2ukey s1b s1i s2ukey2 s2c s1c s2s s1s |
||||
step s2b: BEGIN; |
||||
step s2ukey: UPDATE parent SET parent_key = 2 WHERE parent_key = 1; |
||||
step s1b: BEGIN; |
||||
step s1i: INSERT INTO child VALUES (1, 1); <waiting ...> |
||||
step s2ukey2: UPDATE parent SET parent_key = 1 WHERE parent_key = 2; |
||||
step s2c: COMMIT; |
||||
step s1i: <... completed> |
||||
step s1c: COMMIT; |
||||
step s2s: SELECT * FROM parent; |
||||
parent_key|aux |
||||
----------+--- |
||||
1|foo |
||||
(1 row) |
||||
|
||||
step s1s: SELECT * FROM child; |
||||
child_key|parent_key |
||||
---------+---------- |
||||
1| 1 |
||||
(1 row) |
||||
|
||||
|
||||
starting permutation: s2b s2ukey s3b s3i s2c s3c s2s s3s |
||||
step s2b: BEGIN; |
||||
step s2ukey: UPDATE parent SET parent_key = 2 WHERE parent_key = 1; |
||||
step s3b: BEGIN ISOLATION LEVEL REPEATABLE READ; |
||||
step s3i: INSERT INTO child VALUES (2, 1); <waiting ...> |
||||
step s2c: COMMIT; |
||||
step s3i: <... completed> |
||||
ERROR: could not serialize access due to concurrent update |
||||
step s3c: COMMIT; |
||||
step s2s: SELECT * FROM parent; |
||||
parent_key|aux |
||||
----------+--- |
||||
2|foo |
||||
(1 row) |
||||
|
||||
step s3s: SELECT * FROM child; |
||||
child_key|parent_key |
||||
---------+---------- |
||||
(0 rows) |
||||
|
||||
|
||||
starting permutation: s2b s2uaux s3b s3i s2c s3c s2s s3s |
||||
step s2b: BEGIN; |
||||
step s2uaux: UPDATE parent SET aux = 'bar' WHERE parent_key = 1; |
||||
step s3b: BEGIN ISOLATION LEVEL REPEATABLE READ; |
||||
step s3i: INSERT INTO child VALUES (2, 1); |
||||
step s2c: COMMIT; |
||||
step s3c: COMMIT; |
||||
step s2s: SELECT * FROM parent; |
||||
parent_key|aux |
||||
----------+--- |
||||
1|bar |
||||
(1 row) |
||||
|
||||
step s3s: SELECT * FROM child; |
||||
child_key|parent_key |
||||
---------+---------- |
||||
2| 1 |
||||
(1 row) |
||||
|
||||
@ -0,0 +1,53 @@ |
||||
# Tests that an INSERT on referencing table correctly fails when |
||||
# the referenced value disappears due to a concurrent update |
||||
setup |
||||
{ |
||||
CREATE TABLE parent ( |
||||
parent_key int PRIMARY KEY, |
||||
aux text NOT NULL |
||||
); |
||||
|
||||
CREATE TABLE child ( |
||||
child_key int PRIMARY KEY, |
||||
parent_key int8 NOT NULL REFERENCES parent |
||||
); |
||||
|
||||
INSERT INTO parent VALUES (1, 'foo'); |
||||
} |
||||
|
||||
teardown |
||||
{ |
||||
DROP TABLE parent, child; |
||||
} |
||||
|
||||
session s1 |
||||
step s1b { BEGIN; } |
||||
step s1i { INSERT INTO child VALUES (1, 1); } |
||||
step s1c { COMMIT; } |
||||
step s1s { SELECT * FROM child; } |
||||
|
||||
session s2 |
||||
step s2b { BEGIN; } |
||||
step s2ukey { UPDATE parent SET parent_key = 2 WHERE parent_key = 1; } |
||||
step s2uaux { UPDATE parent SET aux = 'bar' WHERE parent_key = 1; } |
||||
step s2ukey2 { UPDATE parent SET parent_key = 1 WHERE parent_key = 2; } |
||||
step s2c { COMMIT; } |
||||
step s2s { SELECT * FROM parent; } |
||||
|
||||
session s3 |
||||
step s3b { BEGIN ISOLATION LEVEL REPEATABLE READ; } |
||||
step s3i { INSERT INTO child VALUES (2, 1); } |
||||
step s3c { COMMIT; } |
||||
step s3s { SELECT * FROM child; } |
||||
|
||||
# fail |
||||
permutation s2b s2ukey s1b s1i s2c s1c s2s s1s |
||||
# ok |
||||
permutation s2b s2uaux s1b s1i s2c s1c s2s s1s |
||||
# ok |
||||
permutation s2b s2ukey s1b s1i s2ukey2 s2c s1c s2s s1s |
||||
|
||||
# RR: key update -> serialization failure |
||||
permutation s2b s2ukey s3b s3i s2c s3c s2s s3s |
||||
# RR: non-key update -> old version visible via transaction snapshot |
||||
permutation s2b s2uaux s3b s3i s2c s3c s2s s3s |
||||
Loading…
Reference in new issue