You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while reviewing #335 (PR for the exception_stack_traces widening). Same defect class, different table. Not measured — filing rather than folding an unmeasured index into that PR, the same way #335 was split out of #323.
CREATEINDEXIF NOT EXISTS idx_ai_traces_project_conversation ON ai_traces(project_id, conversation_id);
It stops at the group column. 0020 (PR #334) widened idx_ai_traces_project_trace_name on the same table, so ai_traces looks covered — but that is a different group column. Nothing covers (project_id, conversation_id, recorded_at).
Why it matters
FindByConversationId (repositories/telemetry/sqlite/ai_trace.repository.go:615) ends at line 634 with:
WHERE project_id = :project_id AND conversation_id = :conversation_id
[AND recorded_at >= :from] [AND recorded_at <= :to]
ORDER BY recorded_at ASCLIMIT1000
The time range is optional — it comes from the request body (ai_trace.controller.go:292) and both bounds are skipped when zero.
This is the shape #335 measured. With no sqlite_stat1, SQLite takes idx_ai_traces_project_recorded to satisfy the ORDER BY and re-filters conversation_id per row. A conversation holds a handful of turns — far fewer than the 1000-row LIMIT — so the limit is never satisfied and the scan runs to the end of the range. With no time bound that is every ai_traces row the project has.
In #335 the equivalent read on exception_stack_traces (5 occurrences, 2M-row project, no stats) took 16-34 seconds. This one has a 1000-row limit rather than 20, so it has strictly less chance of exiting early.
What "done" looks like
Same widening, but measure first — that was #323's rule and #335 is the reason it exists (the predicted mechanism there was wrong in both directions):
DROPINDEX IF EXISTS idx_ai_traces_project_conversation;
CREATEINDEXIF NOT EXISTS idx_ai_traces_project_conversation_recorded ON ai_traces(project_id, conversation_id, recorded_at);
Seed with many conversations of realistic turn counts.
Also check conversation_id's other consumers (/api/ai-conversations/grouped semi-joins on conversation_id) — they may benefit or may be unaffected.
Note
app/migrations/telemetry_group_index_test.go guards this class. Its rows are {table, group, timeCol} and it iterates them independently, so ai_traces can carry a second row — add {"ai_traces", "conversation_id", "recorded_at"} when the index lands. It is deliberately absent today because the test would fail.
Found while reviewing #335 (PR for the
exception_stack_traceswidening). Same defect class, different table. Not measured — filing rather than folding an unmeasured index into that PR, the same way #335 was split out of #323.SQLite telemetry backend only.
The gap
0017_add_ai_conversation_columns.up.sql:6creates:It stops at the group column.
0020(PR #334) widenedidx_ai_traces_project_trace_nameon the same table, soai_traceslooks covered — but that is a different group column. Nothing covers(project_id, conversation_id, recorded_at).Why it matters
FindByConversationId(repositories/telemetry/sqlite/ai_trace.repository.go:615) ends at line 634 with:The time range is optional — it comes from the request body (
ai_trace.controller.go:292) and both bounds are skipped when zero.This is the shape #335 measured. With no
sqlite_stat1, SQLite takesidx_ai_traces_project_recordedto satisfy theORDER BYand re-filtersconversation_idper row. A conversation holds a handful of turns — far fewer than the 1000-rowLIMIT— so the limit is never satisfied and the scan runs to the end of the range. With no time bound that is everyai_tracesrow the project has.In #335 the equivalent read on
exception_stack_traces(5 occurrences, 2M-row project, no stats) took 16-34 seconds. This one has a 1000-row limit rather than 20, so it has strictly less chance of exiting early.What "done" looks like
Same widening, but measure first — that was #323's rule and #335 is the reason it exists (the predicted mechanism there was wrong in both directions):
EXPLAIN QUERY PLANbefore/after, both with and withoutANALYZE— exception_stack_traces per-hash reads sort the whole group: idx_exceptions_project_hash stops short of recorded_at #335 found the old index's pathology moves between the two states rather than disappearing, so one stats state is not evidence.Also check
conversation_id's other consumers (/api/ai-conversations/groupedsemi-joins onconversation_id) — they may benefit or may be unaffected.Note
app/migrations/telemetry_group_index_test.goguards this class. Its rows are{table, group, timeCol}and it iterates them independently, soai_tracescan carry a second row — add{"ai_traces", "conversation_id", "recorded_at"}when the index lands. It is deliberately absent today because the test would fail.