perf(state): add messages(session_id, id) index for window/ordering queries
Every ORDER BY id query on the messages table sorted or scanned the whole session: get_messages_around's window seek, latest_message_row_id (LIMIT 1), and get_messages' full-load ordering all paid O(session history) per call — hot mid-turn via session_search and reactions. messages.id is an original column (INTEGER PRIMARY KEY AUTOINCREMENT), so the index lives in SCHEMA_SQL next to idx_messages_session — no legacy-column migration hazard (the kanban lesson from #28776 does not apply). Measured (real schema, one 20k-message session, median of 30): get_messages_around 7.08 -> 0.22 ms (32x), latest_message_row_id 3.37 -> 0.011 ms (307x), get_messages full load 111.6 -> 98.6 ms (1.13x — remaining cost is row deserialization, not the sort). Window results byte-identical at probe points across the session. Tests: VM-step pin (get_messages_around bounded work, calibrated ~12 vs ~855 handler calls, threshold 300 — fails without the index) and window parity with/without the index. No EXPLAIN/plan text (behavior contracts, AGENTS.md).
This commit is contained in:
@@ -283,6 +283,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_source_id ON sessions(source, id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id, id);
|
||||
CREATE INDEX IF NOT EXISTS idx_compression_locks_expires ON compression_locks(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_model_usage_session ON session_model_usage(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_model_usage_model ON session_model_usage(model);
|
||||
|
||||
@@ -2698,6 +2698,48 @@ class TestGetMessagesPagination:
|
||||
messages = db.get_messages("s1")
|
||||
assert [m["content"] for m in messages] == [f"msg-{i}" for i in range(10)]
|
||||
|
||||
|
||||
def test_window_query_bounded_work(self, db):
|
||||
"""Perf contract: get_messages_around must seek by index, not scan
|
||||
the session's whole message history. Measured behaviorally via
|
||||
SQLite progress-handler steps (behavior contracts over snapshots,
|
||||
AGENTS.md — no EXPLAIN text). Calibrated on this seed (3000
|
||||
messages): indexed = ~12 handler calls, unindexed full-session
|
||||
scan = ~855. Threshold 300: >25x headroom above the indexed path,
|
||||
~3x below the scan. Same pattern as the loader call-count pins in
|
||||
tests/tools/test_approval_config_readonly.py."""
|
||||
self._seed(db, n=3000)
|
||||
mid = db.get_messages("s1", limit=1, offset=1500)[0]["id"]
|
||||
steps = [0]
|
||||
|
||||
def progress():
|
||||
steps[0] += 1
|
||||
return 0
|
||||
|
||||
db._conn.set_progress_handler(progress, 100)
|
||||
try:
|
||||
db.get_messages_around("s1", mid, window=20)
|
||||
finally:
|
||||
db._conn.set_progress_handler(None, 0)
|
||||
assert steps[0] < 300, (
|
||||
f"get_messages_around executed {steps[0]}x100 VM steps — the "
|
||||
"session-history scan is back (idx_messages_session_id missing "
|
||||
"or unused)")
|
||||
|
||||
|
||||
def test_window_results_identical_with_and_without_index(self, db):
|
||||
"""The index must not change results: identical windows at probe
|
||||
points across the session, with and without it."""
|
||||
self._seed(db, n=500)
|
||||
ids = [m["id"] for m in db.get_messages("s1")]
|
||||
probes = (ids[0], ids[len(ids) // 2], ids[-1])
|
||||
with_index = [db.get_messages_around("s1", mid, window=5)
|
||||
for mid in probes]
|
||||
db._conn.execute("DROP INDEX idx_messages_session_id")
|
||||
without_index = [db.get_messages_around("s1", mid, window=5)
|
||||
for mid in probes]
|
||||
assert with_index == without_index
|
||||
|
||||
def test_limit_pages_in_insertion_order(self, db):
|
||||
self._seed(db)
|
||||
page1 = db.get_messages("s1", limit=4, offset=0)
|
||||
|
||||
@@ -110,6 +110,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id, id);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -90,6 +90,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id, id);
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
Reference in New Issue
Block a user