1+ -- =============================================================================
2+ -- MIGRATION SCRIPT: getAddressTransactions Optimization
3+ -- Eliminates query timeouts for high-transaction addresses
4+ -- =============================================================================
5+
6+ -- Verify no conflicting indexes exist (something like this)
7+ SELECT schemaname, tablename, indexname
8+ FROM pg_indexes
9+ WHERE indexname LIKE ' %_canonical_optimized'
10+ OR indexname LIKE ' %_subquery_optimized'
11+ ORDER BY tablename, indexname;
12+
13+ -- =============================================================================
14+ -- CANONICAL TRANSACTION FILTERING
15+ -- =============================================================================
16+ -- Problem: The query joins txs table for every transaction to check canonical = TRUE
17+ -- and microblock_canonical = TRUE. This creates expensive nested loops that scan
18+ -- thousands of transactions, applying filters after the join.
19+ --
20+ -- Solution: Create partial index containing only canonical transactions with
21+ -- built-in ordering. This eliminates the filter step entirely and supports
22+ -- efficient sorting without additional operations
23+ --
24+ -- Trade-off: Additional storage on txs table to get significant query speedup.
25+ -- But index only contains canonical transactions which should reduce overall size
26+
27+ CREATE INDEX CONCURRENTLY idx_txs_canonical_optimized
28+ ON txs (tx_id, index_block_hash, microblock_hash, block_height DESC , microblock_sequence DESC , tx_index DESC )
29+ WHERE canonical = TRUE AND microblock_canonical = TRUE;
30+
31+ -- Optional index `address_txs` CTE if it were materialized as its own table
32+ -- CREATE INDEX CONCURRENTLY idx_address_txs_dedupe
33+ -- ON address_txs (tx_id, index_block_hash, microblock_hash);
34+
35+ ANALYZE txs;
36+
37+ -- =============================================================================
38+ -- EVENT TABLE SUBQUERIES
39+ -- =============================================================================
40+ -- Problem: Each transaction requires 11 correlated subqueries that scan event tables
41+ -- using expensive bitmap operations. For 50 returned transactions, this means 550
42+ -- separate bitmap scans combining tx_id and index_block_hash lookups
43+ --
44+ -- Solution: Create compound indexes that cover all subquery conditions in a single
45+ -- lookup. The INCLUDE clause adds frequently accessed columns without increasing
46+ -- the index key size, enabling index-only scans
47+ --
48+ -- Trade-off: Additional storage per event table to remov bitmap
49+ -- operations and heap lookups in subqueries
50+
51+ -- STX Events: used in 5 subqueries per transaction
52+ CREATE INDEX CONCURRENTLY idx_stx_events_subquery_optimized
53+ ON stx_events (tx_id, index_block_hash, microblock_hash, asset_event_type_id)
54+ INCLUDE (amount, sender, recipient);
55+
56+ -- FT Events: used in 3 subqueries per transaction
57+ CREATE INDEX CONCURRENTLY idx_ft_events_subquery_optimized
58+ ON ft_events (tx_id, index_block_hash, microblock_hash, asset_event_type_id)
59+ INCLUDE (sender, recipient);
60+
61+ -- NFT Events: used in 3 subqueries
62+ CREATE INDEX CONCURRENTLY idx_nft_events_subquery_optimized
63+ ON nft_events (tx_id, index_block_hash, microblock_hash, asset_event_type_id)
64+ INCLUDE (sender, recipient);
65+
66+ ANALYZE stx_events, ft_events, nft_events;
67+
68+ -- =============================================================================
69+ -- MONITORING / VERIFICATION
70+ -- =============================================================================
71+
72+ -- Ensure all indexes were created successfully and are valid
73+ SELECT
74+ psi .schemaname ,
75+ psi .relname as tablename,
76+ psi .indexrelname ,
77+ pi .indisvalid as is_valid,
78+ pi .indisready as is_ready,
79+ pg_size_pretty(pg_relation_size(psi .indexrelid )) as index_size
80+ FROM pg_stat_user_indexes psi
81+ JOIN pg_index pi ON psi .indexrelid = pi .indexrelid
82+ WHERE psi .indexrelname LIKE ' %_canonical_optimized'
83+ OR psi .indexrelname LIKE ' %_subquery_optimized'
84+ ORDER BY psi .relname , psi .indexrelname ;
85+
86+ -- Create view to monitor ongoing performance tracking
87+ CREATE OR REPLACE VIEW address_transactions_performance AS
88+ SELECT
89+ schemaname,
90+ relname as tablename,
91+ pg_stat_user_indexes .indexrelname ,
92+ idx_scan as times_used,
93+ pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
94+ CASE
95+ WHEN idx_scan = 0 THEN ' Not yet used'
96+ WHEN idx_scan < 100 THEN ' Low usage'
97+ ELSE ' Active'
98+ END as status
99+ FROM pg_stat_user_indexes
100+ WHERE pg_stat_user_indexes .indexrelname LIKE ' %_canonical_optimized'
101+ OR pg_stat_user_indexes .indexrelname LIKE ' %_subquery_optimized'
102+ ORDER BY idx_scan DESC ;
103+
104+ SELECT * FROM address_transactions_performance;
105+
106+ -- Verify all indexes are valid and being used
107+ SELECT
108+ schemaname, relname, pg_stat_user_indexes .indexrelname , idx_scan,
109+ CASE
110+ WHEN idx_scan = 0 THEN ' INDEX NOT USED - INVESTIGATE'
111+ ELSE ' OK'
112+ END as health_status
113+ FROM pg_stat_user_indexes
114+ WHERE pg_stat_user_indexes .indexrelname LIKE ' %_optimized'
115+ ORDER BY idx_scan DESC ;
116+
117+ /*
118+ -- Rollback:
119+ DROP INDEX CONCURRENTLY IF EXISTS idx_stx_events_subquery_optimized;
120+ DROP INDEX CONCURRENTLY IF EXISTS idx_ft_events_subquery_optimized;
121+ DROP INDEX CONCURRENTLY IF EXISTS idx_nft_events_subquery_optimized;
122+ DROP INDEX CONCURRENTLY IF EXISTS idx_txs_canonical_optimized;
123+
124+ ANALYZE txs, stx_events, ft_events, nft_events;
125+
126+ DROP VIEW IF EXISTS address_transactions_performance;
127+ */
0 commit comments