|
| 1 | +/** |
| 2 | + * Add narrow, query-driven indexes to cover the hottest read paths. |
| 3 | + * |
| 4 | + * Each index is created with CREATE INDEX CONCURRENTLY so the migration can be |
| 5 | + * applied to a running relay without taking an ACCESS EXCLUSIVE lock on the |
| 6 | + * events table. CONCURRENTLY is not allowed inside a transaction, so this |
| 7 | + * migration opts out of Knex's default transactional wrapper via |
| 8 | + * `exports.config.transaction = false`. |
| 9 | + * |
| 10 | + * Rationale for each index is documented inline. See also: |
| 11 | + * https://devcenter.heroku.com/articles/postgresql-indexes |
| 12 | + */ |
| 13 | + |
| 14 | +exports.config = { transaction: false } |
| 15 | + |
| 16 | +exports.up = async function (knex) { |
| 17 | + // Covers the hottest subscription / per-message reads: |
| 18 | + // |
| 19 | + // 1. NIP-01 REQ with `authors` + `kinds` ordered by created_at DESC |
| 20 | + // (see EventRepository.findByFilters): |
| 21 | + // WHERE event_pubkey = ? AND event_kind IN (...) |
| 22 | + // ORDER BY event_created_at DESC, event_id ASC LIMIT N |
| 23 | + // |
| 24 | + // 2. `EventRepository.hasActiveRequestToVanish(pubkey)` — invoked on every |
| 25 | + // inbound event via UserRepository.isVanished: |
| 26 | + // WHERE event_pubkey = ? AND event_kind = 62 AND deleted_at IS NULL |
| 27 | + // |
| 28 | + // 3. `EventRepository.deleteByPubkeyExceptKinds(pubkey, kinds)`: |
| 29 | + // WHERE event_pubkey = ? AND event_kind NOT IN (...) AND deleted_at IS NULL |
| 30 | + // |
| 31 | + // The index is intentionally NOT partial on `deleted_at IS NULL`: the REQ |
| 32 | + // subscription path in findByFilters does not currently add that predicate, |
| 33 | + // so a partial index would be ineligible for the most important query shape. |
| 34 | + // Soft-deleted rows are a small fraction of total rows in practice (they get |
| 35 | + // hard-deleted by the retention sweep), so the bloat is negligible compared |
| 36 | + // to the benefit of the index being usable by the hot path. |
| 37 | + // |
| 38 | + // Including `event_id` as the final column makes the composite key match the |
| 39 | + // full ORDER BY (created_at DESC, event_id ASC) used by findByFilters, so the |
| 40 | + // planner can satisfy LIMIT N directly from the index without an extra sort |
| 41 | + // step for the tie-breaker. |
| 42 | + await knex.raw(` |
| 43 | + CREATE INDEX CONCURRENTLY IF NOT EXISTS events_active_pubkey_kind_created_at_idx |
| 44 | + ON events (event_pubkey, event_kind, event_created_at DESC, event_id) |
| 45 | + `) |
| 46 | + |
| 47 | + // Supports the retention / purge scan in `deleteExpiredAndRetained` and the |
| 48 | + // vanish hard-delete follow-up: |
| 49 | + // WHERE deleted_at IS NOT NULL |
| 50 | + // Partial index is tiny because well-maintained relays hard-delete these |
| 51 | + // rows periodically and the vast majority of events have deleted_at IS NULL. |
| 52 | + await knex.raw(` |
| 53 | + CREATE INDEX CONCURRENTLY IF NOT EXISTS events_deleted_at_partial_idx |
| 54 | + ON events (deleted_at) |
| 55 | + WHERE deleted_at IS NOT NULL |
| 56 | + `) |
| 57 | + |
| 58 | + // Supports `InvoiceRepository.findPendingInvoices`, which is polled by the |
| 59 | + // maintenance worker to detect settled invoices: |
| 60 | + // WHERE status = 'pending' ORDER BY created_at ASC OFFSET ? LIMIT ? |
| 61 | + // Partial on status = 'pending' so the index only contains the rows the |
| 62 | + // poller actually scans. Keyed on `created_at` so the planner can satisfy |
| 63 | + // the ORDER BY straight from the index (FIFO polling, bounded tail latency |
| 64 | + // even with large pending backlogs). |
| 65 | + await knex.raw(` |
| 66 | + CREATE INDEX CONCURRENTLY IF NOT EXISTS invoices_pending_created_at_idx |
| 67 | + ON invoices (created_at) |
| 68 | + WHERE status = 'pending' |
| 69 | + `) |
| 70 | +} |
| 71 | + |
| 72 | +exports.down = async function (knex) { |
| 73 | + await knex.raw('DROP INDEX CONCURRENTLY IF EXISTS invoices_pending_created_at_idx') |
| 74 | + await knex.raw('DROP INDEX CONCURRENTLY IF EXISTS events_deleted_at_partial_idx') |
| 75 | + await knex.raw('DROP INDEX CONCURRENTLY IF EXISTS events_active_pubkey_kind_created_at_idx') |
| 76 | +} |
0 commit comments