Count unread feed by feed (#352)
After #347 an unread count read only the items since each feed's unread_from, but Postgres guessed a range join across every feed keeps a third of its rows (17,000 where there are 200), so it hashed all 22,949 of the person's entry_state rows. Counted in a subquery per subscription, the feed and its unread_from are constants there, and it looks up the few items since and their read state by key: on production the feed list's unread counts went from 19.7 to 14.8 ms and All Subscriptions' unread count from 19 to 13. The feed list's unread counts and count_of for Unread without a search (All Subscriptions, a feed, an OPML's shows) use it; a page of unread items still joins, since it needs rows in order, not a count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,6 +191,21 @@ pub fn location() -> String {
|
||||
/// The Postgres connection option that keeps notices from being sent at all; see `connect`.
|
||||
const QUIET: &str = "options=-c%20client_min_messages%3Dwarning";
|
||||
|
||||
/// How many of one subscription's items, `sub`, are unread and not hidden: a query of its own
|
||||
/// for each, so Postgres sees the feed and where its unread begins as constants and looks up the
|
||||
/// few items since. Joined across every feed, it guessed a third of them and hashed all of the
|
||||
/// person's read state, 22,949 rows for 44 unread (#352).
|
||||
macro_rules! unread_in_sub {
|
||||
() => {
|
||||
"SELECT count(*) FROM entries e
|
||||
LEFT JOIN entry_state s ON s.user_id = sub.user_id AND s.feed_id = e.feed_id AND s.guid = e.guid
|
||||
WHERE e.feed_id = sub.feed_id AND e.first_seen >= coalesce(sub.unread_from, 0)
|
||||
AND NOT coalesce(s.read, false)
|
||||
AND NOT EXISTS (SELECT 1 FROM hidden h
|
||||
WHERE h.user_id = sub.user_id AND h.feed_id = e.feed_id AND h.guid = e.guid)"
|
||||
};
|
||||
}
|
||||
|
||||
impl Db {
|
||||
/// The connection pool as it stands, for /metrics (#194): connections open, those of them
|
||||
/// idle, and the most it will open.
|
||||
@@ -545,16 +560,13 @@ impl Db {
|
||||
);
|
||||
let unread = counts(
|
||||
"unread",
|
||||
"SELECT e.feed_id, count(*) AS n FROM entries e
|
||||
JOIN subscriptions sub ON sub.user_id = $1 AND sub.feed_id = e.feed_id
|
||||
AND e.first_seen >= coalesce(sub.unread_from, 0)
|
||||
LEFT JOIN entry_state s
|
||||
ON s.user_id = $1 AND s.feed_id = e.feed_id AND s.guid = e.guid
|
||||
WHERE NOT coalesce(s.read, false)
|
||||
AND (CAST($2 AS TEXT) IS NULL OR e.feed_id = $2)
|
||||
AND NOT EXISTS (SELECT 1 FROM hidden h
|
||||
WHERE h.user_id = $1 AND h.feed_id = e.feed_id AND h.guid = e.guid)
|
||||
GROUP BY e.feed_id",
|
||||
concat!(
|
||||
"SELECT * FROM (SELECT sub.feed_id, (",
|
||||
unread_in_sub!(),
|
||||
") AS n FROM subscriptions sub
|
||||
WHERE sub.user_id = $1 AND (CAST($2 AS TEXT) IS NULL OR sub.feed_id = $2)) x
|
||||
WHERE n > 0"
|
||||
),
|
||||
vec![user_id.into(), one()],
|
||||
);
|
||||
let blocked = tracing::Instrument::instrument(
|
||||
@@ -1561,7 +1573,23 @@ impl Db {
|
||||
search: Option<&str>,
|
||||
) -> Result<i64> {
|
||||
let mut a = Args::default();
|
||||
let sql = format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, feeds, filter, search, None, None));
|
||||
let sql = if filter == Filter::Unread && search.is_none() {
|
||||
// Feed by feed, as the feed list counts (#352); the same feeds as `entries_from`.
|
||||
let user = a.p(user_id);
|
||||
let scope = match feeds {
|
||||
Some([f]) => format!("sub.feed_id = {}", a.p(*f)),
|
||||
Some([]) => "false".into(),
|
||||
Some(fs) => format!("sub.feed_id IN ({})", fs.iter().map(|f| a.p(*f)).collect::<Vec<_>>().join(", ")),
|
||||
None => shown_in("hide_from_all", "sub"),
|
||||
};
|
||||
format!(
|
||||
"SELECT CAST(coalesce(sum(n), 0) AS BIGINT) AS n
|
||||
FROM (SELECT ({}) AS n FROM subscriptions sub WHERE sub.user_id = {user} AND {scope}) x",
|
||||
unread_in_sub!()
|
||||
)
|
||||
} else {
|
||||
format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, feeds, filter, search, None, None))
|
||||
};
|
||||
let r = self.rows(&sql, a.0).await?;
|
||||
Ok(r.first().context("count(*) always returns a row")?.try_get("", "n")?)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user