본문으로 건너뛰기

[Open WebUI] DB 쿼리 최적화: 루프 삭제를 벌크 연산으로 교체

PR 링크: open-webui/open-webui#21019 상태: Merged | 변경: +12 / -20

들어가며

ORM을 사용하다 보면 편의를 위해 비효율적인 패턴이 코드에 자리 잡는 경우가 많습니다. 대표적으로 "전체 조회 후 루프 삭제"와 "업데이트 후 불필요한 재조회"가 있습니다. 이번 PR은 Open WebUI의 세 가지 모델 파일에서 이러한 패턴을 일괄적으로 정리합니다.

핵심 코드 분석

루프 기반 삭제를 벌크 DELETE로 교체

Before (feedbacks.py):

def delete_feedbacks_by_user_id(self, user_id: str, db=None) -> bool:
    with get_db_context(db) as db:
        feedbacks = db.query(Feedback).filter_by(user_id=user_id).all()
        if not feedbacks:
            return False
        for feedback in feedbacks:
            db.delete(feedback)
        db.commit()
        return True

After:

def delete_feedbacks_by_user_id(self, user_id: str, db=None) -> bool:
    with get_db_context(db) as db:
        result = db.query(Feedback).filter_by(user_id=user_id).delete()
        db.commit()
        return result > 0

N개의 행을 삭제할 때 기존에는 먼저 SELECT *로 전체 행을 Python 메모리에 로드한 뒤, 각 객체에 대해 개별 DELETE SQL을 발행했습니다. 변경 후에는 단일 DELETE FROM ... WHERE 쿼리 하나로 처리됩니다.

그룹 멤버 제거를 IN 절로 배치 처리

Before (groups.py):

for user_id in user_ids:
    db.query(GroupMember).filter(
        GroupMember.group_id == id, GroupMember.user_id == user_id
    ).delete()

After:

db.query(GroupMember).filter(
    GroupMember.group_id == id, GroupMember.user_id.in_(user_ids)
).delete(synchronize_session=False)

업데이트 후 불필요한 재조회 제거

Before (functions.py):

db.commit()
db.refresh(function)
return self.get_function_by_id(id, db=db)

After:

db.commit()
db.refresh(function)
return FunctionModel.model_validate(function)

db.refresh()가 이미 DB에서 최신 상태를 가져왔으므로, get_function_by_id를 다시 호출하여 동일한 행을 한 번 더 SELECT할 필요가 없습니다.

왜 이게 좋은가

  • functions.py: 업데이트 연산당 불필요한 SELECT 쿼리 1회 절감
  • feedbacks.py: N개 행 삭제 시 N+1개의 SQL(SELECT 1 + DELETE N)이 단일 DELETE 1개로 감소
  • groups.py: 멤버 N명 제거 시 DELETE N개가 IN 절을 사용한 DELETE 1개로 감소
  • ORM의 편의성을 유지하면서도 실제 발행되는 SQL 수를 최소화하는 좋은 패턴입니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글