perf(storage): composite (profile_id, created_at DESC) index on transcripts
Migration v15 adds a composite index covering the dominant transcripts
query path:
SELECT ... FROM transcripts
WHERE profile_id = ? ORDER BY created_at DESC LIMIT ?
Previously SQLite had to choose between idx_transcripts_profile_id
(filter by profile, then in-memory sort by date) and idx_transcripts_created
(scan dates and filter on profile). Both work fine at hundreds of rows
and degrade past a few thousand.
`migration_v15_creates_profile_created_index` asserts (a) the index exists
and (b) `EXPLAIN QUERY PLAN` shows the planner picks it for the canonical
profile-scoped, date-ordered list query.
Test count assertions in `test_migrations_run_on_empty_db` and
`test_migrations_idempotent` bumped 14 → 15.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
This commit is contained in:
@@ -463,6 +463,20 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[
|
|||||||
ALTER TABLE transcripts ADD COLUMN llm_tags TEXT NOT NULL DEFAULT '';
|
ALTER TABLE transcripts ADD COLUMN llm_tags TEXT NOT NULL DEFAULT '';
|
||||||
"#,
|
"#,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
15,
|
||||||
|
"transcripts: composite (profile_id, created_at DESC) index",
|
||||||
|
r#"
|
||||||
|
-- Performance index for the dominant transcripts query path:
|
||||||
|
-- WHERE profile_id = ? ORDER BY created_at DESC LIMIT ?
|
||||||
|
-- The standalone idx_transcripts_profile_id and idx_transcripts_created
|
||||||
|
-- forced SQLite to either filter by profile then sort, or scan the date
|
||||||
|
-- index and filter — fine at hundreds of rows, painful past a few thousand.
|
||||||
|
-- A composite index covers both predicates in one ordered seek.
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transcripts_profile_created
|
||||||
|
ON transcripts(profile_id, created_at DESC);
|
||||||
|
"#,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Split SQL into individual statements, respecting BEGIN...END trigger blocks.
|
/// Split SQL into individual statements, respecting BEGIN...END trigger blocks.
|
||||||
@@ -612,7 +626,7 @@ mod tests {
|
|||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count, 14);
|
assert_eq!(count, 15);
|
||||||
|
|
||||||
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
@@ -631,7 +645,7 @@ mod tests {
|
|||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count, 14);
|
assert_eq!(count, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1124,4 +1138,48 @@ mod tests {
|
|||||||
.expect("query");
|
.expect("query");
|
||||||
assert_eq!(auto, 0, "pre-existing completed rows must default to 0");
|
assert_eq!(auto, 0, "pre-existing completed rows must default to 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn migration_v15_creates_profile_created_index() {
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.expect("pool");
|
||||||
|
run_migrations(&pool).await.expect("migrate");
|
||||||
|
|
||||||
|
// Index exists by name.
|
||||||
|
let names: Vec<String> = sqlx::query_scalar(
|
||||||
|
"SELECT name FROM sqlite_master \
|
||||||
|
WHERE type = 'index' AND tbl_name = 'transcripts'",
|
||||||
|
)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.expect("read indexes");
|
||||||
|
assert!(
|
||||||
|
names.iter().any(|n| n == "idx_transcripts_profile_created"),
|
||||||
|
"expected composite (profile_id, created_at) index, got {names:?}",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query planner picks the composite index for the dominant
|
||||||
|
// profile-scoped, date-ordered list query. EXPLAIN QUERY PLAN
|
||||||
|
// returns (id, parent, notused, detail) — we want detail.
|
||||||
|
let plan_rows = sqlx::query(
|
||||||
|
"EXPLAIN QUERY PLAN \
|
||||||
|
SELECT id FROM transcripts \
|
||||||
|
WHERE profile_id = ? ORDER BY created_at DESC LIMIT 50",
|
||||||
|
)
|
||||||
|
.bind(crate::DEFAULT_PROFILE_ID)
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.expect("explain");
|
||||||
|
let plan: Vec<String> = plan_rows
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.get::<String, _>("detail"))
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
plan.iter().any(|row| row.contains("idx_transcripts_profile_created")),
|
||||||
|
"planner should use the composite index, got plan: {plan:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user