2025. 5. 19. 15:44ㆍ개발/Django
Django와 PostgreSQL을 사용하다 보면 bulk_create를 할 때 ID가 갑자기 크게 건너뛰는 현상을 경험할 수 있습니다. 예를 들어 ID가 12845에서 갑자기 1892456으로 점프하는 경우 말이죠. 이 글에서는 이 현상의 원인과 해결 방법을 알아보겠습니다.
🤔 문제 상황
다음과 같은 Django 코드를 실행했을 때:
# 데이터 준비
products = []
for data in data_list:
products.append(
Product(
sku=data["sku"],
name=data["name"],
category_id=data["category_id"],
price=data["price"],
description=data["description"],
# ... 기타 필드들
)
)
# bulk_create 실행
Product.objects.bulk_create(
products,
update_conflicts=True,
unique_fields=["sku"],
update_fields=[
"name",
"category_id",
"price",
"description",
# ... 업데이트할 필드들
],
)
ID가 12845에서 갑자기 1892456으로 건너뛰는 현상이 발생했습니다.
🔍 원인 분석
이 현상은 오류가 아닌 PostgreSQL의 정상적인 동작입니다. 두 가지 주요 원인이 있습니다:
1. PostgreSQL 시퀀스 캐싱
PostgreSQL은 성능 향상을 위해 시퀀스 값을 미리 캐시합니다. 연결이 종료되거나 트랜잭션이 롤백되면 캐시된 값들이 버려지면서 ID가 점프할 수 있습니다.
2. ON CONFLICT의 시퀀스 소모 (주요 원인)
update_conflicts=True를 사용하면 Django는 PostgreSQL의 INSERT ... ON CONFLICT DO UPDATE 구문을 생성합니다. 이때:
- PostgreSQL이 먼저 nextval()을 호출해서 새로운 ID를 가져옴
- 충돌(conflict)이 발생하면 INSERT 대신 UPDATE를 실행
- 이미 소모된 시퀀스 값은 되돌려지지 않음
만약 1000개의 레코드 중 950개가 이미 존재한다면, 950개의 시퀀스 값이 불필요하게 소모됩니다.
🛠️ 해결 방법
방법 1: 기존 레코드 분리 후 처리 (권장)
가장 효율적인 방법은 새로운 레코드와 업데이트할 레코드를 미리 분리하는 것입니다:
from django.db import transaction
@transaction.atomic
def efficient_bulk_upsert(data_list):
# 1. 기존에 존재하는 SKU 조회
existing_skus = set(
Product.objects.filter(
sku__in=[item["sku"] for item in data_list]
).values_list('sku', flat=True)
)
# 2. 새로운 레코드와 업데이트할 레코드 분리
new_products = []
update_products = []
for data in data_list:
product_obj = Product(
sku=data["sku"],
name=data["name"],
category_id=data["category_id"],
price=data["price"],
description=data["description"],
stock_quantity=data["stock_quantity"],
manufacturer=data["manufacturer"],
# ... 나머지 필드들
)
if data["sku"] in existing_skus:
update_products.append(product_obj)
else:
new_products.append(product_obj)
# 3. 새로운 레코드만 bulk_create (시퀀스 정상 사용)
if new_products:
Product.objects.bulk_create(new_products)
# 4. 기존 레코드는 bulk_update
if update_products:
Product.objects.bulk_update(
update_products,
fields=[
"name", "category_id", "price", "description",
"stock_quantity", "manufacturer", "is_active",
# ... 업데이트할 필드들
]
)
print(f"생성: {len(new_products)}개, 업데이트: {len(update_products)}개")
장점:
- 시퀀스가 불필요하게 소모되지 않음
- 성능이 더 좋음 (한 번의 쿼리로 기존 레코드 확인)
- 더 예측 가능한 동작
방법 2: 다른 모델 예제 - 사용자 정보
@transaction.atomic
def efficient_user_bulk_upsert(user_data_list):
# 1. 기존에 존재하는 이메일 조회
existing_emails = set(
UserProfile.objects.filter(
email__in=[item["email"] for item in user_data_list]
).values_list('email', flat=True)
)
# 2. 새로운 사용자와 업데이트할 사용자 분리
new_users = []
update_users = []
for data in user_data_list:
user_obj = UserProfile(
email=data["email"],
username=data["username"],
first_name=data["first_name"],
last_name=data["last_name"],
phone=data["phone"],
birth_date=data["birth_date"],
# ... 나머지 필드들
)
if data["email"] in existing_emails:
update_users.append(user_obj)
else:
new_users.append(user_obj)
# 3. 새로운 사용자만 bulk_create
if new_users:
UserProfile.objects.bulk_create(new_users)
# 4. 기존 사용자는 bulk_update
if update_users:
UserProfile.objects.bulk_update(
update_users,
fields=[
"username", "first_name", "last_name", "phone",
"birth_date", "last_login", "is_verified",
# ... 업데이트할 필드들
]
)
print(f"생성: {len(new_users)}개, 업데이트: {len(update_users)}개")
방법 3: ignore_conflicts 사용
업데이트가 필요 없고 단순히 중복을 무시하고 싶다면:
Product.objects.bulk_create(
products,
ignore_conflicts=True # update_conflicts 대신
)
방법 4: 시퀀스 리셋
이미 건너뛴 ID를 다시 연속적으로 만들고 싶다면:
from django.db import connection
def reset_sequence(table_name):
with connection.cursor() as cursor:
cursor.execute(f"""
SELECT setval(
pg_get_serial_sequence('{table_name}', 'id'),
COALESCE(MAX(id), 1)
)
FROM {table_name};
""")
# 사용 예시
reset_sequence('shop_product') # 또는 UserProfile 테이블의 경우
reset_sequence('accounts_userprofile')
또는 Django 관리 명령 사용:
python manage.py sqlsequencereset shop accounts
🎯 성능 최적화 팁
1. 배치 크기 조정
대량의 데이터를 처리할 때는 배치 크기를 조정하세요:
def bulk_upsert_in_batches(data_list, batch_size=1000):
for i in range(0, len(data_list), batch_size):
batch = data_list[i:i + batch_size]
efficient_bulk_upsert(batch)
print(f"처리 완료: {i + len(batch)}/{len(data_list)}")
# 사용자 데이터의 경우
def bulk_user_upsert_in_batches(user_data_list, batch_size=500):
for i in range(0, len(user_data_list), batch_size):
batch = user_data_list[i:i + batch_size]
efficient_user_bulk_upsert(batch)
print(f"사용자 처리 완료: {i + len(batch)}/{len(user_data_list)}")
💡 마무리
Django의 bulk_create에서 ID가 건너뛰는 현상은 오류가 아닌 PostgreSQL의 정상적인 동작입니다. 특히 update_conflicts=True를 사용할 때 ON CONFLICT 구문의 특성상 시퀀스가 소모되는 것은 피할 수 없습니다.
가장 좋은 해결책은 기존 레코드를 미리 분리해서 새로운 레코드만 bulk_create하고, 기존 레코드는 bulk_update를 사용하는 것입니다. 이렇게 하면 시퀀스도 절약하고 성능도 향상시킬 수 있습니다.
ID의 연속성이 중요한 비즈니스 로직이 있다면 시퀀스를 리셋할 수 있지만, 일반적으로는 ID의 갭은 문제가 되지 않으므로 크게 신경 쓸 필요는 없습니다.
'개발 > Django' 카테고리의 다른 글
Django Logging 에러 해결 방법 (0) | 2025.03.04 |
---|---|
DRF Spectacular에서 Swagger 파라미터 순서 변경하기 (0) | 2025.02.06 |
Django 윈도우 마이그레이션 파일 일괄 제거 (0) | 2024.10.21 |
Django에서 대용량 데이터 처리하기: bulk_create 메서드 활용하기 (0) | 2024.01.16 |
Django DB Query 설명과 예시 (1) | 2023.12.30 |