본문으로 건너뛰기

[Open WebUI] 필터 함수 배치 조회로 N+1 쿼리 제거

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

들어가며

Open WebUI에서 채팅 요청을 처리할 때 필터 함수들을 로드하는 과정에서 전형적인 N+1 쿼리 문제가 존재했다. 필터 함수가 N개 있으면 get_function_by_id()를 N번 호출하여 N개의 개별 데이터베이스 쿼리가 발생했다. 이는 필터 함수 수가 늘어날수록 선형적으로 DB 부하가 증가하는 구조적 문제였다.

핵심 코드 분석

Before: 개별 쿼리를 리스트 컴프리헨션으로 반복 호출

filter_functions = [
    Functions.get_function_by_id(filter_id)
    for filter_id in get_sorted_filter_ids(
        request, model, metadata.get("filter_ids", [])
    )
]

필터 ID마다 get_function_by_id()를 호출하므로, 5개의 필터가 있으면 5번의 DB 쿼리가 발생한다.

After: 단일 IN 쿼리로 배치 조회

먼저 FunctionsTable에 배치 조회 메서드를 추가했다:

def get_functions_by_ids(
    self, ids: list[str], db: Optional[Session] = None
) -> list[FunctionModel]:
    if not ids:
        return []
    try:
        with get_db_context(db) as db:
            functions = db.query(Function).filter(Function.id.in_(ids)).all()
            # Create a dict for O(1) lookup
            func_dict = {f.id: FunctionModel.model_validate(f) for f in functions}
            # Return in original order, filtering out any not found
            return [func_dict[id] for id in ids if id in func_dict]
    except Exception:
        return []

그리고 호출부를 단순화했다:

filter_ids = get_sorted_filter_ids(request, model, metadata.get("filter_ids", []))
filter_functions = Functions.get_functions_by_ids(filter_ids)

왜 이게 좋은가

  1. 쿼리 수 감소: N개의 쿼리가 1개의 IN 쿼리로 통합된다. SQL의 IN 절은 인덱스를 활용할 수 있어 매우 효율적이다.
  2. 순서 보장: dict를 활용한 O(1) 룩업으로 원래 ID 순서를 유지하면서도 성능을 해치지 않는다.
  3. 빈 입력 방어: ids가 비어있으면 즉시 빈 리스트를 반환하여 불필요한 DB 연결을 방지한다.
  4. 동일 패턴 적용: chat.pymiddleware.py 두 곳 모두에 같은 패턴이 적용되어 일관성 있는 개선이 이루어졌다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글