-- AppView database schema 0009: indexes for handle → DID lookups. -- -- `/api/profile/` took 9.5 s on a 3.3 M-row `posts` table -- (measured against the dev instance). Both halves of `resolve_profile` -- were unindexed: -- -- 1. SELECT did FROM profiles WHERE LOWER(handle) = LOWER($1) -- 2. SELECT did FROM posts WHERE handle = $1 -- ORDER BY indexed_at DESC LIMIT 1 -- the fallback -- -- Step 2 was a parallel sequential scan over every post ever indexed -- (`Rows Removed by Filter: 1101310` per worker), and it runs on every -- profile view in the client. -- -- On `profiles`: migration 0007 dropped exactly this index, reasoning -- that "every caller derives a DID first (via posts.handle or the -- handle-sync worker) and then queries profiles by PK". That stopped -- being true when `resolve_profile` learned to prefer the profiles -- cache — it now asks `profiles` by handle *first*, precisely the -- lookup 0007 removed the support for. Re-added, matching the -- expression in the query (`LOWER(handle)`) so the planner can use it. CREATE INDEX IF NOT EXISTS profiles_handle_lower_idx ON profiles (LOWER(handle)); -- On `posts`: `(handle, indexed_at DESC)` covers filter *and* sort, so -- the LIMIT 1 becomes an index scan that stops at the first row. -- -- Partial on `handle <> ''`: empty handles are the un-backfilled -- majority on a firehose-fed instance and are never looked up by this -- path (the handle-sync worker queries them through its own predicate), -- so excluding them keeps the index small on the largest table we have. CREATE INDEX IF NOT EXISTS posts_handle_indexed_at_idx ON posts (handle, indexed_at DESC) WHERE handle <> ''; -- ===================================================== -- posts: the cold-start global feed -- ===================================================== -- -- `/api/timeline/home` falls back to the global recent feed for users -- without a follow graph — every new account's first screen. It took -- 7.4 s (parallel seq scan + top-N sort over 3.3 M rows) and timed out -- the integration tests' 5 s client. -- -- `posts_collection_indexed_at_uri_idx (collection, indexed_at DESC, -- uri DESC)` cannot serve it: the query filters -- `collection IN ('app.twi.post','app.bsky.feed.post')`, and with two -- leading values the index no longer yields rows in `indexed_at` order, -- so the planner falls back to scanning and sorting. -- -- A partial index over exactly that predicate moves the collection -- filter into the index definition, which leaves `(indexed_at DESC, -- uri DESC)` as the sort key — the LIMIT then stops after the first -- page. Same shape as the existing `posts_did_indexed_at_uri_idx`, -- which is partial on the same two collections. CREATE INDEX IF NOT EXISTS posts_feed_indexed_at_uri_idx ON posts (indexed_at DESC, uri DESC) WHERE collection IN ('app.twi.post', 'app.bsky.feed.post');