Improve the index for latest_channel_message on p_duo_workflows_checkpoint_blobs
Problem
Merge request !251388 (merged) changed Ai::DuoWorkflows::Workflow#latest_channel_message in ee/app/models/ai/duo_workflows/workflow.rb.
During database review, @l.rosa approved the change but flagged the index. Their comment:
The index will cover only
workflow_id, everything else is applied as a filter, so the cost grows with the workflow's checkpoint count. Fine for now withLIMIT 1, but we should consider a better index in a follow-up.
The query needs an index that limits the scan and the sort, not one that scales with the whole session.
Evidence
The query reads the newest checkpoint blob for one channel of one session.
SELECT "p_duo_workflows_checkpoint_blobs".*
FROM "p_duo_workflows_checkpoint_blobs"
WHERE "workflow_id" = 6405842
AND "workflow_created_at" >= '2026-08-17 09:04:38+00'
AND "workflow_created_at" < '2026-08-17 09:04:39+00'
AND "project_id" = 39903947
AND "thread_ts" IN (<172 values, one per checkpoint in the session>)
AND "channel" = 'ui_chat_log'
ORDER BY "id" DESC
LIMIT 1Production plan from Database Lab (28M rows in the table, about 1M rows per daily partition): https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/55275/commands/158801
Plan numbers:
- The planner uses
p_duo_workflows_checkpoint_blobs_20260817_workflow_id_idx, which coversworkflow_idalone. Index Condis onlyworkflow_id = 6405842.workflow_created_at,project_id,channel, and thethread_tslist are all applied as aFilter.- The index scan returns 122 rows and removes 434 by filter, so it visits 556 rows to return 1.
- Those rows go through a top-N heapsort on
id DESCbeforeLIMIT 1. - Buffers: shared hit=6, read=202. I/O read time: 113.223 ms. Total execution time: 116.362 ms.
- The session has 172 checkpoints, which is a normal length, not an outlier.
Rows visited scale with the session's total blob count (checkpoints times channels). A long session costs proportionally more. This read path sits behind the dw_read_blobs_graphql feature flag, not yet enabled on GitLab.com, so there's no user impact today. It needs a fix before that flag rolls out at scale.
Existing indexes
On p_duo_workflows_checkpoint_blobs:
idx_duo_wf_checkpoint_blobs_dedup, unique, on(project_id, workflow_id, thread_ts, channel, version, step_action, workflow_created_at), withNULLS NOT DISTINCT.index_duo_wf_checkpoint_blobs_on_workflow_idon(workflow_id).index_duo_wf_checkpoint_blobs_on_namespace_idon(namespace_id).
Neither of the first two can serve ORDER BY id DESC, so the current query always sorts.
Direction
This is a candidate to measure, not a decided design. One option: an index leading with workflow_id and channel and ending in id, for example (workflow_id, channel, id).
That shape would restrict the scan to one channel's rows and let the planner walk id backward, stopping at the first row whose thread_ts is in the list, instead of sorting the whole result set.
Notes for whoever picks this up:
thread_tsis matched with anINlist, so adding it to the index doesn't help the ordering.- The partition key is constant within a partition, so it doesn't need to be in the index.
- Check GitLab's conventions for sharding key placement in indexes on partitioned tables before finalizing the column order.
- Confirm the choice against Database Lab. Don't assume the shape works without a real plan.
Any new index should serve the whole read family on this table, not just this query. Other callers in the same file: #accumulated_blobs_for, #history_blobs_for, #full_history_blobs, and #chat_log_thread_ts. They filter on combinations of project_id, workflow_id, current_thread, thread_ts, and channel, and most order by id.
Related concern: IN list growth
A second, separate concern from the same reviewer on the same query: the thread_ts IN (...) list grows with the session's checkpoint count. PostgreSQL 18 CI validation confirmed that IN lists over roughly 624 parameters can flip plans to sequential scans.
This query's LIMIT 1 and lack of joins make it less exposed than most queries with this pattern. Still, a fix should consider bounding the list size.
See https://docs.gitlab.com/development/database/lateral_planner_fence/ for the LATERAL join rewrite that addresses this class of problem.
Acceptance criteria
-
latest_channel_messageno longer visits a number of rows proportional to the whole session's blob count. - The
ORDER BY id DESCsort is removed or bounded to a small, fixed set of rows. - A Database Lab plan is attached showing before and after execution for this query.
- The other readers on
p_duo_workflows_checkpoint_blobs(#accumulated_blobs_for,#history_blobs_for,#full_history_blobs,#chat_log_thread_ts) are checked against the new index for regressions. - A decision is recorded on whether to bound the
thread_ts IN (...)list, and why.
Follow-up for !251388 (comment 3727948213).