BACKEND/Framework & Library

[SQLAlchemy] SQLAlchemy 관계 로딩 / eager loading, lazy loading, joinedload, selectinload, contains_eager

ez1n 2025. 10. 28. 19:28

[SQLAlchemy 관계 로딩]

 

SQLAlchemy의 관계 로딩에 대해 공부해보자.

 


 

최근 sqlalchemy 버전을 올림과 동시에 flast to fastapi 마이그레이션 작업을 하고 있는데 모르는 개념들이 많이 생기고 있다.

특히 relationship(관계)에 대한 부분을 많이 공부해야할 것 같다는 생각이 들었다.

그래서 오늘은 sqlalchemy의 relationship loading에 대해 정리해 보려고 한다.


 

<STUDY>

 

❓ 관계 로딩 전략 (relationship loading strategy)

연관된 객체를 언제, 어떻게 가져올지를 결정하는 방법

 

- 구분설명쿼리 실행 시점

 

Lazy Loading 관계 필드를 접근할 때 쿼리를 실행 접근 시점
Eager Loading 관계를 미리 한 번에 로드 즉시 (Parent 쿼리 시점)

 

 

1️⃣ Lazy Loading (기본값)

 
class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", lazy="select")
    

## 사용
parents = session.query(Parent).all()

for p in parents:
    print(p.children)  # 여기서 매번 쿼리 실행됨 (N+1 문제)
  • 관계에 접근할 때마다 쿼리 추가로 실행
  • 데이터가 많을 경우 성능 저하 (N+1 Problem)

 

 

2️⃣ joinedload

JOIN을 사용하여 한 번에 데이터를 가져오는 방식
 
from sqlalchemy.orm import joinedload

stmt = select(Parent).options(joinedload(Parent.children))
parents = session.execute(stmt).scalars().all()
  • SQL: SELECT ... FROM parent JOIN child ...
  • 장점: 쿼리 한 번으로 모든 데이터 로드
  • 단점: JOIN이 많아지면 결과가 중복되고 비효율적일 수 있음

 

 

3️⃣ selectinload

JOIN 대신 IN 쿼리를 사용하여 한 번에 데이터를 가졍호는 방식

 

from sqlalchemy.orm import selectinload

stmt = select(Parent).options(selectinload(Parent.children))
parents = session.execute(stmt).scalars().all()
  • SQL
    1. 첫 번째 쿼리: 부모 리스트 조회
    2. 두 번째 쿼리: WHERE child.parent_id IN (...)
  • 장점: JOIN보다 깔끔하고 중복 감소
  • 단점: 쿼리가 최소 2회 실행

 

 

4️⃣ contains_eager

직접 작성한 JOIN 결과를 ORM 관계로 매핑할 때 사용

 

from sqlalchemy.orm import contains_eager

stmt = (
    select(Parent)
    .join(Parent.children)
    .options(contains_eager(Parent.children))
)
parents = session.execute(stmt).scalars().unique().all()
 

💡 특징

  • ORM에게 “이 JOIN 결과는 이미 관계 데이터야”라고 알려줌
  • JOIN 구문을 직접 제어할 수 있어, 복잡한 쿼리에 유용
  • 필터링된 관계 로딩, alias / 서브쿼리 조합에서 특히 중요

 

 

✅ 비교

lazy 접근 시 SELECT 접근 시점 단순, 느림 기본 관계
joinedload JOIN으로 로딩 즉시 쿼리 1회, 중복 가능 간단한 관계
selectinload IN 쿼리 2회 즉시 효율적, 중복 적음 다대일 / 일대다
contains_eager 수동 JOIN 매핑 즉시 커스텀 JOIN 가능 필터링된 관계, alias 사용

 


 

 

 

📌 contains_eager + alias 

JOIN 대상이 단순한 테이블이 아니라 별칭(alias) 혹은 서브쿼리인 경우 SQLAlchemy가 관계를 자동으로 인식하지 못하기 때문에 alias 인자를 명시적으로 넘겨줌

 

 

💡 필터링된 관계 eager load

 
from sqlalchemy.orm import aliased, contains_eager

ChildAlias = aliased(Child)

stmt = (
    select(Parent)
    .join(ChildAlias, Parent.id == ChildAlias.parent_id)
    .where(ChildAlias.age > 10)
    .options(contains_eager(Parent.children, alias=ChildAlias))
)

parents = session.execute(stmt).scalars().unique().all()
  • ChildAlias는 Child의 별칭 → ORM 자동 인식 불가
  • contains_eager(..., alias=ChildAlias)로 관계 직접 지정
  • 필터링된 alias 데이터를 Parent.children에 매핑 가능

 

💡 서브쿼리 alias

child_subq = (
    select(Child)
    .where(Child.age >= 10)
    .subquery()
)

FilteredChild = aliased(Child, child_subq)

stmt = (
    select(Parent)
    .join(FilteredChild, Parent.id == FilteredChild.parent_id)
    .options(contains_eager(Parent.children, alias=FilteredChild))
)

parents = session.execute(stmt).scalars().unique().all()
  • 서브쿼리 기반 alias를 JOIN하면 ORM이 이를 기본 관계로 인식하지 않음
  • alias 옵션을 지정해야 관계 필드(Parent.children)로 자동 매핑 가능

 

💡 여러 alias 관계를 동시에 eager load

AddressAlias = aliased(Address)
OrderAlias = aliased(Order)

stmt = (
    select(User)
    .join(AddressAlias, User.id == AddressAlias.user_id)
    .join(OrderAlias, User.id == OrderAlias.user_id)
    .options(
        contains_eager(User.addresses, alias=AddressAlias),
        contains_eager(User.orders, alias=OrderAlias),
    )
)
 

 

📌 각 관계마다 별도의 alias를 지정하여 매핑 가능하다.

 

 

✅ 정리

기본 관계 eager load .join(Parent.children).options(contains_eager(Parent.children))
필터링된 관계 eager load .join(ChildAlias)... .options(contains_eager(Parent.children, alias=ChildAlias))
서브쿼리 기반 alias eager load .join(FilteredChild)... .options(contains_eager(Parent.children, alias=FilteredChild))
여러 관계를 동시에 eager load .options(contains_eager(..., alias=...)) 여러 개 지정

 

 

 

 마무리 요약

 

lazy 관계 접근 시점에 쿼리 실행
joinedload JOIN으로 즉시 로딩
selectinload IN 쿼리로 즉시 로딩
contains_eager 직접 JOIN 결과를 ORM 관계로 매핑
alias + contains_eager 필터링된 / 서브쿼리 기반 관계를 깔끔하게 매핑할 때 필수

 

 

 


 

 

 

📘 자세한 내용은 공식 문서를 참고해주세요.

 


 

 

 

👉ez1n github 구경하기👈 

 

 

ez1n - Overview

Front-End Developer. ez1n has 18 repositories available. Follow their code on GitHub.

github.com