|
14 | 14 | exports.config = { transaction: false } |
15 | 15 |
|
16 | 16 | exports.up = async function (knex) { |
17 | | - // Covers the hottest write-adjacent reads: |
| 17 | + // Covers the hottest subscription / per-message reads: |
18 | 18 | // |
19 | | - // 1. `EventRepository.hasActiveRequestToVanish(pubkey)` |
| 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: |
20 | 26 | // WHERE event_pubkey = ? AND event_kind = 62 AND deleted_at IS NULL |
21 | | - // -- invoked on every inbound event via UserRepository.isVanished |
22 | 27 | // |
23 | | - // 2. `EventRepository.deleteByPubkeyExceptKinds(pubkey, kinds)` |
| 28 | + // 3. `EventRepository.deleteByPubkeyExceptKinds(pubkey, kinds)`: |
24 | 29 | // WHERE event_pubkey = ? AND event_kind NOT IN (...) AND deleted_at IS NULL |
25 | 30 | // |
26 | | - // 3. NIP-01 REQ with `authors` + `kinds` filters ordered by created_at: |
27 | | - // WHERE event_pubkey IN (...) AND event_kind IN (...) |
28 | | - // ORDER BY event_created_at DESC LIMIT N |
| 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. |
29 | 37 | // |
30 | | - // Partial on `deleted_at IS NULL` so soft-deleted rows never bloat the index. |
31 | | - // DESC on event_created_at lets the planner satisfy LIMIT N without a sort. |
| 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. |
32 | 42 | await knex.raw(` |
33 | 43 | CREATE INDEX CONCURRENTLY IF NOT EXISTS events_active_pubkey_kind_created_at_idx |
34 | | - ON events (event_pubkey, event_kind, event_created_at DESC) |
35 | | - WHERE deleted_at IS NULL |
| 44 | + ON events (event_pubkey, event_kind, event_created_at DESC, event_id) |
36 | 45 | `) |
37 | 46 |
|
38 | | - // Supports the retention/purge scan in `deleteExpiredAndRetained`: |
| 47 | + // Supports the retention / purge scan in `deleteExpiredAndRetained` and the |
| 48 | + // vanish hard-delete follow-up: |
39 | 49 | // WHERE deleted_at IS NOT NULL |
40 | | - // Partial index is tiny because well-maintained relays hard-delete these rows |
41 | | - // periodically and most events have deleted_at IS 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. |
42 | 52 | await knex.raw(` |
43 | 53 | CREATE INDEX CONCURRENTLY IF NOT EXISTS events_deleted_at_partial_idx |
44 | 54 | ON events (deleted_at) |
45 | 55 | WHERE deleted_at IS NOT NULL |
46 | 56 | `) |
47 | 57 |
|
48 | | - // Supports `InvoiceRepository.findPendingInvoices` which is polled by the |
49 | | - // maintenance worker: |
50 | | - // WHERE status = 'pending' ORDER BY created_at |
51 | | - // Partial on status='pending' so the index only contains the rows we scan. |
| 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). |
52 | 65 | await knex.raw(` |
53 | 66 | CREATE INDEX CONCURRENTLY IF NOT EXISTS invoices_pending_created_at_idx |
54 | 67 | ON invoices (created_at) |
|
0 commit comments