From d6377871b34a999c1dd85671af59c108bf1d34e6 Mon Sep 17 00:00:00 2001 From: parsnips Date: Fri, 22 May 2026 16:03:44 -0700 Subject: [PATCH] perf(gsi): add index on (base_pk, base_sk_*) for cascade deletes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cascade-delete from base-table writes runs `DELETE FROM WHERE base_pk = $1 [AND base_sk_ = $n ...]` against every GSI table. Neither the PK constraint nor the existing ordering index can serve this lookup because both lead with `pk`, so PostgreSQL falls back to a Seq Scan. In a recent benchmark this missing index accounted for ~74% of total DB time. Create an index that mirrors the cascade-delete predicate at GSI table creation time. Column order matches the PK constraint minus the leading `pk`, so for a single-bytes base SK this produces `(base_pk, base_sk_b)`. The index is created unconditionally — HASH-only base tables still benefit from `(base_pk)` alone. Only applies to GSI tables created after this change; existing deployments need a backfill migration to pick up the index. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/storage-postgres/src/data/ddl.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/storage-postgres/src/data/ddl.rs b/crates/storage-postgres/src/data/ddl.rs index 45115cb9..2bd7bdef 100755 --- a/crates/storage-postgres/src/data/ddl.rs +++ b/crates/storage-postgres/src/data/ddl.rs @@ -229,6 +229,26 @@ impl PostgresEngine { .map_err(|e| StorageError::Internal(e.to_string()))?; } + // Index on base-table key columns to support efficient cascade deletes + // from `delete_index_row_multi`, whose predicate is + // `WHERE base_pk = $1 [AND base_sk_ = $n ...]`. The PK constraint + // and ordering index both lead with `pk`, so neither can serve this + // lookup — without this index PostgreSQL falls back to a Seq Scan. + let mut cascade_cols = vec!["base_pk".to_owned()]; + for (i, &(_, sk_type)) in base_sks.iter().enumerate() { + let col = if i == 0 { + format!("base_{}", sk_column(sk_type)) + } else { + format!("base_{}", sk_column_n(i, sk_type)) + }; + cascade_cols.push(col); + } + let cascade_idx = format!("CREATE INDEX ON {idx_table} ({})", cascade_cols.join(", ")); + sqlx::query(&cascade_idx) + .execute(&mut **tx) + .await + .map_err(|e| StorageError::Internal(e.to_string()))?; + Ok(()) }