Django에서 JOIN 다루기 - ORM부터 Raw SQL까지
Django에서 JOIN 다루기 - ORM부터 Raw SQL까지
Django ORM의 JOIN 메커니즘
모델 관계 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# models.py
from django.db import models
class User(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField()
city = models.CharField(max_length=50)
created_at = models.DateTimeField(auto_now_add=True)
class Category(models.Model):
name = models.CharField(max_length=50)
description = models.TextField(blank=True)
class Product(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock_quantity = models.IntegerField(default=0)
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
order_date = models.DateField(auto_now_add=True)
status = models.CharField(max_length=20, default='pending')
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField()
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
select_related - SQL JOIN 활용
select_related
는 SQL의 LEFT JOIN을 생성하여 한 번의 쿼리로 관련 객체를 함께 가져온다.
기본 사용
1 2 3 4 5 6 7 8 9 10 # N+1 문제 발생 코드 orders = Order.objects.all() for order in orders: print(f"{order.user.name}: {order.total_amount}") # 각 order마다 user 쿼리 실행 # 생성되는 SQL (N+1 문제) # SELECT * FROM orders; -- 1번째 쿼리 # SELECT * FROM users WHERE id = 1; -- 2번째 쿼리 (첫 번째 주문의 사용자) # SELECT * FROM users WHERE id = 2; -- 3번째 쿼리 (두 번째 주문의 사용자) # ... 주문 개수만큼 반복
1 2 3 4 5 6 7 8 9 # select_related로 최적화 orders = Order.objects.select_related('user').all() for order in orders: print(f"{order.user.name}: {order.total_amount}") # 추가 쿼리 없음 # 생성되는 SQL (JOIN 사용) # SELECT orders.*, users.* # FROM orders # LEFT OUTER JOIN users ON orders.user_id = users.id;
다중 관계 select_related
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 여러 관계를 한 번에 JOIN order_items = OrderItem.objects.select_related( 'order__user', # order → user 'product__category' # product → category ).all() for item in order_items: print(f"고객: {item.order.user.name}") print(f"상품: {item.product.name}") print(f"카테고리: {item.product.category.name}") # 생성되는 SQL # SELECT orderitem.*, order.*, users.*, product.*, category.* # FROM orderitem # LEFT OUTER JOIN order ON orderitem.order_id = order.id # LEFT OUTER JOIN users ON order.user_id = users.id # LEFT OUTER JOIN product ON orderitem.product_id = product.id # LEFT OUTER JOIN category ON product.category_id = category.id;
select_related 사용 조건
- ForeignKey (N:1 관계)
- OneToOneField (1:1 관계)
- 역방향 OneToOneField
1 2 3 4 5 6 7 8 # 사용 가능한 경우 Order.objects.select_related('user') # ForeignKey User.objects.select_related('profile') # OneToOneField Profile.objects.select_related('user') # 역방향 OneToOneField # 사용 불가능한 경우 User.objects.select_related('order_set') # 역방향 ForeignKey (1:N) Order.objects.select_related('products') # ManyToManyField (N:N)
prefetch_related - 별도 쿼리 + Python 연결
prefetch_related
는 JOIN을 사용하지 않고 별도의 쿼리를 실행한 후 Python에서 관계를 연결한다.
동작 원리
1 2 3 4 5 6 7 # prefetch_related 사용 users = User.objects.prefetch_related('order_set').all() for user in users: print(f"{user.name}의 주문:") for order in user.order_set.all(): # 추가 쿼리 없음 print(f" {order.order_date}: {order.total_amount}")
내부 동작 과정
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1단계: 사용자 조회 SELECT id, name, email, city FROM users; # 2단계: 관련 주문들을 IN절로 한 번에 조회 SELECT id, user_id, order_date, total_amount FROM orders WHERE user_id IN (1, 2, 3, 4, 5); # 3단계: Python에서 user_id 기준으로 관계 연결 Django가 내부적으로 수행: { 1: [Order(id=1), Order(id=3)], # 김철수의 주문들 2: [Order(id=2), Order(id=5)], # 이영희의 주문들 3: [], # 박민수는 주문 없음 }
복잡한 prefetch_related
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from django.db.models import Prefetch # 조건부 prefetch users = User.objects.prefetch_related( Prefetch( 'order_set', queryset=Order.objects.filter(status='completed').order_by('-order_date'), to_attr='completed_orders' ) ).all() for user in users: for order in user.completed_orders: # 완료된 주문만 print(f"{order.order_date}: {order.total_amount}")
prefetch_related 사용 조건
- 역방향 ForeignKey (1:N 관계)
- ManyToManyField (N:N 관계)
- GenericForeignKey
1 2 3 User.objects.prefetch_related('order_set') # 역방향 ForeignKey Order.objects.prefetch_related('products') # ManyToManyField User.objects.prefetch_related('liked_products') # ManyToManyField
N+1 문제와 해결방법
N+1 문제 발생 패턴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 패턴 1: ForeignKey 접근 orders = Order.objects.all() for order in orders: print(order.user.name) # N+1 문제! # 해결: select_related 사용 orders = Order.objects.select_related('user').all() # 패턴 2: 역방향 ForeignKey 접근 users = User.objects.all() for user in users: print(user.order_set.count()) # N+1 문제! # 해결: prefetch_related 사용 users = User.objects.prefetch_related('order_set').all() # 패턴 3: 중첩 관계 접근 order_items = OrderItem.objects.all() for item in order_items: print(item.order.user.name) # N+1 문제! print(item.product.category.name) # N+1 문제! # 해결: 중첩 select_related 사용 order_items = OrderItem.objects.select_related( 'order__user', 'product__category' ).all()
Django ORM의 한계
지원하지 않는 JOIN 타입
1 2 3 4 5 6 7# Django ORM이 지원하지 않는 기능들 # 1. RIGHT JOIN # 2. FULL OUTER JOIN # 3. CROSS JOIN # 4. Self JOIN (제한적 지원) # 5. 복잡한 조건의 JOIN # 6. 윈도우 함수와 함께 사용되는 JOIN
Self Join의 한계
1 2 3 4 5 6 7 8 9 10 11 class Employee(models.Model): name = models.CharField(max_length=50) manager = models.ForeignKey('self', on_delete=models.SET_NULL, null=True) # ORM으로는 제한적 employees = Employee.objects.select_related('manager').all() for emp in employees: manager_name = emp.manager.name if emp.manager else '없음' print(f"{emp.name}의 관리자: {manager_name}") # 더 복잡한 Self JOIN은 Raw SQL 필요
This post is licensed under CC BY 4.0 by the author.