From 704f1e82fcbdae12145c3905a7216eff94a8a382 Mon Sep 17 00:00:00 2001 From: Brandur Date: Sun, 23 Aug 2026 20:24:04 +0800 Subject: [PATCH] Fix SQLite filter used in River UI queue count pane I noticed while testing SQLite out in the River UI that it's fairly slow when you click on the "queues" tab. This turns out to be because there was an accidental divergence in one of the SQLite queries in which it was omitting a filter that was present in the Postgres version. `JobCountByQueueAndStateResult` is used only in the queue pane of River UI and filters to `available` and `running`. Although omitting this filter may use the same index, there tends to be a massive quantity of `completed` rows that have to be counted unless that state is filtered out, which is why SQLite specifically was slow. --- CHANGELOG.md | 1 + riverdriver/riverdrivertest/job_read.go | 28 +++++++++++++++++++ .../riversqlite/internal/dbsqlc/river_job.sql | 1 + .../internal/dbsqlc/river_job.sql.go | 1 + 4 files changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 263d126d..9abc27b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed periodic jobs advancing their durable next run time when job insertion fails. [PR #1359](https://github.com/riverqueue/river/pull/1359). +- Improved SQLite queue count performance on large job tables by limiting counts to available and running jobs so the existing state and queue index can be used. [PR #1360](https://github.com/riverqueue/river/pull/1360). ## [0.44.1] - 2026-08-21 diff --git a/riverdriver/riverdrivertest/job_read.go b/riverdriver/riverdrivertest/job_read.go index fd6ad6d1..f8fe4418 100644 --- a/riverdriver/riverdrivertest/job_read.go +++ b/riverdriver/riverdrivertest/job_read.go @@ -116,6 +116,34 @@ func exerciseJobRead[TTx any](ctx context.Context, t *testing.T, executorWithTx require.Equal(t, int64(1), countsByQueue[1].CountRunning) }) + t.Run("IgnoresJobsInOtherStates", func(t *testing.T) { + t.Parallel() + + exec, _ := setup(ctx, t) + + for _, state := range []rivertype.JobState{ + rivertype.JobStateCancelled, + rivertype.JobStateCompleted, + rivertype.JobStateDiscarded, + rivertype.JobStatePending, + rivertype.JobStateRetryable, + rivertype.JobStateScheduled, + } { + _ = testfactory.Job(ctx, t, exec, &testfactory.JobOpts{Queue: new("queue1"), State: new(state)}) + } + + countsByQueue, err := exec.JobCountByQueueAndState(ctx, &riverdriver.JobCountByQueueAndStateParams{ + QueueNames: []string{"queue1"}, + Schema: "", + }) + require.NoError(t, err) + + require.Len(t, countsByQueue, 1) + require.Equal(t, "queue1", countsByQueue[0].Queue) + require.Equal(t, int64(0), countsByQueue[0].CountAvailable) + require.Equal(t, int64(0), countsByQueue[0].CountRunning) + }) + t.Run("IncludesRequestedQueuesThatHaveNoJobs", func(t *testing.T) { t.Parallel() diff --git a/riverdriver/riversqlite/internal/dbsqlc/river_job.sql b/riverdriver/riversqlite/internal/dbsqlc/river_job.sql index 6f8bd9a4..dd284e7e 100644 --- a/riverdriver/riversqlite/internal/dbsqlc/river_job.sql +++ b/riverdriver/riversqlite/internal/dbsqlc/river_job.sql @@ -63,6 +63,7 @@ WITH queue_stats AS ( COUNT(CASE WHEN river_job.state = 'running' THEN 1 END) AS count_running FROM /* TEMPLATE: schema */river_job WHERE river_job.queue IN (sqlc.slice('queue_names')) + AND river_job.state IN ('available', 'running') GROUP BY river_job.queue ) diff --git a/riverdriver/riversqlite/internal/dbsqlc/river_job.sql.go b/riverdriver/riversqlite/internal/dbsqlc/river_job.sql.go index 61ef76b9..68701243 100644 --- a/riverdriver/riversqlite/internal/dbsqlc/river_job.sql.go +++ b/riverdriver/riversqlite/internal/dbsqlc/river_job.sql.go @@ -109,6 +109,7 @@ WITH queue_stats AS ( COUNT(CASE WHEN river_job.state = 'running' THEN 1 END) AS count_running FROM /* TEMPLATE: schema */river_job WHERE river_job.queue IN (/*SLICE:queue_names*/?) + AND river_job.state IN ('available', 'running') GROUP BY river_job.queue )