perf(channels): batch N+1 queries for reactions and thread replies (#25831)

Replace per-message database queries with batch IN-clause queries in
channel message handlers. This eliminates the N+1 query pattern that
caused ~102 queries per channel page load (50 messages × 2 queries each).

Changes:
- Add get_reactions_by_message_ids() to MessageTable: single query
  fetches all reactions for multiple messages using IN clause with
  User JOIN, returns dict[message_id, list[Reactions]]
- Add get_thread_reply_counts_by_message_ids() to MessageTable: single
  GROUP BY aggregate query returns (count, max_created_at) per parent,
  replacing full object loads just to call len()
- Refactor get_channel_messages(): 102 → 4 queries per page
- Refactor get_pinned_channel_messages(): 22 → 3 queries per page
- Refactor get_channel_thread_messages(): 53 → 4 queries per page
- Refactor send_notification(): N+1 membership check → batch set lookup
This commit is contained in:
G30
2026-06-29 02:05:16 -05:00
committed by GitHub
parent 6050a94d77
commit e69ce6e1c6
2 changed files with 91 additions and 8 deletions
+23 -8
View File
@@ -808,10 +808,14 @@ async def get_channel_messages(
user_ids = list(set(m.user_id for m in message_list))
fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)}
# Batch fetch reactions and reply counts in 2 queries (fixes N+1)
message_ids = [m.id for m in message_list]
all_reactions = await Messages.get_reactions_by_message_ids(message_ids, db=db)
all_reply_counts = await Messages.get_thread_reply_counts_by_message_ids(message_ids, db=db)
messages = []
for message in message_list:
thread_replies = await Messages.get_thread_replies_by_message_id(message.id, db=db)
latest_thread_reply_at = thread_replies[0].created_at if thread_replies else None
reply_count, latest_reply_at = all_reply_counts.get(message.id, (0, None))
# Use message.user if present (for webhooks), otherwise look up by user_id
user_info = message.user
@@ -822,9 +826,9 @@ async def get_channel_messages(
MessageUserResponse(
**{
**message.model_dump(),
'reply_count': len(thread_replies),
'latest_reply_at': latest_thread_reply_at,
'reactions': await Messages.get_reactions_by_message_id(message.id, db=db),
'reply_count': reply_count,
'latest_reply_at': latest_reply_at,
'reactions': all_reactions.get(message.id, []),
'user': user_info,
}
)
@@ -873,6 +877,10 @@ async def get_pinned_channel_messages(
user_ids = list(set(m.user_id for m in message_list))
fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)}
# Batch fetch reactions in 1 query (fixes N+1)
message_ids = [m.id for m in message_list]
all_reactions = await Messages.get_reactions_by_message_ids(message_ids, db=db)
messages = []
for message in message_list:
# Check for webhook identity in meta
@@ -892,7 +900,7 @@ async def get_pinned_channel_messages(
MessageWithReactionsResponse(
**{
**message.model_dump(),
'reactions': await Messages.get_reactions_by_message_id(message.id, db=db),
'reactions': all_reactions.get(message.id, []),
'user': user_info,
}
)
@@ -913,8 +921,11 @@ async def send_notification(request, channel, message, active_user_ids, db=None)
users = await get_channel_users_with_access(channel, 'read', db=db)
# Batch fetch channel members in 1 query (fixes N+1)
member_ids = {m.user_id for m in await Channels.get_members_by_channel_id(channel.id, db=db)}
for u in users:
if (u.id not in active_user_ids) and await Channels.is_user_channel_member(channel.id, u.id, db=db):
if (u.id not in active_user_ids) and u.id in member_ids:
if enable_user_webhooks and u.settings:
webhook_url = u.settings.ui.get('notifications', {}).get('webhook_url', None)
if webhook_url:
@@ -1401,6 +1412,10 @@ async def get_channel_thread_messages(
user_ids = list(set(m.user_id for m in message_list))
fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)}
# Batch fetch reactions in 1 query (fixes N+1)
message_ids = [m.id for m in message_list]
all_reactions = await Messages.get_reactions_by_message_ids(message_ids, db=db)
messages = []
for message in message_list:
# Use message.user if present (for webhooks), otherwise look up by user_id
@@ -1414,7 +1429,7 @@ async def get_channel_thread_messages(
**message.model_dump(),
'reply_count': 0,
'latest_reply_at': None,
'reactions': await Messages.get_reactions_by_message_id(message.id, db=db),
'reactions': all_reactions.get(message.id, []),
'user': user_info,
}
)