From d4673c2e91168dcdb0aca9d6c039df0d9c52bb28 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sat, 28 Feb 2026 18:18:34 +0000 Subject: [PATCH] fix: add partial index for fast failure filtering on runs page (#8150) When failures are sparse (<1%), filtering by failure status on the runs page required scanning millions of success rows. Add a partial index on v2_job_completed (workspace_id, completed_at DESC) WHERE status IN ('failure', 'canceled') and switch ORDER BY to completed_at when filtering failures, so Postgres walks the small partial index directly. Benchmarked at 5.2M rows / 1% failure rate: - LIMIT 30: 800ms -> 0.4ms (2000x faster) - LIMIT 1000: 550ms -> 21ms (26x faster) Co-authored-by: Claude Opus 4.6 --- .../20260228000000_v2_job_completed_failure_index.down.sql | 1 + .../20260228000000_v2_job_completed_failure_index.up.sql | 7 +++++++ backend/windmill-api-jobs/src/query.rs | 5 ++++- backend/windmill-api/src/db.rs | 3 +++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/20260228000000_v2_job_completed_failure_index.down.sql create mode 100644 backend/migrations/20260228000000_v2_job_completed_failure_index.up.sql diff --git a/backend/migrations/20260228000000_v2_job_completed_failure_index.down.sql b/backend/migrations/20260228000000_v2_job_completed_failure_index.down.sql new file mode 100644 index 0000000000..ad1c0922f9 --- /dev/null +++ b/backend/migrations/20260228000000_v2_job_completed_failure_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS ix_v2_job_completed_failure_workspace; diff --git a/backend/migrations/20260228000000_v2_job_completed_failure_index.up.sql b/backend/migrations/20260228000000_v2_job_completed_failure_index.up.sql new file mode 100644 index 0000000000..1e669f0a33 --- /dev/null +++ b/backend/migrations/20260228000000_v2_job_completed_failure_index.up.sql @@ -0,0 +1,7 @@ +-- Partial index for fast failure/canceled filtering on the runs page. +-- When failures are sparse (<1%) this avoids scanning millions of successful jobs. +-- The query orders by completed_at DESC (switched from created_at when success=false), +-- so this index provides both filtering and ordering in a single scan. +CREATE INDEX IF NOT EXISTS ix_v2_job_completed_failure_workspace + ON v2_job_completed (workspace_id, completed_at DESC) + WHERE status IN ('failure', 'canceled'); diff --git a/backend/windmill-api-jobs/src/query.rs b/backend/windmill-api-jobs/src/query.rs index 6be9487191..d8128d90e7 100644 --- a/backend/windmill-api-jobs/src/query.rs +++ b/backend/windmill-api-jobs/src/query.rs @@ -556,7 +556,10 @@ pub fn list_completed_jobs_query( let mut sqlb = SqlBuilder::select_from("v2_job_completed") .fields(fields) .order_by( - if lq.completed_before.is_some() || lq.completed_after.is_some() { + if lq.completed_before.is_some() + || lq.completed_after.is_some() + || lq.success == Some(false) + { "v2_job_completed.completed_at" } else { "v2_job.created_at" diff --git a/backend/windmill-api/src/db.rs b/backend/windmill-api/src/db.rs index 1c4b15fea8..8b0fb44dfe 100644 --- a/backend/windmill-api/src/db.rs +++ b/backend/windmill-api/src/db.rs @@ -81,6 +81,9 @@ lazy_static::lazy_static! { (20260225100000, include_str!( "../../migrations/20260225100000_asset_covering_index.up.sql" ).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY").replace("DROP INDEX", "DROP INDEX CONCURRENTLY")), + (20260228000000, include_str!( + "../../migrations/20260228000000_v2_job_completed_failure_index.up.sql" + ).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY")), ].into_iter().collect(); }