Repository navigation
perf(db): plan multi-value #d reads with their values - #9131
Conversation
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
5f71e71 to
28d9f34
Compare
A REQ with several `#d` values for NIP-33 kinds (a client resolving channel metadata for up to 128 channels, say) ran as a named, cached statement. After a few runs on a connection, Postgres can switch it to a generic plan. That plan walks the author's `created_at` index and filters `d_tag` row by row. Its LIMIT (one row per value) is never reached early, so it reads every stored version of the kind instead of probing `idx_events_parameterized` once per value. Multi-value `#d` reads now run as unnamed statements, as tag-value matches already do, so every call is planned with its values. The values are also bound as one array (`d_tag = ANY($n)`), so the number of values no longer changes the SQL text. The page SELECT and the COUNT share one helper for the `d` predicate instead of two copies. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
28d9f34 to
49bb40f
Compare
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 LGTM. Every read path that takes d_tags (page SELECT, COUNT, thread replies, which were already unnamed) now plans with its values, and d_tag = ANY($n) on the TEXT column stays an index condition on idx_events_parameterized. The shared push_d_tag_filter removes the duplicated predicate, and both tests pin the behavior that matters.
* origin/main: (54 commits) release(mobile): push candidate tags as the operator, not the App (#9084) test(mobile): check posterless video previews renew the relay assertion (#9144) feat(relay): log WebSocket EVENT writes on the access target (#9154) perf(db): plan multi-value #d reads with their values (#9131) fix(mobile): confirm before starting a Huddle from the header (#9155) feat(cli): send NIP-FI evidence via credential chain and Buzz desktop (#8999) fix(mobile): find verified people who have no profile (#9086) feat(protocol): define compact membership discovery (BUZZ-615) (#9117) perf(db): hydrate search hits with exact event timestamps (#9126) perf(db): prune post-commit push enqueue by signed event timestamp (#9128) perf(db): use exact event timestamps for transactional deletion (#9130) perf(redis): index mesh ready records and share one Redis connection (#9113) fix(mobile): delay iOS message actions until a deliberate hold (#9049) feat(cli): user status expiry and status read (#8777) feat(cli): send a thread message to the channel (#8778) feat(relay): add kind 30180 custom emoji entry (#8783) feat(cli): read-only unread state (NIP-RS) (#8789) feat(cli): report a message to relay moderators (#8829) buzz-db: read explicit #h sets one channel at a time (#9110) feat(cli): list huddles and their participants (#8830) ... Signed-off-by: Taylor Ho <[email protected]>
Summary
A REQ with several
#dvalues for a NIP-33 kind runs as a named, cached statement. A client resolving channel metadata for up to 128 channels sends this kind of REQ. After a few runs on a connection, Postgres can switch the statement to a generic plan, which ignores the bound values:dmatch stops being an index condition onidx_events_parameterized.created_atorder and filtersd_tagrow by row.LIMITis one row per requested value, so the walk never stops early. It reads every stored version of the kind, not one probe per value.This change makes three fixes:
#dreads now run as unnamed statements, as tag-value matches already do (plans_with_bound_values), so Postgres plans every call with its values. This covers both the pageSELECTand theCOUNT.d_tag = ANY($n)) instead of one bind per value. The SQL text no longer changes with the number of values, which matches how channel scope is bound.SELECTand theCOUNTnow share onepush_d_tag_filterhelper instead of two copies of thedpredicate.What got simpler:
dpredicate blocks.#dread no longer produces a different statement for every value count.Related issue
N/A. None found.
Testing
d_tag_sql_does_not_depend_on_value_count: the SQL text is identical for 2 and 128 values, for both the page query and the count query.value_matched_queries_are_not_cached_as_prepared_statements, extended from the tag-value test. It runs each#dpage and count query 6 times and asserts nod_tag = ANY(statement is left inpg_prepared_statements. It also checks the page and count results.d_tagsfromplans_with_bound_valuesfails the Postgres test;IN (...)list fails the unit test.cargo fmt --checkcargo clippy -p buzz-db -p buzz-relay --all-targets -D warningsbuzz-dbunit tests: 215 passedscripts/postgres-test-run.sh -p buzz-relay -p buzz-db --lib --tests: 1291 passedscripts/test-postgres-test-discovery.shEXPLAIN ANALYZEon synthetic data (50k versions of one author's kind 39000 rows, 128#dvalues):force_custom_plan,d_tag = ANY(...)is in the Index Cond on every partition.force_generic_plan, the partition holding the rows dropsd_tagto a row filter.created_atwalk.🤖 Generated with Claude Code