Repository navigation
perf: PG19 RI fast path for FK checks + planner join-path allocations (#20) - #41
Merged
Merged
Conversation
…heck RI_FKey_check now probes the referenced table's unique index directly and KEY SHARE locks the matching row (ri_FastPathCheck, ri_FastPathProbeOne, ri_LockPKTuple, recheck_matched_pk_tuple, ri_check_fastpath_index, ri_populate_fastpath_metadata, build_index_scankeys, ri_CheckPermissions, ri_CheckFunctionPermissions), instead of running the cached SPI plan per row. Everything else keeps the unchanged SPI path. Ported from PG19's ri_triggers.c as it stands after upstream removed batching, so only the per-row path exists. Exclusions, as upstream: the fast path is skipped for partitioned referenced tables, temporal (PERIOD) FKs, non-btree referenced indexes, index collations differing from the referenced column's, pf_eq operators that are no longer the opfamily's equality member, and FK columns that need a cast function (not binary- coercible) to the operator's input type. The eligibility verdict and the scan-key metadata are cached on the constraint-cache entry; this port hands out clones of RiConstraintInfo, so PG19's dead-FastPathMeta list is not needed. Both open upstream issues are handled: the row lock is refused in a read-only transaction (non-temp PK) like ExecCheckXactReadOnly refuses the SPI query, and the lock uses the scan snapshot's curcid, as ExecLockRows uses es_output_cid, so "attempted to lock invisible tuple" cannot arise. Deliberate deviations from PG19, to match pgx's SPI path exactly: the read-only error names the command "SELECT" (pgx's ExecCheckXactReadOnly text), and a concurrent delete under RR/SERIALIZABLE reports "could not serialize access due to concurrent update" (ExecLockRows' text in both C and pgx). Errors raised on the fast path carry no SPI CONTEXT line, as in PG19. New intra-workspace deps of ri_triggers (no external crates; Cargo.lock only lists them for ri_triggers): indexam, tableam, exectuples, types_scan, types_slot for the index scan and tuple lock, namespace_seams for isTempNamespace, objectaccess for InvokeFunctionExecuteHook.
Behavior-preserving (same iteration order, plans and costs); every loop that now reads a rel's path list in place only adds paths to the joinrel, and create_unique_path caches on the rel without touching its lists. joinpath.rs - match_unsorted_outer, consider_parallel_nestloop/_mergejoin, hash_inner_and_outer: index outerrel/innerrel pathlist, partial_pathlist and cheapest_parameterized_paths in place instead of pgvec_clone_shallow snapshots; outer pathkeys go into one scratch buffer reused per outer path. - generate_mergejoin_paths: take the caller's outer_pathkeys copy instead of cloning outerpath's again; read innerrel's pathlist in place; compute the trimmed mergeclauses at most once per sortkeycnt without the extra clones of mergeclauses/innersortkeys/merge_pathkeys. - sort_inner_and_outer: no clones for the partial merge leg; parallel-safe inner lookup reads pathlist in place. - try_mergejoin_path/try_partial_mergejoin_path: borrow key/clause slices; sort-key elision is a slice rebinding instead of clearing owned vectors. - try_nestloop_path, try_partial_nestloop_path, clause_sides_match_join, select_mergejoin_clauses, compute_semi_anti_join_factors, add_paths_to_joinrel (param_source_rels), get_memoize_path: borrow relids and rel/path lists instead of relids_copy / Relids::clone / list clones. pathkeys.rs - build_join_pathkeys -> build_join_pathkeys_len: the join's pathkeys are a prefix of the outer pathkeys, so callers borrow a slice (no per-outer-path vector); truncate_useless_pathkeys shares the count via useful_pathkeys_len. pathnode - create_mergejoin_path takes slices and copies them with exact-size allocations, so precheck-rejected candidates allocate nothing. - create_nestloop_path filters restrict clauses straight into a pre-sized vector; borrows the outer/inner relids. - calc_non_nestloop_required_outer borrows instead of copying four relids. costsize - final_cost_nestloop/hashjoin/mergejoin pass the (not yet arena-owned) path's clause lists directly instead of cloning them; hashjoin re-borrows the inner rel's relids instead of cloning them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands the two patches contributed in #20 by @hoangquocvietuet, cherry-picked onto current
mainwith their commits and authorship unchanged. They applied cleanly (no conflicts,Cargo.lockdelta is the new intra-workspace deps ofri_triggers).Fixes #20
What changes
ri_triggers: PostgreSQL 19 per-row RI fast path (67572923cb).RI_FKey_checkprobes the referenced unique index and KEY SHARE locks the row directly instead of running the cached SPI plan per row. Falls back to the unchanged SPI path for partitioned referenced tables, temporal FKs, non-btree indexes, collation mismatches, operators no longer in the opfamily, and FK columns that need a cast. Mirrors upstream masterri_FastPathCheckstep for step (CCI under the PK lock, constraint reload, snapshot taken after the lock, owner security context, permission checks,TM_*handling as inExecLockRows).66ef4718b8). Behavior-preserving: path lists iterated in place, borrowed relids/clause lists, exact-size copies afteradd_path_precheck,build_join_pathkeys_lenprefix slicing.Verification (this branch, fast-profile build, M1 Pro,
synchronous_commit=off,fsync=off)Issue benchmark, medians of 3:
mainunnest … WITH ORDINALITY … USING (ord)EXPLAIN ×300Correctness:
cargo test --profile fast-profile -p ri_triggers -p planner -p pathnode -p costsize: 22 / 203 / 0 / 30 passed.benchmarks/pgx/regress-baseline.pyon both binaries: 231 tests, 219 byte-exact and the same 12 pre-existing diffs on each; 239 of 240 result files byte-identical. The one difference ismemoize.out, where only the leader/worker split of cache hits moved (run-dependent, and that test already fails against expected onmain).COLLATE "C"key, partitioned PK, self-reference, MATCH FULL, unprivileged role, FORCE RLS on the PK table, ON CONFLICT DO UPDATE, UNIQUE … INCLUDE target, REINDEX between inserts): identical output tomainand same outcomes as PG16. Only difference: errors raised on the fast path carry no SPICONTEXT:line, as in upstream PG19.mainand PG16, sameCONTEXT:caveat.Notes
🤖 Generated with Claude Code