schema: add partial index on channel_members.pubkey#359
Open
tlongwell-block wants to merge 1 commit intomainfrom
Open
schema: add partial index on channel_members.pubkey#359tlongwell-block wants to merge 1 commit intomainfrom
tlongwell-block wants to merge 1 commit intomainfrom
Conversation
The channel_members table only has a PK index on (channel_id, pubkey). Every query that looks up channels-for-a-user (WHERE pubkey = $1) does a sequential scan because the PK has channel_id first. get_accessible_channel_ids() runs on every REQ (subscription) message — it is the first thing the relay does after auth. On staging this has accumulated 5.6M seq scans reading 7.2B rows total (~5 scans/sec steady). Add a partial index on (pubkey) WHERE removed_at IS NULL, which covers the exact predicate used by the hot-path queries in sprout-db/channel.rs: - get_accessible_channel_ids (line 529) - channel_ids_for_pubkey (line 531) - is_member (line 491) - get_member_role (line 604) The table is small today (1,360 rows) so each scan is <0.3ms, but this is O(N) per subscription and will degrade linearly as users grow.
wesbillman
approved these changes
Apr 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every Nostr REQ (subscription) message calls
get_accessible_channel_ids()as its first database operation after auth. This query filterschannel_membersbyWHERE pubkey = $1 AND removed_at IS NULL— but the only index on the table is the primary key(channel_id, pubkey), which haschannel_idfirst. PostgreSQL cannot use this index for pubkey-first lookups, so every subscription triggers a sequential scan of the entire table.On staging right now:
channel_memberschannelstable seq scans (same pattern)The table is small today (1,360 rows) so each scan completes in <0.3ms and fits in shared_buffers (99.99% cache hit ratio). But this is O(N) per subscription — as users and channels grow, it will degrade linearly and become a real bottleneck.
Symptoms observed
Users are seeing "Failed to refresh channel history after subscribing" and "Timed out while loading channel history" on staging. While investigating, we also found:
redis-cli MONITORsession that had been running for ~6 hours, accumulating a 73MB output buffer in a single client connection (Redis pod limit is 256Mi). Killed it — memory dropped from 71MB → 1.45MB instantly.The index fix addresses the underlying database inefficiency; the Redis MONITOR issue was the acute trigger.
Solution
Add a partial index on
channel_members.pubkeyfor active members:This covers the exact predicate used by all hot-path queries in
sprout-db/src/channel.rs:get_accessible_channel_idsWHERE cm.pubkey = $1 AND cm.removed_at IS NULLchannel_ids_for_pubkeyWHERE cm.pubkey = $1 AND cm.removed_at IS NULLis_memberWHERE cm.channel_id = $1 AND cm.pubkey = $2 AND cm.removed_at IS NULLget_member_roleWHERE channel_id = $1 AND pubkey = $2 AND removed_at IS NULLlist_accessible_channelsLEFT JOIN ... AND cm.pubkey = $1 AND cm.removed_at IS NULLAlso used by DM lookups in
sprout-db/src/dm.rs(lines 244-245, 266-267).Why partial?
removed_at IS NOT NULL)Why not composite
(pubkey, channel_id)?A composite index would enable index-only scans for
SELECT channel_id WHERE pubkey = $1, but that is an optional future optimization. The single-column partial index is sufficient to eliminate the seq scans and is the minimal correct fix.Queries already covered by existing PK
Queries that filter on
(channel_id, pubkey)— likeget_member,remove_member,get_member_role— are already well-served by the PK index(channel_id, pubkey). No additional index needed for those.Rollout
CREATE INDEX(notCONCURRENTLY) takes a brief write lock onchannel_members. At 1,360 rows this is sub-millisecond and safe.EXPLAIN ANALYZEafter deploy that the planner picks the new index forget_accessible_channel_ids().