Skip to content

fix(storage): qualify presence store lookups by bucket so the primary key serves them - #579

Merged
rickyrombo merged 1 commit into
mainfrom
fix/presence-store-pk-lookup
Sep 10, 2026
Merged

fix(storage): qualify presence store lookups by bucket so the primary key serves them#579
rickyrombo merged 1 commit into
mainfrom
fix/presence-store-pk-lookup

Conversation

@rickyrombo

Copy link
Copy Markdown
Contributor

Summary

presenceForCIDs filtered blob_presence on key alone, but the primary key is (bucket, key). On Postgres 15 that predicate cannot use the index, so every repair batch seq-scanned the whole table — ~4.8M rows per 1000-upload batch on a store-all node. The cost is a CPU-bound filter of each row against the key array, not I/O, so it does not improve as the table warms.

This adds bucket = any($1) naming both bucket labels. The "fetch both buckets and let Lookup pick" semantics are unchanged; the query becomes a handful of primary-key index probes.

Measurements

On a 4.8M-row copy of the table in the mediorum test Postgres (15), one batch of 1,360 keys:

query plan time
key = any($1), array inlined as a Const Seq Scan, hashed ScalarArrayOp ~0.7–1s
key = any($1), bare Param (generic plan) Seq Scan, linear compare per row ~104s
bucket = any($1) and key = any($2) Index Scan on blob_presence_pkey ~100ms

The first row is what pgx's custom plans give today, so on audius.rickyrombo.com this is a ~1s tax per batch rather than the main cost. The second row is the cliff this removes: if the planner ever switches that statement to a generic plan, each batch would spend minutes in the query.

Test plan

  • make test-mediorum passes
  • New TestPresenceForCIDsCoversEveryBucketLabel: a row recorded under the archive label resolves through the same query, so a label left out of the predicate's list cannot silently read as "missing"

🤖 Generated with Claude Code

… key serves them

presenceForCIDs filtered blob_presence on key alone, but the primary key is
(bucket, key). On Postgres 15 that predicate cannot use the index, so every
repair batch read the whole table. On a store-all node that is ~4.8M rows per
1000-upload batch, and the cost is a CPU-bound filter of each row against the
key array rather than I/O, so it does not improve as the table warms. Measured
on a 4.8M-row copy: ~0.7-1s per batch when the planner inlines the array and
hashes it, 100s+ when it runs as a bare parameter under a generic plan.

Naming both bucket labels in the predicate keeps the "fetch both buckets, let
Lookup pick" semantics and turns the query into a handful of index probes
(~100ms on the same copy).

Adds a test that a row recorded under the archive label resolves through the
same query, since a label missing from that list would read as absent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rickyrombo
rickyrombo merged commit 3456c3f into main Sep 10, 2026
5 checks passed
@rickyrombo
rickyrombo deleted the fix/presence-store-pk-lookup branch September 10, 2026 01:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant