perf(gen): JOIN a DISTINCT unnest(...) for composite-key relationship loaders & counts on PostgreSQL#718
Open
sandonemaki wants to merge 8 commits into
Conversation
DISTINCT unnest(...) for composite-key relationship loaders & counts on PostgreSQL
Contributor
Author
|
@stephenafamo The failing check (Test / test (stable)) appears to be unrelated to the changes in this PR. I've confirmed that all tests pass locally. |
Owner
|
Can you rebase on main and try again? |
Move tuple assignment to shared clause/setcols.go with SetColsOptions.RowPrefix for dialect-specific ToRow rendering. Expose um/im.SetCols on SQLite and update docs.
…n psql Generated <Parent>Slice relationship queries and the batch count method LoadCount<Rel> filtered children with `fk IN (SELECT unnest(CAST($1 AS T[])))`. For a single join column on PostgreSQL, emit `fk = ANY(CAST($1 AS T[]))` instead, so the planner can use an index scan on the FK rather than a Seq Scan + Hash Semi Join. Composite keys keep the original unnest-IN form.
… psql Composite-key relationship loaders and LoadCount used `(a, b) IN (SELECT unnest($1), unnest($2))`, which mis-estimates row counts and forces a Seq Scan + Hash Semi Join. Switch to an INNER JOIN on a DISTINCT unnest so the planner can use an indexed nested loop. Single-column keys are unchanged.
6841eff to
03eb642
Compare
Owner
|
Kindly fix the merge conflicts, also make sure you're only including your commits, not commits from other PRs that have now been merged |
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.
perf(gen): JOIN a
DISTINCT unnest(...)for composite-key relationship loaders & counts on PostgreSQLSummary
Generated PostgreSQL composite-key relationship Slice loaders
(
gen/templates/models/table/009_rel_query.go.tpl) and relationship counts(
gen/templates/counts/table/115_counts.go.tpl) previously filtered children with:The
unnestsubquery gives the planner a fixed row-count estimate, so it tends topick a Seq Scan + Hash Semi Join on the child table even when a usable index
exists.
This PR changes the composite-key filter to
INNER JOINaDISTINCT unnest(...)of the parent keys:
The planner can now use an Indexed Nested Loop (or combine per-column indexes
via BitmapAnd), while the parameter count stays bounded — one array per key column,
regardless of batch size.
Why JOIN (and not
IN (VALUES ...)or a composite= ANY)(a,b) IN (SELECT unnest(...))(before)(a,b) IN (VALUES (...), ...)JOIN unnest(...) AS k(a,b)(this PR)Scope
009_rel_query.go.tpl— composite-key relationship Slice loaders.115_counts.go.tpl— composite-key relationship counts.col = ANY($1)from perf(gen): use col = ANY(\$1) for single-column relationship loaders on psql #714).INlist).Behavior (no functional change)
DISTINCTprevents row multiplication from duplicateparent keys, so the loaded children and the counts match the previous output exactly.
INand the JOIN.nested-loop path, and the joined
keyscolumns are not in theSELECTlist(used only in
ON), so the typed scan path is unaffected.Runtime behavior: before vs after (by composite-index presence)
(a,b) IN (SELECT unnest…)INsubqueryJOIN (DISTINCT unnest…)BitmapAnd. Only when no usable index exists at all does it fall back to Before’s Seq Scan.DISTINCTover the bounded parent-key set.Verification
(
references videos(user_id, sponsor_id)): the generated loaders(
<Parent>Slice.<Relationship>) and counts (<Parent>Slice.LoadCount<Relationship>)emit the expected
JOIN (SELECT DISTINCT ... FROM unnest(...) ...)and compilecleanly (
go build ./models/).EXPLAIN ANALYZE benchmark (PostgreSQL 18.4, composite index on join columns)
Tested against a child table with a composite index on
(par_a, par_b)and uniformlydistributed random data. The 50M-row cases use the exact SQL each template emits
(including type casts); smaller cases use equivalent SQL without explicit casts — the
plan is identical. The Returned rows column confirms both forms return identical results.
Both forms use
Index Scanon the composite index in all cases. The speedup comes fromrow-count estimation accuracy: the old
ProjectSetnode (two separateunnest()calls)over-estimates by 100–500×, causing buffer eviction and disk reads at scale. The new
Function Scannode (singleunnest($1, $2)) estimates accurately, keeping all readsin
shared_buffers.Checklist
golangci-lint runreports 0 issuesgofumpt -lreports no changes (changed files are.tplonly; flagged.gofiles are pre-existing and unrelated to this PR)pgx-v5-std: assemble / generate / generate_with_aliases)modernc/ncruces: assemble / generate / generate_with_aliases)EXPLAIN ANALYZEconfirms composite index is used and results are identical before/after (see benchmark above)./gen/...unit tests pass