Faster Dashboard Queries: New Index on pipelineRuns(projectId, startedAt)
Faster Dashboard Queries: New Index on pipelineRuns(projectId, startedAt)
Version: 1.0.26 · Category: Performance / Database
Overview
v1.0.26 introduces a targeted database schema improvement that dramatically speeds up pipeline list queries for projects with large run histories. If you're running thousands of pipeline runs per project, you'll notice significantly snappier load times on the pipeline dashboard and any paginated pipeline list view.
The Problem
The pipeline.list router executes a query of this shape:
SELECT *
FROM pipeline_runs
WHERE project_id = ANY($ids)
ORDER BY started_at DESC
LIMIT 50;
Before this release, the schema only had a composite index on (projectId, status) — idx_runs_project_status. While useful for filtering by status, this index could not satisfy the ORDER BY started_at DESC clause directly. The database engine was forced to:
- Perform an index scan using
idx_runs_project_statusto find matching rows. - Load those rows.
- Perform a separate, in-memory sort on
started_at. - Only then apply the
LIMIT 50.
For small datasets this is imperceptible. For projects accumulating tens of thousands of runs, step 3 becomes a blocking, memory-intensive operation that grows with the size of the result set — contributing 50–200 ms of avoidable latency per query.
The Fix
A new composite index idx_runs_project_started has been added to the pipelineRuns table in src/db/schema.ts:
// src/db/schema.ts
index('idx_runs_project_started').on(table.projectId, table.startedAt)
With (projectId, startedAt) co-located in the index, the database can now:
- Seek directly to the matching
projectIdpartition of the index. - Traverse entries in descending
startedAtorder — no separate sort step required. - Stop after reading 50 rows (
LIMIT 50).
This collapses a multi-step scan-sort-limit pipeline into a single efficient index range scan.
Performance Impact
| Metric | Before | After |
|---|---|---|
| Query time (10,000+ runs / project) | 50–200 ms | < 5 ms |
| Sort strategy | Post-scan in-memory sort | Index-order traversal (no sort) |
| Rows read before LIMIT | All matching rows | Exactly 50 |
What's Next: Partial Index for Active Pipelines
The active pipelines dashboard widget filters runs to only those with a non-terminal status. A partial index is being considered as a follow-up optimization:
CREATE INDEX idx_runs_project_started_active
ON pipeline_runs (project_id, started_at)
WHERE status NOT IN ('completed', 'failed');
This would create a much smaller, highly targeted index that covers only in-flight runs — the exact subset the active pipelines widget queries. As completed and failed runs accumulate over time, this index stays lean while the full idx_runs_project_started index handles historical/paginated queries.
Files Changed
src/db/schema.ts— Addedidx_runs_project_startedindex onpipelineRuns