All Docs
FeaturesSaaS FactoryUpdated February 19, 2026

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:

  1. Perform an index scan using idx_runs_project_status to find matching rows.
  2. Load those rows.
  3. Perform a separate, in-memory sort on started_at.
  4. 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:

  1. Seek directly to the matching projectId partition of the index.
  2. Traverse entries in descending startedAt order — no separate sort step required.
  3. 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

MetricBeforeAfter
Query time (10,000+ runs / project)50–200 ms< 5 ms
Sort strategyPost-scan in-memory sortIndex-order traversal (no sort)
Rows read before LIMITAll matching rowsExactly 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 — Added idx_runs_project_started index on pipelineRuns