Summary
The raw_events table created in ensureSchema() (services/indexer/src/index.ts) has no TTL, no retention policy, and no partitioning. Every Soroban event ever observed is stored forever. At Stellar mainnet throughput (~100 events/ledger, ~1 ledger/5s), the table accumulates ~1.7 million rows/day. After ~6 months the table exceeds 300 M rows, sequential scans on the processed_at IS NULL filter degrade, and the UPDATE raw_events SET processed_at = NOW() WHERE ledger_sequence = $1 AND event_index = $2 loop — one UPDATE per event inside the commit transaction — causes severe write amplification.
Root Cause
// index.ts — ensureSchema()
CREATE TABLE IF NOT EXISTS raw_events (
id BIGSERIAL NOT NULL,
ledger_sequence BIGINT NOT NULL,
event_index INT NOT NULL,
-- ...
processed_at TIMESTAMPTZ -- NULL until pipeline stamps it
)
Issues:
- No
processed_at index → full table scan to find unprocessed rows after a crash.
- N individual
UPDATE statements inside the batch transaction (one per event) instead of a single bulk update.
- No partition pruning — the entire historical table is scanned on every gap-detection query.
- No archival or eviction of rows where
processed_at IS NOT NULL and ledger_sequence < cursor - retention_window.
Impact
- Table bloat degrades indexer throughput non-linearly over time.
- Crash recovery (re-scan for unprocessed rows) becomes increasingly slow.
- PostgreSQL VACUUM struggles to reclaim space from the high UPDATE churn on
processed_at.
Fix
- Partition by ledger range (monthly or by 1M-ledger buckets) so old partitions can be dropped atomically.
- Bulk update
processed_at in a single statement:
UPDATE raw_events SET processed_at = NOW()
WHERE (ledger_sequence, event_index) = ANY($1::record[])
- Add index
CREATE INDEX ON raw_events (processed_at) WHERE processed_at IS NULL.
- Retention policy: a scheduled job (or partition drop) removes fully-processed rows older than a configurable retention window (default 30 days).
- Document the migration path for existing deployments that already have a monolithic
raw_events table.
Acceptance Criteria
Summary
The
raw_eventstable created inensureSchema()(services/indexer/src/index.ts) has no TTL, no retention policy, and no partitioning. Every Soroban event ever observed is stored forever. At Stellar mainnet throughput (~100 events/ledger, ~1 ledger/5s), the table accumulates ~1.7 million rows/day. After ~6 months the table exceeds 300 M rows, sequential scans on theprocessed_at IS NULLfilter degrade, and theUPDATE raw_events SET processed_at = NOW() WHERE ledger_sequence = $1 AND event_index = $2loop — one UPDATE per event inside the commit transaction — causes severe write amplification.Root Cause
Issues:
processed_atindex → full table scan to find unprocessed rows after a crash.UPDATEstatements inside the batch transaction (one per event) instead of a single bulk update.processed_at IS NOT NULLandledger_sequence < cursor - retention_window.Impact
processed_at.Fix
processed_atin a single statement:CREATE INDEX ON raw_events (processed_at) WHERE processed_at IS NULL.raw_eventstable.Acceptance Criteria
raw_eventsis range-partitioned by ledger sequence (monthly cadence or equivalent).processed_at IS NULLexists and is used by the EXPLAIN plan for crash-recovery queries.processed_atstamp is a single bulk UPDATE, not N individual UPDATEs.pg_cronornode-cronjob drops/archives partitions older than the retention window.