From 732d9b484db97a81ff7c8f71b16ad08d05da2477 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:26:14 +0100 Subject: [PATCH] fix: resolve N+1 query pattern in users endpoint (#20427) ## Summary Fixed N+1 query pattern in the `/api/v1/users` endpoint where groups were being fetched for each user individually. ### Problem The `GET /api/v1/users` endpoint called `Groups.get_groups_by_member_id()` for each user, resulting in: - 1 query for users - N queries for groups (one per user) ### Solution Added a new `Groups.get_groups_by_member_ids()` method that fetches groups for multiple users in a single query using SQL `IN` clause and `JOIN`. ### Changes - **[groups.py](open_webui/models/groups.py)**: Added `get_groups_by_member_ids()` method - **[users.py](open_webui/routers/users.py)**: Updated endpoint to use bulk method ### Result - Before: 1 + N queries - After: 2 queries total (1 for users, 1 for all groups) --- backend/open_webui/models/groups.py | 21 +++++++++++++++++++++ backend/open_webui/routers/users.py | 9 +++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index 053125a6d..ae557f4da 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -271,6 +271,27 @@ class GroupTable: .all() ] + def get_groups_by_member_ids( + self, user_ids: list[str], db: Optional[Session] = None + ) -> dict[str, list[GroupModel]]: + """Fetch groups for multiple users in a single query to avoid N+1.""" + with get_db_context(db) as db: + # Query GroupMember joined with Group, filtering by user_ids + results = ( + db.query(GroupMember.user_id, Group) + .join(Group, Group.id == GroupMember.group_id) + .filter(GroupMember.user_id.in_(user_ids)) + .order_by(Group.updated_at.desc()) + .all() + ) + + # Group groups by user_id + user_groups: dict[str, list[GroupModel]] = {uid: [] for uid in user_ids} + for user_id, group in results: + user_groups[user_id].append(GroupModel.model_validate(group)) + + return user_groups + def get_group_by_id( self, id: str, db: Optional[Session] = None ) -> Optional[GroupModel]: diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index f1758f26a..3c3d22c61 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -84,15 +84,16 @@ async def get_users( users = result["users"] total = result["total"] + # Fetch groups for all users in a single query to avoid N+1 + user_ids = [user.id for user in users] + user_groups = Groups.get_groups_by_member_ids(user_ids, db=db) + return { "users": [ UserGroupIdsModel( **{ **user.model_dump(), - "group_ids": [ - group.id - for group in Groups.get_groups_by_member_id(user.id, db=db) - ], + "group_ids": [group.id for group in user_groups.get(user.id, [])], } ) for user in users