perf(coderd/database): sort AI Bridge sessions by denormalized last_prompt_at - #27900
Draft
jeremyruppel wants to merge 1 commit into
Draft
perf(coderd/database): sort AI Bridge sessions by denormalized last_prompt_at#27900jeremyruppel wants to merge 1 commit into
jeremyruppel wants to merge 1 commit into
Conversation
…rompt_at ListAIBridgeSessions sorted by COALESCE(MAX(prompt.created_at), MIN(started_at)) via a per-interception LEFT JOIN LATERAL into aibridge_user_prompts. That lateral ran for every matched interception across the whole filtered set before LIMIT/OFFSET, making the sessions page scale O(N) with interception/prompt counts (AIGOV-580). Denormalize the sort key onto aibridge_interceptions.last_prompt_at, maintained monotonically at prompt-insert time, and read it from the stored column instead of joining prompts. Ordering, offset pagination, total count, and last_active_at semantics are unchanged. - Add migration 000563 (column + backfill from aibridge_user_prompts). - Fold a NULL-safe GREATEST bump into InsertAIBridgeUserPrompt via a data-modifying CTE so the write stays atomic in one statement. - Drop the prompt lateral from ListAIBridgeSessions; cursor_pos and the keyset HAVING comparison read last_prompt_at. - Add TestAIBridgeInterceptionLastPromptAt (bump + monotonicity). This work was generated by Coder Agents.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
/ai-gateway/sessionspage takes 5-10s to load (AIGOV-580). Root cause is a regression from #24440 (AIGOV-208):ListAIBridgeSessionssorts by the runtime aggregateCOALESCE(MAX(prompt.created_at), MIN(started_at)), computed via a per-interceptionLEFT JOIN LATERALintoaibridge_user_prompts. That lateral runs for every matched interception across the whole filtered set beforeLIMIT/OFFSET, so the query scales O(N) with interception/prompt counts.This denormalizes the sort key onto
aibridge_interceptions.last_prompt_at, maintained monotonically at prompt-insert time, and reads it from the stored column instead of joining prompts. Ordering, offset pagination, total count, andlast_active_atsemantics are unchanged.Fixes AIGOV-580.
Changes
aibridge_interceptions.last_prompt_at timestamptz NULL, backfilled fromaibridge_user_prompts.InsertAIBridgeUserPrompt: fold a NULL-safeGREATEST(last_prompt_at, @created_at)bump into the insert via a data-modifying CTE, so the write stays atomic in a single statement.ListAIBridgeSessions: drop the per-interception prompt lateral;session_page,cursor_pos, and the keysetHAVINGcomparison now readlast_prompt_at.ListAuthorizedAIBridgeSessionThreadsfor the new embedded column.TestAIBridgeInterceptionLastPromptAt(cache bump + monotonicity).Implementation note
The plan's 3.2 sketch put the
INSERTin the CTE and the bump second. The final query flips that (bumpUPDATEin the CTE,INSERT ... RETURNING *as the final statement) so sqlc keeps the return type asAIBridgeUserPromptinstead of generating a new row type that would churn the store interface. Postgres runs data-modifying CTEs to completion regardless of reference, so behavior is identical.Testing
TestAIBridge*(enterprise/coderd), migration tests, and the new DB test pass against Postgres.make gen,make fmt,golangci-lint ./coderd/database/..., and the emdash lint are clean.Follow-up
Restores most of the regression by removing the whole-set prompt lateral. The sort key is still a per-session aggregate (not indexable), so true keyset early-termination is intentionally deferred (see plan 3.4). Recommend confirming with
EXPLAIN ANALYZEon a large seed before deciding whether phase 2 is needed; this PR has not been benchmarked against a large dataset.Implementation plan & decision log
Brainstorm: restore
ListAIBridgeSessionsperformance without sacrificing paginationRegression source: PR #24440 (AIGOV-208).
Goal: sort AI Bridge sessions by last activity (last prompt time) and keep the current numbered/offset pagination + total count, while making the query scale to large interception/prompt row counts.
Constraints chosen by the user:
PaginationContainer. Not switching to pure keyset.1. Root cause (verified in the tree)
ListAIBridgeSessionsorders by a runtime aggregateCOALESCE(MAX(latest_prompt.latest_prompt_at), MIN(ai.started_at))via aLEFT JOIN LATERALoveraibridge_user_prompts, per interception. Pre-#24440 it ordered byMIN(ai.started_at)(a stored column).Why this hurts at scale:
session_pageCTE beforeLIMIT/OFFSET, so Postgres probesaibridge_user_promptsfor every matched interception in the entire filter window, then aggregates, sorts, and slices the page. Dominant new cost.last_active_atis computed per group, so the planner must build and sort every group before honoringOFFSET/LIMIT. Theafter_session_idkeyset path does not help either: itsHAVINGcompares the same aggregate, so all groups are still materialized.Supporting facts: no index covers a per-session "last activity" value;
session_idis a stored generated column so a session is not a first-class row;CountAIBridgeSessionsis independent of the sort key; prompts are written fromServer.RecordPromptUsage, the natural place to maintain a denormalized timestamp.2. Reframing the tradeoff
"keyset vs numbered pagination" is a false dichotomy. The real blocker is that the sort key is not materialized. Once it derives from stored columns: offset pagination gets cheaper, total count is already independent, and a future keyset path becomes possible.
3. Recommended direction
Denormalize a per-interception activity timestamp and drop the prompt lateral.
3.1 Schema
Add
last_prompt_at timestamptz NULLtoaibridge_interceptions= maxcreated_atof that interception's prompts (NULL when none). Backfill in the migration. Follow DATABASE.md (migration pair,make gen, audit table if needed).3.2 Write-path maintenance (app write, no triggers)
Fold the bump into the prompt insert as a single CTE (
GREATESTis NULL-safe and monotonic; retries/out-of-order records never regress the value). Rejected: two store calls wrapped inInTx(extra transaction + round trip for a cache write inseparable from the insert).3.3 Query rewrite
Session sort key becomes
COALESCE(MAX(ai.last_prompt_at), MIN(ai.started_at)).session_pageis a singleGROUP BYover interceptions on stored columns;cursor_posand the keysetHAVINGuse the same expression. Page-only lateral joins andORDER BYunchanged.3.4 Optional follow-up (deferred): true early termination
Decision (7.2): tabled. If deep pages remain slow, add an
activity_atcolumn + index and a recursive-CTE skip scan that emits the max-activity row per session in global order and stops afteroffset+limit. Phase 2, gated on measurement.4. Correctness considerations
Fallback: prompt-less sessions fall back to
MIN(started_at)(only when every interception haslast_prompt_at IS NULL). Multiple prompts per interception handled by the max. Monotonic viaGREATEST.ended_at IS NOT NULLfilter stays. Backfill must match the query aggregate. Audit table entry not required (not audited).5. Alternatives considered
6. Validation plan
Large synthetic seed;
EXPLAIN (ANALYZE, BUFFERS)before/after for first page, deep offset, filtered, time-window, and keyset paths; golden comparison of ordering and count; Go tests for ordering/fallback/multi-prompt/prompt-less/pagination;make gen/fmt/lint/test.7. Decisions
last_prompt_atonly; computeactivity_atinline later if needed.This pull request was generated by Coder Agents.