Count a Signal's items and unread in one pass (#348)

signal_rows ran the same correlated subquery over signal_verdicts,
entries, subscriptions and entry_state twice a Signal, once for items
and once for unread. One grouped subquery now gives both: count(*) and
a sum of the unread, joined to the person's Signals. Same results on
SQLite and Postgres (the existing counts test runs on both).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 15:30:30 +00:00
parent 21fa0ae476
commit c4878731c6
2 changed files with 19 additions and 16 deletions

View File

@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Traces show more of where a request's time goes: a page of items' query and its files apart, artwork's cache, check and fetch, the status counts, and each part of changing or leaving a feed (#350).
- A page of items finds its files by feed as well as item: it read every file's row in the database, about a quarter of a second a page (#344).
- A page of items asks for its count beside the items, and the feed list asks its five questions at once, where each waited for the one before (#346).
- A Signal's count of items and of unread ones are worked out in one pass, not two (#348).
### Fixed

View File

@@ -2804,23 +2804,25 @@ impl Db {
let mut a = Args::default();
let user = a.p(user_id);
let one = id.map(|i| format!("AND g.id = {}", a.p(i))).unwrap_or_default();
// The items it shows, and of those the unread: what its list counts, by the same rules.
let hits = |unread: &str| {
format!(
"(SELECT count(*) FROM signal_verdicts v
JOIN entries e ON e.feed_id = v.feed_id AND e.guid = v.guid
JOIN subscriptions s ON s.user_id = g.user_id AND s.feed_id = v.feed_id
LEFT JOIN entry_state st ON st.user_id = g.user_id AND st.feed_id = v.feed_id AND st.guid = v.guid
WHERE v.signal_id = g.id AND v.hit AND {unread} AND {} AND {})",
not_hidden(&user),
shown_in("hide_from_signals", "s")
)
};
// The items it shows, and of those the unread: what its list counts, by the same rules. One
// pass for both; they were two copies of the same subquery (#348).
let sql = format!(
"SELECT g.id, g.name, g.keywords, g.description, g.icon, {} AS items, {} AS unread
FROM signals g WHERE g.user_id = {user} {one} ORDER BY lower(g.name), g.id",
hits("true"),
hits("NOT coalesce(st.read, false)")
"SELECT g.id, g.name, g.keywords, g.description, g.icon,
coalesce(c.items, 0) AS items, coalesce(c.unread, 0) AS unread
FROM signals g
LEFT JOIN (SELECT v.signal_id, count(*) AS items,
sum(CASE WHEN NOT coalesce(st.read, false) THEN 1 ELSE 0 END) AS unread
FROM signal_verdicts v
JOIN signals vg ON vg.id = v.signal_id
JOIN entries e ON e.feed_id = v.feed_id AND e.guid = v.guid
JOIN subscriptions s ON s.user_id = vg.user_id AND s.feed_id = v.feed_id
LEFT JOIN entry_state st
ON st.user_id = vg.user_id AND st.feed_id = v.feed_id AND st.guid = v.guid
WHERE vg.user_id = {user} AND v.hit AND {} AND {}
GROUP BY v.signal_id) c ON c.signal_id = g.id
WHERE g.user_id = {user} {one} ORDER BY lower(g.name), g.id",
not_hidden(&user),
shown_in("hide_from_signals", "s")
);
self.rows(&sql, a.0)
.await?