Page an item list from an index, then read only its rows (#188)
All Subscriptions took 130-170 ms when used a minute before, and 0.5-1.9 s the first time after a quiet spell. Its sort needs each item's date, which was only in the item's row, so every one of a person's 13,700 rows was read, show notes inline and all, and cold that came off disk; count_in, which needs no date, took 20 ms throughout. entries_by_time holds the keys and dates, the page's keys are sorted and cut from it, and only the rows shown are read. On a copy of the shape in the test database: 619 pages read against 4,174, with no visit to the table. Neither change alone helped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
21
src/db.rs
21
src/db.rs
@@ -73,6 +73,10 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
||||
// (#165). Also what the healthcheck's count and the Downloaded filter look for.
|
||||
"CREATE INDEX IF NOT EXISTS enclosures_on_disk ON enclosures (feed_id, guid) WHERE path IS NOT NULL",
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS users_name_lower ON users (lower(name))",
|
||||
// Every key and date an item list sorts by, so `entries_where` pages from the index alone
|
||||
// and visits the table only for the rows it shows. Sorting All Subscriptions from the
|
||||
// table read all 13,700 of its rows, show notes and all, half a second to two cold (#188).
|
||||
"CREATE INDEX IF NOT EXISTS entries_by_time ON entries (feed_id, guid, published, first_seen)",
|
||||
] {
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
}
|
||||
@@ -1258,16 +1262,19 @@ impl Db {
|
||||
) -> Result<Vec<EntryRow>> {
|
||||
let mut a = Args::default();
|
||||
let from = entries_from(&mut a, user_id, feed_id, filter, search, guid);
|
||||
// The page's keys first, then its rows: the sort then needs only `entries_by_time`, not
|
||||
// every item's row, and the show notes are read for the page alone (#188).
|
||||
let (limit, offset) = (a.p(limit), a.p(offset));
|
||||
let user = a.p(user_id);
|
||||
let sql = format!(
|
||||
"SELECT e.guid, e.feed_id, e.title, e.link, e.published, e.description,
|
||||
coalesce(s.read, false) AS read, coalesce(s.flagged, false) AS flagged, e.image,
|
||||
coalesce(s.duration, e.duration) AS duration,
|
||||
e.episode, e.season, coalesce(s.position, 0) AS position
|
||||
{from}
|
||||
ORDER BY {order}
|
||||
LIMIT {} OFFSET {}",
|
||||
a.p(limit),
|
||||
a.p(offset)
|
||||
FROM (SELECT e.feed_id, e.guid {from} ORDER BY {order} LIMIT {limit} OFFSET {offset}) p
|
||||
JOIN entries e ON e.feed_id = p.feed_id AND e.guid = p.guid
|
||||
LEFT JOIN entry_state s ON s.user_id = {user} AND s.feed_id = e.feed_id AND s.guid = e.guid
|
||||
ORDER BY {order}"
|
||||
);
|
||||
let mut rows = self
|
||||
.rows(&sql, a.0)
|
||||
@@ -2631,6 +2638,10 @@ mod tests {
|
||||
assert_eq!(order("type", "asc").await.first().map(String::as_str), Some("n"));
|
||||
db.exec_for_test("DELETE FROM entries WHERE guid = 'n';").await.unwrap();
|
||||
assert_eq!(order("published", "desc").await, ["c", "b", "a"]);
|
||||
// A page from the middle: the keys are paged before the rows are read (#188).
|
||||
let page: Vec<String> = db.entries_in(1, None, Filter::All, None, 1, 1, &order_sql("published", "desc")).await
|
||||
.unwrap().into_iter().map(|e| e.guid).collect();
|
||||
assert_eq!(page, ["b"]);
|
||||
db.set_entry_flag(1, "f", "a", EntryFlag::Flagged, true).await.unwrap();
|
||||
assert_eq!(order("kept", "desc").await[0], "a");
|
||||
// A pin does not lift an item above the others in any other sort (#36).
|
||||
|
||||
Reference in New Issue
Block a user