Skip to content

fix: Eliminate top DB queries — hourly_statistics MV, balance N+1, VACUUM in transaction#672

Open
nelitow wants to merge 8 commits intomainfrom
nj/fix/db-performance-v2
Open

fix: Eliminate top DB queries — hourly_statistics MV, balance N+1, VACUUM in transaction#672
nelitow wants to merge 8 commits intomainfrom
nj/fix/db-performance-v2

Conversation

@nelitow
Copy link
Copy Markdown
Contributor

@nelitow nelitow commented Mar 13, 2026

Summary

  • Replace hourly_statistics MV with incremental aggregate table (migration 046): The MV refresh was the feat: add pre loaded data on fuel-core #1 DB query at 201,290ms latency, scanning all 24h of transactions every 30min. New approach upserts only the affected hour per block during indexing (~25ms vs 2,132ms — 85x improvement locally). Idempotent: recomputes the full hour from indexer.transactions so re-processing blocks never double-counts.
  • Batch balance lookups in IndexBalance: Was doing 1 SELECT per (account, asset) pair at 43.27 calls/sec (top load query). Now does 1 batched DISTINCT ON query per block using WHERE (account_hash, asset_id) IN (...).
  • Add composite index on balance(account_hash, asset_id, _id DESC) (migration 047): Single index seek instead of multi-index scan + sort.
  • Fix queryWithTimeout for VACUUM and REFRESH MATERIALIZED VIEW CONCURRENTLY: Both fail inside a BEGIN/COMMIT block. All VACUUM jobs were silently retrying forever with "VACUUM cannot run inside a transaction block". Fix: detect these queries and apply timeouts at session level with SET/RESET instead of SET LOCAL.

Test plan

  • Run migrations 046 and 047 on testnet — verify hourly_statistics_agg table created, old MV and jobs deleted
  • Start syncer and confirm hourly_statistics_agg is populated per block
  • Confirm BlockDAO.getTotalFee returns data from new table
  • Confirm VACUUM database jobs complete without error in logs
  • Monitor AWS Performance Insights — top SQL queries should no longer show MV refresh or high-frequency balance lookups

Manual testnet cleanup needed

Run this on testnet DB since migration 046 already executed there with the old exact-match DELETE:

DELETE FROM indexer.database_jobs WHERE query LIKE '%hourly_statistics%';

…regate table, batch balance lookups, fix VACUUM in transaction

- Replace hourly_statistics MV with incremental aggregate table (migration 046):
  - MV refresh was top query at 201,290ms latency, running every 30min
  - Per-block upsert recomputes only the affected hour (~25ms vs 2,132ms locally)
  - 85x improvement; cost scales with 1 hour of data not 24h history
- Add composite index (account_hash, asset_id, _id DESC) on balance table (migration 047):
  - Enables single index seek for IndexBalance lookups instead of multi-index scan
- Batch balance lookups in IndexBalance: 1 query per block instead of N+1 per (account, asset) pair
- Fix queryWithTimeout: VACUUM and REFRESH MATERIALIZED VIEW CONCURRENTLY cannot
  run inside a transaction block — detect these and use session-level SET instead of SET LOCAL
@nelitow nelitow requested a review from helciofranco as a code owner March 13, 2026 18:43
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fuel-explorer-v2-vite Ready Ready Preview Mar 17, 2026 5:22pm

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 13, 2026

PR Summary

Medium Risk
Touches indexing and database maintenance paths (hourly fee/gas aggregation, balance indexing, and timeout handling), so a logic error could skew stats or leave long-running queries with incorrect timeout behavior, but changes are localized and performance-motivated.

Overview
Replaces the expensive hourly_statistics materialized view with an incremental aggregate table. Migration 046 creates/backfills indexer.hourly_statistics_agg, deletes the old MV refresh/analyze jobs, and drops the MV; NewAddBlockRange now upserts per-block fee/gas into the hourly row, and BlockDAO.getTotalFee reads from the new table.

Eliminates balance lookup N+1s during indexing. IndexBalance now collects unique (account_hash, asset_id) pairs per block and fetches latest balances in a single unnest + LATERAL query (paired with migration 047 adding a composite (account_hash, asset_id, _id DESC) index).

Reduces unnecessary DB load and fixes timeout execution for non-transactional commands. Migration 048 drops the unused recent_account_transactions_mv and its job, and DatabaseConnection.queryWithTimeout now detects VACUUM/REFRESH MATERIALIZED VIEW CONCURRENTLY to apply session-level SET/RESET timeouts instead of SET LOCAL in a transaction.

Written by Cursor Bugbot for commit 23155aa. This will update automatically on new commits. Configure here.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Add migration 048 to remove the unused indexer.recent_account_transactions_mv. The migration deletes any scheduled database_jobs that reference the view and drops the materialized view with CASCADE. This MV was created in migration 030, is not queried anywhere in the codebase, and its 4-hour refresh appeared as a top load query despite having no consumers.
nelitow added 2 commits March 17, 2026 12:30
Use COALESCE for total_fee and total_gas_used when backfilling indexer.hourly_statistics_agg from indexer.hourly_statistics to avoid inserting NULLs. This ensures missing values are treated as 0 during the migration (packages/graphql/database/migrations/046_replace_hourly_statistics_mv_with_agg_table.sql) and preserves the existing ON CONFLICT update behavior.
Replace the multi-placeholder IN tuple query with an unnest($1, $2) approach and a LATERAL subquery to select the most recent balance per (account_hash, asset_id). Build separate accountHashes and assetIds arrays as query params instead of expanding pair placeholders. This simplifies parameter handling and reliably returns the latest balance (ORDER BY _id DESC LIMIT 1) for each requested pair.
Update migration 047 to clarify the need for a composite index on (account_hash, asset_id, _id DESC) including rationale and benchmark results, and keep the CREATE INDEX CONCURRENTLY statement. Add migration 049 to recompute hourly_statistics_agg from the blocks table, overwriting total_fee and total_gas_used to fix issues from the previous MV-based backfill (incorrect gas totals and missed blocks).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant