Skip to content

Index the columns 'llm logs' looks rows up by, use 'union all' for the latest-conversation lookup - #1657

Open
rdslw wants to merge 1 commit into
simonw:mainfrom
rdslw:perf-log-lookup-indexes
Open

rdslw wants to merge 1 commit into
simonw:mainfrom
rdslw:perf-log-lookup-indexes

Conversation

@rdslw

@rdslw rdslw commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

EXPLAIN QUERY PLAN over the queries llm logs runs shows five lookups that scan a whole table. Fine at hundreds of rows, painful as the store grows. This adds one migration with five indexes and a one-word query change:

  • parts(type, tool_name, message_hash) - the --tools / -T filters resolve "messages with a tool_result part" by scanning parts, the fastest-growing table, and building a transient automatic index on every query.
  • tool_calls, tool_results, tool_responses on response_id - the legacy extras and the legacy --tools filter probe these per listed response through correlated subqueries.
  • responses(conversation_id) - legacy --cid filtering; turns.thread_id got its index in m023, this column never did.
  • load_conversation() for prompt -c used a distinct union of threads and conversations, which materializes every id before taking one row. union all lets SQLite stop at the first row of each primary key; the outer limit 1 already discards any dual-write duplicate.

Measured on a copy of my logs.db (442 legacy responses, 359 turns, 1202 parts) and the same database replicated 50x (22k responses, 18k turns, 60k parts). Query timings, median of 15 runs:

Query real, before real, after 50x, before 50x, after
legacy extras for 20 rows (tool_calls + tool_results subqueries) 0.32 ms 0.09 ms 38.6 ms 0.10 ms
legacy --tools filter 0.13 ms 0.08 ms 4.3 ms 0.18 ms
legacy --cid filter 1.90 ms 0.03 ms 108.8 ms 0.03 ms
--tools filter (parts) 6.4 ms 0.4 ms 12 695 ms 27 ms
-T name filter (parts) 3.8 ms 0.2 ms 5 427 ms 11 ms
latest conversation for prompt -c (12k threads) - - 36 ms 0.1 ms

End to end at 50x, hyperfine mean of 3 runs (about 2 s of each is interpreter and plugin startup):

Command before after
llm logs list --tools -n 5 14.4 s 2.2 s
llm logs list -T Brave_context -n 5 42.1 s 2.2 s
llm logs list --cid ID -n 5 2.2 s 2.1 s

The five indexes add about 0.6% to the file size. If you prefer a smaller footprint on parts, a partial index on parts (tool_name, message_hash) where type = 'tool_result' serves the same query; happy to switch.

Created with help of Fable-5.1 during migration of llm-openai-codex from llm-0.32 to llm-0.33.

…ry conversation id for 'prompt -c'

EXPLAIN QUERY PLAN over the queries llm logs issues showed five scans
that grow linearly with the store.

m028_log_lookup_indexes:
- parts(type, tool_name, message_hash): the --tools/-T filters resolve
  "messages carrying a tool_result part (of this name)". That was a
  full scan of parts - the fastest-growing table - plus a transient
  automatic index SQLite built on every query. The trailing
  message_hash makes the index covering for exactly that subquery.
- tool_calls/tool_results/tool_responses(response_id): the legacy
  extras and legacy --tools filter read these only through correlated
  subqueries on response_id - one full scan per listed response, and
  tool_results rows carry the tool outputs. tool_responses' primary
  key starts with tool_id, so it could not serve the probe either.
- responses(conversation_id): --cid on the legacy side was a full
  responses scan; turns got idx_turns_thread_id in m023 but the
  legacy column never got the equivalent.

load_conversation(): the latest-conversation lookup for 'prompt -c'
used a distinct union of threads and conversations, which materializes
every id from both tables before sorting for one row. 'union all' lets
SQLite stop at the first row of each primary key; the outer limit 1
already discards any dual-write duplicate.

tests/test_migrate.py asserts migrate() leaves all five indexes in
place.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant