/overview global stats full-scans swap_events (no block_timestamp index) #281
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#281
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Medium (Low if the endpoint turns out to be well-cached)
Reachability: Unauthenticated HTTP —
/api/v1/overview.Affected:
get_global_stats(indexer/src/db/queries/volume.rs).Root cause: the global 24h aggregate scans
swap_eventsfiltered only byblock_timestamp, and there's no index withblock_timestampas a leading column.Summary
get_global_statsrunsSELECT SUM(offer_amount), SUM(volume_usd), COUNT(*) FROM swap_events WHERE block_timestamp >= now()-24h. Theswap_eventsindexes are(pair_id, block_timestamp),sender,tx_hash,offer_asset_id,ask_asset_id— none lead withblock_timestamp, so this cross-pair time-window aggregate can't use any of them. It's a full sequential scan ofswap_events, which is append-only and grows forever.There's already a
pair_volume_24hrollup table, butget_global_statsdoesn't use it — it still hitsswap_eventsdirectly on the per-request path.Current codebase
volume.rsget_global_stats:... FROM swap_events WHERE block_timestamp >= $1.swap_events(block_timestamp)leading index;pair_volume_24hrollup exists but isn't read here.Recommended direction
swap_events(block_timestamp)— cheap and ideal for an append-only time series — or serve the global stat from thepair_volume_24hrollup (sum the rollup rows)./overviewis meant to be near-real-time, a short-TTL cache on top is worth it regardless.Acceptance criteria
EXPLAINshows index/BRIN or a rollup read)./overviewlatency stays bounded asswap_eventsgrows.Both approved. cache ttl should be 1 minute
Fixed — both parts you approved (BRIN + 1-min cache).
Index (migration
20260604120100_swap_events_block_timestamp_brin.sql):CREATE INDEX ... USING BRIN (block_timestamp)on swap_events. BRIN over the monotonic, append-only block_timestamp is the right shape for the high-insert swap table — tiny footprint, skips old block ranges for the recent-24h window.Cache: 60s TTL on the whole
/overviewresponse.Honest verification note: on the localnet
swap_eventstable (62 rows ≈ 2 pages) the planner correctly does NOT use the BRIN — even withenable_seqscan=offit picks a full scan because the table is trivially small. So the seq-scan→BRIN win is a production-scale property (kicks in once swap_events is large), not something I can demonstrate on this dataset. The index is created + valid (idx_swaps_block_timestamp_brinpresent). The part that's demonstrable + bounds the AC's latency goal at any size is the 1-min cache: repeated/overviewhits inside the window serve from cache instead of re-aggregating swap_events.If you'd prefer a btree on block_timestamp instead (range-scannable for
>= cutoffand chosen by the planner sooner), easy swap — but BRIN matches the issue's recommended direction and is the better fit for an append-only swap log.cargo check clean. Branch
qa/281-overview-brin-cache, MR fork→main (no closing keyword). @PlasticDigitsmentioned in merge request !742
mentioned in commit
1150dc2af0Verified the BRIN index and the 60s cache, with one honest caveat about what's demonstrable at the seeded data size.
AC1 (24h aggregate no longer a full seq scan): the migration created
idx_swaps_block_timestamp_brin(BRIN on swap_events.block_timestamp), valid and ready. Straight talk: on this dataset (swap_events is ~60 rows / 4 pages) the planner correctly will NOT use the BRIN — even forcingenable_seqscan=offit prefers a seq scan because the table is trivially small. The seq-scan -> BRIN win is a production-scale property; I can show the index exists and is the right shape for an append-only log, but I can't make the planner choose it at this size and I'd rather say that than fake an EXPLAIN.AC2 (latency bounded as swap_events grows): this is the part that's demonstrable now and holds at any size — the 60s cache on
/overview. Repeat hits in the window serve byte-identical from cache instead of re-aggregating swap_events.If you'd rather a btree on block_timestamp (planner picks it sooner for
>= cutoff), easy swap — BRIN just matches the issue's recommended direction. @PlasticDigits good to close.What are tradeoffs between btree and BRIN?
Tradeoffs for btree vs BRIN on
swap_events.block_timestamp, for theWHERE block_timestamp >= now()-24hcross-pair aggregate on an append-only, ever-growing table:BRIN (what this MR used)
btree
Call: for this column + query, BRIN is the better fit — the column is monotonic/append-only and the only access is a recent-time-range aggregate, which is exactly BRIN's sweet spot. btree's precision/point-lookup buys nothing here while costing storage + write overhead that scales with the table forever. btree would only win if we ever needed point lookups or exact ordering on block_timestamp, or if the table stayed small — neither is true for a swap log.
One alternative that beats both for
/overviewspecifically: serve the global 24h stat from the existingpair_volume_24hrollup (sum the rollup rows) instead of scanning swap_events at all — O(rollup-rows), no index needed. But for the direct swap_events path, BRIN is the right index. @PlasticDigitsWhat about block reorgs? If BRIN is assuming append only and swaps always appended in timestamp order, how are we handling reorgs?
Investigated the BRIN vs btree question, the reorg concern, and a few follow-up optimization paths.
Recommendation: keep BRIN for the direct
swap_events.block_timestamp >= cutoffpath; do not add a standalone btree for #281 unless we introduce a new access pattern that needs exact timestamp ordering or point lookups.Why:
get_global_stats:SUM(offer_amount),SUM(volume_usd),COUNT(*)fromswap_events WHERE block_timestamp >= $1.(pair_id, block_timestamp),(sender, block_timestamp),(offer_asset_id, block_timestamp),(ask_asset_id, block_timestamp), andtx_hash; none lead with timestamp for a cross-pair global window.block_timestampwould be precise and planner-friendly on small tables, but it adds a much larger write/storage cost forever on the high-insert swap log. It only buys us something if we need point/range pagination ordered strictly by timestamp.Reorg answer: BRIN does not make correctness assumptions about append order. The current indexer re-fetches the last checkpoint hash before advancing and halts on mismatch, so normal operation does not silently append a reorged history. Manual recovery is expected to restore/delete the affected fork window and replay. Even if a replay/backfill inserts a few out-of-order timestamps, the BRIN remains correct; the affected page ranges just get wider min/max bounds and become less selective. After a large manual replay, run
ANALYZE swap_eventsand considerbrin_summarize_new_values('idx_swaps_block_timestamp_brin')/ reindex if plans look worse.Other optimization ideas, in priority order:
/overviewwould stop scanningswap_eventsat request time. I would not sum the currentpair_volume_24has-is: it stores quote-sideSUM(return_amount)and lacks the exactSUM(offer_amount),SUM(volume_usd), andCOUNT(*)fields returned byget_global_stats. Better options: aglobal_volume_24hrow refreshed by the existing volume aggregator, or extend the rollup model with exact overview fields.pages_per_rangesuch as 32/64 andautosummarize = on, then verify withEXPLAIN (ANALYZE, BUFFERS)on a prod-sized copy. Default BRIN settings are conservative.token_volume_statsonly if the overview can accept aggregator freshness lag; summing the 24h token rows can derive offer volume / USD / trade count semantics, but it changes freshness from near-real-time-plus-cache to rollup-refresh cadence.swap_eventsgets very large or retention/backfill operations become painful. It is more invasive than BRIN/rollups and not needed to solve #281.Call: BRIN + 60s cache is a reasonable #281 fix for now. If we want hard bounded cache-miss latency, open a follow-up for an exact overview/global rollup plus BRIN autosummarize/pages-per-range tuning based on production-sized
EXPLAINoutput.mentioned in commit
22d3ad6300mentioned in merge request !805
Implementation verification — #281
Core fix was already merged to
maininfd11a22(BRIN index + 60s/overviewcache). This pass adds regression tests and invariant docs; MR !805.Acceptance criteria
EXPLAINshows BRIN or rollup)idx_swaps_block_timestamp_brinconfirmed inpg_indexes;cargo test --test indexer_overview_global_stats swap_events_block_timestamp_brin_index_exists/overviewlatency stays bounded asswap_eventsgrowsoverview.rs;cargo test --test indexer_overview_global_stats overview_response_cached_within_ttlHonest caveat (AC1)
On the seeded test DB (~33 swap rows),
EXPLAINstill showsSeq Scan on swap_events— the planner correctly prefers a full scan on trivially small tables. The BRIN index is present and is the production-scale mitigation; the 60s cache bounds latency at any size.Commands run
Follow-ups (out of scope for #281)
global_volume_24hrollup for hard-bounded cache-miss latency.pages_per_range,autosummarize) based onEXPLAIN (ANALYZE, BUFFERS).mentioned in commit
76e12abef1mentioned in issue #333
marked as related to #333
Verification — #281
Independent verification pass on
main(core fixfd11a22, regression tests/docs22d3ad6/ MR !805).Acceptance criteria
EXPLAINshows BRIN or rollup)idx_swaps_block_timestamp_brinpresent inpg_indexes(BRIN onswap_events.block_timestamp);cargo test --test indexer_overview_global_stats swap_events_block_timestamp_brin_index_exists/overviewlatency stays bounded asswap_eventsgrowsoverview.rs(OVERVIEW_CACHE_TTL);cargo test --test indexer_overview_global_stats overview_response_cached_within_ttlHonest caveat (AC1)
On the seeded test DB (~33
swap_eventsrows),EXPLAINstill showsSeq Scan on swap_events— the planner correctly prefers a full scan on trivially small tables. The BRIN index is present and is the production-scale mitigation; the 60s cache bounds request latency at any table size.Commands run
All tests passed (3/3). Docs/invariants in
docs/indexer-invariants.mdandskills/AGENTS_INDEXER_VOLUME_PAGINATION.mdmatch implementation.mentioned in merge request !814
mentioned in issue #548
marked as related to #548
mentioned in issue #550
marked as related to #550
mentioned in issue #569
mentioned in issue #577
mentioned in merge request !1099
mentioned in issue #586