feat: queries benchmarking and optimization (#68)#534
Merged
cameri merged 5 commits intocameri:mainfrom Apr 19, 2026
Merged
Conversation
Collaborator
There was a problem hiding this comment.
Pull request overview
Adds operational tooling and schema changes to benchmark and optimize PostgreSQL “hot path” queries for relay workloads (NIP-01 subscriptions, vanish checks, retention purge, invoice polling).
Changes:
- Adds a read-only
EXPLAIN (ANALYZE, BUFFERS)benchmark script and wires it intonpm run db:benchmark. - Introduces a non-transactional Knex migration creating three partial indexes via
CREATE INDEX CONCURRENTLY IF NOT EXISTS. - Documents the new indexes and benchmarking workflow in
README.mdandCONFIGURATION.md, and adds a changeset entry.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/scripts/benchmark-queries.ts |
New benchmark harness that runs representative EXPLAIN ANALYZE query shapes and summarizes planner/index usage. |
package.json |
Adds db:benchmark script entry. |
migrations/20260420_120000_add_hot_path_indexes.js |
Adds concurrent partial indexes intended for hot query paths on events and invoices. |
README.md |
Adds a section describing how to run the benchmark and what it measures. |
CONFIGURATION.md |
Documents key indexes and includes benchmark invocation instructions. |
.changeset/hot-path-indexes-benchmark.md |
Release note entry for the new indexes/tooling/docs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cameri
approved these changes
Apr 19, 2026
This was referenced Apr 22, 2026
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.
Description
Adds PostgreSQL indexes aligned with the relay's hottest query paths (NIP-01
REQfilters, vanish checks, retention purge, pending invoices), following Heroku's PostgreSQL indexing guidance.20260420_120000_add_hot_path_indexes.js: creates three indexes withCREATE INDEX CONCURRENTLY IF NOT EXISTSin a non-transactional migration (so concurrent builds do not block reads). The compositeevents (event_pubkey, event_kind, event_created_at DESC, event_id)index is non-partial (matchesEventRepository.findByFilters, which does not filter ondeleted_at). The other two are partial:events (deleted_at) WHERE deleted_at IS NOT NULLandinvoices (created_at) WHERE status = 'pending'.src/scripts/benchmark-queries.ts+npm run db:benchmark: read-onlyEXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON)harness for the representative queries (loads.envautomatically vianode --env-file-if-exists=.env).scripts/verify-index-impact.ts+npm run db:verify-index-impact: seeds test data, drops/recreates the new indexes, prints median-of-NEXPLAIN ANALYZEbefore/after numbers so reviewers can reproduce the speed-up.InvoiceRepository.findPendingInvoices: nowORDER BY created_at ASCfor deterministic FIFO polling — aligned with the partial pending-invoice index.CONFIGURATION.mdandREADME.mddescribe the indexes and how to run the benchmarks (nodotenv/configdependency).Related Issue
Closes #68
Motivation and Context
Public relays scale on Postgres; subscription
REQqueries, per-event vanish checks, retention purges, and invoice polling must stay cheap aseventsgrows. Targeted indexes reduce sequential scans and sort work; partial indexes on low-cardinality tails (soft-deleted rows, pending invoices) keep those indexes small.Benchmark results
Environment. Postgres 14 (docker
postgres:14-alpine), seeded vianpm run db:verify-index-impact -- --events 200000 --pubkeys 500 --runs 5:events= 200,000 rows,invoices= 1,000 pending rows, synthetic kind distribution[0,1,1,1,1,1,3,4,7,7,1059,62], ~2% soft-deleted. Identical schema to production afternpm run db:migrate.Before vs after (median of 5
EXPLAIN ANALYZEruns, fromnpm run db:verify-index-impact)authors + kindORDER BYcreated_at DESC, event_id ASCLIMIT 500events_event_pubkey_index→events_active_pubkey_kind_created_at_idxhasActiveRequestToVanish(pubkey + kind=62 + deleted_at IS NULL) LIMIT 1events_event_pubkey_index→events_active_pubkey_kind_created_at_idx(composite pays off when the sample pubkey has more rows; for a cold pubkey the single-column index is already ~40 µs)events_deleted_at_partial_idx(bitmap)findPendingInvoicesORDER BYcreated_at ASCLIMIT 500invoices_pending_created_at_idx(Index Scan, no sort)kind + time rangeORDER BYcreated_at DESC, event_id ASCLIMIT 500events_event_created_at_index; kept in the benchmark to confirm that i did not regress itThe two cases where the new index does not speed things up are expected: the vanish-check query is already sub-millisecond on any single-column pubkey index, and the time-range REQ is covered by the pre-existing
events_event_created_at_index. The new composite index wins on every query shape where the planner has a choice.npm run db:benchmarkoutput (same DB, 3 runs each)Every new index shows up in a plan:
events_active_pubkey_kind_created_at_idx,events_deleted_at_partial_idx,invoices_pending_created_at_idx. ThefindPendingInvoicescase went fromSeq Scan + Sortto a pureIndex Scan— that is the largest latency win and it grows with the size of theinvoicestable.Reproduce
How Has This Been Tested?
Automated (this branch)
npm run build:checktsc --project tsconfig.build.json --noEmit)npm run lintnpm run test:unitnpm run knipDatabase / indexes (real Postgres, numbers above)
npm run db:migrate— applies the new migration concurrently alongside the 27 pre-existing ones.npm run db:benchmark -- --runs 5— read-only EXPLAIN harness (no writes).npm run db:verify-index-impact -- --events 200000 --pubkeys 500 --runs 5— seeds a throwaway dataset, drops/recreates the new indexes, prints the before/after table shown above.Screenshots (if appropriate)
Types of changes
Checklist