perf(orm): use EXISTS instead of COUNT subquery for to-one relation filters#2579
Conversation
…ilters Sibling fix to zenstackhq#2455. `buildToOneRelationFilter` was emitting `(select count(1) ...) > 0` for relation predicates like `{ relation: { field: value } }` and `{ relation: { is: {...} } }`. PostgreSQL cannot convert that into a semi-join, so the aggregate fires once per parent row and performance collapses on large tables. Switch to the `buildExistsExpression` helper already used by `buildToManyRelationFilter`, mapping: - `is: {...}` / default payload -> EXISTS - `is: null` -> NOT EXISTS - `isNot: null` -> EXISTS - `isNot: {...}` -> NOT EXISTS OR NOT EXISTS(with filter) Same semantics, but the planner can now turn it into a proper semi-join. On a 2.6M-row product_site table with the right index, execution drops from ~2100 ms to ~49 ms (~43x). Fixes zenstackhq#2578
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR refactors the to-one relation filter generation in the base dialect to replace COUNT-based subqueries with EXISTS-based semantics, improving query optimization. A regression test is added to validate the correctness of various to-one relation filter scenarios. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ymc9
left a comment
There was a problem hiding this comment.
LGTM. Thanks for making this great fix @evgenovalov !
Summary
COUNT(*) > 0subqueries withEXISTSinbuildToOneRelationFilterfor relation predicates like{ relation: { field: value } },{ relation: { is: {...} } }, and{ relation: { isNot: {...} } }buildToManyRelationFilter(some/none/every)is: {...}/ default payload →EXISTS (...)is: null→NOT EXISTS (...)isNot: null→EXISTS (...)isNot: {...}→NOT EXISTS (...) OR NOT EXISTS(... WHERE filter)The old
COUNTpattern prevents PostgreSQL from converting the subquery into a semi-join, so the aggregate executes once per parent row. On a production dataset with 2.6Mproduct_siterows and an ~8.5M-row join table, execution time drops from ~2100 ms to ~49 ms (~43×) with the same indexes and the same filter set.EXISTSshort-circuits on the first match and the planner picks aParallel Hash Semi Joindriven by the existing covering index.Fixes #2578.
Test plan
tests/regression/test/issue-2578.test.tscovering:{ user: { name: 'A' } }){ user: { is: {...} } }{ user: { is: null } }{ user: { isNot: null } }{ user: { isNot: {...} } }(including rows with no related record)issue-2440.test.ts(thesome/none/everysibling) still passesbuildExistsExpressionoverride carries over unchanged (derived-table wrapping is handled by the existing helper)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes