개인공부

[ SQLAlchemy ] selecteload 와 joinload의 차이는 무엇일까?

KEEMSY 2024. 11. 12. 22:52

SQLAlchemy의 selectload와 joinload 차이는 무엇일까?

 

SQLAlchemy를 사용하다 보면 관계된 데이터를 로드하는 방식에 따라 성능이 크게 달라지는 것을 경험하게 됩니다. 특히 selectloadjoinload는 자주 사용되는 두 가지 로딩 전략인데, 각각의 특징과 적절한 사용 시점을 알아보겠습니다.

ORM에서의 데이터 로딩 전략

SQLAlchemy에서는 크게 세 가지 로딩 전략을 제공합니다:

  1. Lazy Loading (기본값)
  2. Eager Loading (joinedload)
  3. Select Loading (selectinload)

이 중에서 특히 많이 사용되는 Eager Loading과 Select Loading을 자세히 비교해보겠습니다.

selectload 상세 분석

selectload는 관계된 데이터를 별도의 SELECT 쿼리로 로드하는 방식입니다.

기본 사용법

from sqlalchemy import select
from sqlalchemy.orm import selectinload

# 기본적인 사용
stmt = select(Post).options(selectinload(Post.comments))
posts = session.execute(stmt).scalars().all()

# 중첩된 관계에서의 사용
stmt = select(Post).options(
    selectinload(Post.comments).selectinload(Comment.replies)
)
posts = session.execute(stmt).scalars().all()

생성되는 SQL

-- 게시글 조회
SELECT * FROM posts;

-- 댓글 조회
SELECT * FROM comments 
WHERE post_id IN (1, 2, 3, ...);

-- 대댓글 조회 (중첩 관계의 경우)
SELECT * FROM replies 
WHERE comment_id IN (1, 2, 3, ...);

성능 특성

  • 메모리 효율성: 필요한 데이터만 정확히 가져와 메모리 사용이 효율적
  • 쿼리 수: 관계마다 추가 쿼리 발생
  • 데이터베이스 부하: 여러 번의 쿼리로 인한 데이터베이스 연결 부하 발생 가능

joinload 상세 분석

joinload는 JOIN을 사용해 한 번에 모든 데이터를 가져오는 방식입니다.

 

기본 사용법

from sqlalchemy import select
from sqlalchemy.orm import joinedload

# 기본적인 사용
stmt = select(Post).options(joinedload(Post.comments))
posts = session.execute(stmt).scalars().all()

# 중첩된 관계에서의 사용
stmt = select(Post).options(
    joinedload(Post.comments).joinedload(Comment.replies)
)
posts = session.execute(stmt).scalars().all()

 

생성되는 SQL

-- 단일 관계
SELECT posts.*, comments.* 
FROM posts 
LEFT OUTER JOIN comments ON posts.id = comments.post_id;

-- 중첩 관계
SELECT posts.*, comments.*, replies.* 
FROM posts 
LEFT OUTER JOIN comments ON posts.id = comments.post_id
LEFT OUTER JOIN replies ON comments.id = replies.comment_id;

 

성능 특성

  • 메모리 사용: 모든 데이터를 한 번에 로드하여 메모리 사용량 증가
  • 쿼리 수: 단일 쿼리로 처리
  • 데이터베이스 부하: 복잡한 JOIN으로 인한 데이터베이스 처리 부하 발생 가능

실제 사용 사례와 성능 비교

게시판 시스템 예시

# 게시판 목록 조회 (selectload가 유리)
stmt = select(Board).options(
    selectinload(Board.posts),
    selectinload(Board.categories)
)

# 게시글 상세 조회 (joinload가 유리)
stmt = select(Post).options(
    joinedload(Post.author),
    joinedload(Post.comments).joinedload(Comment.author)
).where(Post.id == post_id)

성능 테스트 결과

다양한 데이터 크기에 따른 성능 비교

# 시나리오 1: 10,000개 게시글, 게시글당 평균 5개 댓글
## selectload
- 메모리 사용량: ~100MB
- 실행 시간: ~2초
- 생성된 쿼리 수: 2개

## joinload
- 메모리 사용량: ~400MB
- 실행 시간: ~1.5초
- 생성된 쿼리 수: 1개

# 시나리오 2: 1,000개 게시글, 게시글당 평균 50개 댓글
## selectload
- 메모리 사용량: ~150MB
- 실행 시간: ~3초
- 생성된 쿼리 수: 2개

## joinload
- 메모리 사용량: ~800MB
- 실행 시간: ~4초
- 생성된 쿼리 수: 1개

최적화 팁

1.복합 전략 사용

# 일부는 join으로, 일부는 select로 로드 
stmt = select(Post).options( 
	joinedload(Post.author), # 작은 데이터는join 
	selectinload(Post.comments) # 큰 데이터는 select 
)

 

2. 조건부 로딩

# 조건에 따라 다른 전략 사용
def get_posts(with_comments=False): 
	stmt = select(Post) 
    if with_comments: 
    	stmt = stmt.options(joinedload(Post.comments)) 
    return session.execute(stmt).scalars().all()

 

주의사항과 고려사항

메모리 관리

  • joinload 사용 시 메모리 모니터링 필요
  • 대량 데이터 처리 시 페이지네이션과 함께 사용

쿼리 최적화

  • 불필요한 관계 로딩 피하기
  • 필요한 컬럼만 선택적으로 로드

데이터베이스 인덱스

  • 관련 컬럼에 적절한 인덱스 설정 필요
  • JOIN 조건에 사용되는 컬럼 인덱싱

결론

두 전략 모두 각자의 장단점이 있어 상황에 맞게 선택하는 것이 중요합니다. 개인적으로는 기본적으로 selectload를 사용하고, 성능 최적화가 필요한 특정 상황에서만 joinload로 전환하는 방식을 선호합니다.

특히 다음 상황에서는 신중한 선택이 필요합니다:

  • 대량의 데이터를 다룰 때
  • 복잡한 관계를 가진 모델을 다룰 때
  • 실시간 성능이 중요한 API를 개발할 때

참고자료

728x90