diff --git a/crates/iota-graphql-rpc/src/backward_view/consistent.rs b/crates/iota-graphql-rpc/src/backward_view/consistent.rs index 6f86ade032a5..82589131b29c 100644 --- a/crates/iota-graphql-rpc/src/backward_view/consistent.rs +++ b/crates/iota-graphql-rpc/src/backward_view/consistent.rs @@ -33,8 +33,17 @@ pub(crate) fn query( /// Returns active objects from `checkpointed_objects` that were consistent /// also at the given checkpoint. /// -/// Uses a LEFT JOIN against `objects_backward_history` to exclude objects -/// that have any entry with `superseded_at_checkpoint > checkpoint_viewed_at`. +/// Uses a NOT EXISTS subquery against `objects_backward_history` to exclude +/// objects that have any entry with +/// `superseded_at_checkpoint > checkpoint_viewed_at`. +/// +/// # Implementation notes +/// +/// NOT EXISTS lets Postgres answer "did this object change?" row by row, +/// with one index lookup each. A LEFT JOIN on a `SELECT DISTINCT` subquery +/// takes that option away: the full list of changed objects must always be +/// built first, and in the worst plans it is then also scanned for every +/// row. fn consistent_checkpointed_objects( checkpoint_viewed_at: i64, page: &Page, @@ -47,17 +56,19 @@ fn consistent_checkpointed_objects( format!("object_status = {ACTIVE}") ); - let changed_subquery = query!(format!( - "SELECT DISTINCT object_id FROM objects_backward_history \ - WHERE superseded_at_checkpoint > {checkpoint_viewed_at}" - )); let mut source = query!( - r#"SELECT candidates.* FROM ({}) candidates - LEFT JOIN ({}) changed ON candidates.object_id = changed.object_id"#, - checkpointed_filtered, - changed_subquery + "SELECT candidates.* FROM ({}) candidates", + checkpointed_filtered + ); + source = filter!( + source, + format!( + "NOT EXISTS (\ + SELECT 1 FROM objects_backward_history changed \ + WHERE changed.object_id = candidates.object_id \ + AND changed.superseded_at_checkpoint > {checkpoint_viewed_at})" + ) ); - source = filter!(source, "changed.object_id IS NULL"); page.apply::(source) } diff --git a/crates/iota-graphql-rpc/src/types/balance.rs b/crates/iota-graphql-rpc/src/types/balance.rs index 7495d5141949..d4d96e8523fb 100644 --- a/crates/iota-graphql-rpc/src/types/balance.rs +++ b/crates/iota-graphql-rpc/src/types/balance.rs @@ -250,19 +250,20 @@ fn balance_query( address, coin_type.clone(), ); - let changed = query!(format!( - "SELECT DISTINCT object_id FROM objects_backward_history \ - WHERE superseded_at_checkpoint > {checkpoint_viewed_at}" - )); + // NOT EXISTS instead of a JOIN; see `consistent_checkpointed_objects` + // for explanation. let source_a = filter!( query!( - r#"SELECT candidates.object_id, candidates.coin_balance, candidates.coin_type - FROM ({}) candidates - LEFT JOIN ({}) changed ON candidates.object_id = changed.object_id"#, - checkpointed, - changed + "SELECT candidates.object_id, candidates.coin_balance, candidates.coin_type \ + FROM ({}) candidates", + checkpointed ), - "changed.object_id IS NULL" + format!( + "NOT EXISTS (\ + SELECT 1 FROM objects_backward_history changed \ + WHERE changed.object_id = candidates.object_id \ + AND changed.superseded_at_checkpoint > {checkpoint_viewed_at})" + ) ); let mut history = filter( diff --git a/crates/iota-indexer/migrations/pg/2026-08-12-120000_backward_view_type_statistics/down.sql b/crates/iota-indexer/migrations/pg/2026-08-12-120000_backward_view_type_statistics/down.sql new file mode 100644 index 000000000000..a33d77d84498 --- /dev/null +++ b/crates/iota-indexer/migrations/pg/2026-08-12-120000_backward_view_type_statistics/down.sql @@ -0,0 +1,6 @@ +-- Restore the default extended-statistics target. +ALTER STATISTICS checkpointed_objects_type_stats SET STATISTICS -1; +ALTER STATISTICS objects_backward_history_type_stats SET STATISTICS -1; + +ANALYZE checkpointed_objects (object_type, object_type_package, object_type_module, object_type_name); +ANALYZE objects_backward_history (object_type, object_type_package, object_type_module, object_type_name); diff --git a/crates/iota-indexer/migrations/pg/2026-08-12-120000_backward_view_type_statistics/up.sql b/crates/iota-indexer/migrations/pg/2026-08-12-120000_backward_view_type_statistics/up.sql new file mode 100644 index 000000000000..05d9e3f4374d --- /dev/null +++ b/crates/iota-indexer/migrations/pg/2026-08-12-120000_backward_view_type_statistics/up.sql @@ -0,0 +1,11 @@ +-- Raise the extended-statistics target on the backward-diff tables so all +-- distinct object types fit in the multi-column MCV list; types outside the +-- list get a rows=1 estimate, leading to plans that time out. +ALTER STATISTICS checkpointed_objects_type_stats SET STATISTICS 2500; +ALTER STATISTICS objects_backward_history_type_stats SET STATISTICS 2500; + +-- Rebuild the statistics objects modified above with their new target. +-- ANALYZE rebuilds them only if all their columns are listed; listing just +-- those columns keeps the ANALYZE cheap. +ANALYZE checkpointed_objects (object_type, object_type_package, object_type_module, object_type_name); +ANALYZE objects_backward_history (object_type, object_type_package, object_type_module, object_type_name);