Context
The entities table currently has no indexes except the primary key (id). Two read paths scan it in full today:
- Leaderboard (
queryLeaderboard, public/private/both/followers) — ORDER BY total_commits DESC LIMIT k. Without an index this is a sequential scan plus a top-N sort.
- Public rank (
publicRankFor, shown on every profile) — SELECT count(*) WHERE suspended_at IS NULL AND total_commits > $x. Sequential scan + count aggregate. The comparison view fires up to 8 of these concurrently per page load.
This is fine at our current size, but worth fixing before the table grows.
Proposal
A partial index matching the active-entity filter both paths use:
CREATE INDEX entities_total_commits_idx
ON entities (total_commits DESC)
WHERE suspended_at IS NULL;
- Leaderboard:
ORDER BY total_commits DESC LIMIT k becomes an index scan that reads ~k rows instead of sorting the whole table. This is the biggest win — O(n log n) sort → effectively O(log n + k).
- Public rank: the count becomes an index range scan (index-only, no heap fetch) instead of a full heap scan.
- Write cost: negligible — rows change only on cache refresh, not on reads.
- Partial (
WHERE suspended_at IS NULL) keeps it small and exactly matches the query predicate.
How much it saves / when it's worth it
Rough Postgres seq-scan throughput on a narrow table is millions of rows/sec, so:
Rows in entities |
Per-query scan cost today |
Verdict |
| < ~10k |
sub-millisecond |
Index unnecessary — noise. |
| ~10k–100k |
low single-digit ms |
Nice-to-have; leaderboard sort starts to show. |
| ~100k–500k |
10s of ms, ×(leaderboard + up to 8 rank counts per compare view) |
Index clearly worth it. |
| > ~1M |
100ms+ per scan |
Required. |
So the trigger is roughly ~100k active rows, or sooner if profile/leaderboard traffic climbs (each compare-view load = up to 8 concurrent rank scans).
Action
Note: the public-rank feature itself needs no schema change — publicRank is a computed response field, not a stored column. This issue is purely a future performance optimization that also speeds up the existing leaderboard.
Context
The
entitiestable currently has no indexes except the primary key (id). Two read paths scan it in full today:queryLeaderboard, public/private/both/followers) —ORDER BY total_commits DESC LIMIT k. Without an index this is a sequential scan plus a top-N sort.publicRankFor, shown on every profile) —SELECT count(*) WHERE suspended_at IS NULL AND total_commits > $x. Sequential scan + count aggregate. The comparison view fires up to 8 of these concurrently per page load.This is fine at our current size, but worth fixing before the table grows.
Proposal
A partial index matching the active-entity filter both paths use:
ORDER BY total_commits DESC LIMIT kbecomes an index scan that reads ~krows instead of sorting the whole table. This is the biggest win —O(n log n)sort → effectivelyO(log n + k).WHERE suspended_at IS NULL) keeps it small and exactly matches the query predicate.How much it saves / when it's worth it
Rough Postgres seq-scan throughput on a narrow table is millions of rows/sec, so:
entitiesSo the trigger is roughly ~100k active rows, or sooner if profile/leaderboard traffic climbs (each compare-view load = up to 8 concurrent rank scans).
Action
EXPLAIN ANALYZEon the leaderboard and rank queries after adding it.Note: the public-rank feature itself needs no schema change —
publicRankis a computed response field, not a stored column. This issue is purely a future performance optimization that also speeds up the existing leaderboard.