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)
This commit is contained in:
@@ -271,6 +271,27 @@ class GroupTable:
|
|||||||
.all()
|
.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(
|
def get_group_by_id(
|
||||||
self, id: str, db: Optional[Session] = None
|
self, id: str, db: Optional[Session] = None
|
||||||
) -> Optional[GroupModel]:
|
) -> Optional[GroupModel]:
|
||||||
|
|||||||
@@ -84,15 +84,16 @@ async def get_users(
|
|||||||
users = result["users"]
|
users = result["users"]
|
||||||
total = result["total"]
|
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 {
|
return {
|
||||||
"users": [
|
"users": [
|
||||||
UserGroupIdsModel(
|
UserGroupIdsModel(
|
||||||
**{
|
**{
|
||||||
**user.model_dump(),
|
**user.model_dump(),
|
||||||
"group_ids": [
|
"group_ids": [group.id for group in user_groups.get(user.id, [])],
|
||||||
group.id
|
|
||||||
for group in Groups.get_groups_by_member_id(user.id, db=db)
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for user in users
|
for user in users
|
||||||
|
|||||||
Reference in New Issue
Block a user