• 카테고리

    질문 & 답변
  • 세부 분야

    풀스택

  • 해결 여부

    해결됨

외래키 참조 관계에서 bulk_create는 어떻게 사용하나요?

24.05.12 19:26 작성 24.05.12 19:28 수정 조회수 182

0

from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()


class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    content = models.TextField()

 

import itertools
from typing import Iterator, Tuple

from django.core.management import BaseCommand
from django.db import transaction

from blog.models import Post, Comment


class Command(BaseCommand):

    help = "test"

    def handle(self, *args, **options):

        generate_post_instances = (
            Post(title=title, content=content)
            for title, content in self.generate_post_row()
        )

        with transaction.atomic():
            for chunks in self.get_chunks(generate_post_instances, chunk_size=1000):
                Post.objects.bulk_create(chunks, ignore_conflicts=True)

            for post_instance in Post.objects.all():

                generate_comment_instance = (
                    Comment(post=post_instance, content=content)
                    for content in self.generate_comment_row()
                )

                for chunks in self.get_chunks(
                    generate_comment_instance, chunk_size=1000
                ):
                    Comment.objects.bulk_create(chunks, ignore_conflicts=True)

    @classmethod
    def generate_post_row(cls, size: int = 1000) -> Iterator[Tuple[str, str]]:
        for i in range(1, size + 1):
            yield f"Test post {i}", f"Test content {i}"

    @classmethod
    def generate_comment_row(cls, size: int = 100) -> Iterator[str]:
        for i in range(1, size + 1):
            yield f"Test comment {i}"

    @classmethod
    def get_chunks(cls, iterable: Iterator, chunk_size: int = 100) -> Iterator:
        iterator = iterable if hasattr(iterable, "__next__") else iter(iterable)
        for first in iterator:
            yield itertools.chain([first], itertools.islice(iterator, chunk_size - 1))


안녕하세요.
기존 강의에서 다루시던 대용량 데이터 처리 관련 코드를 참조하여 좀 더 이해하고자 커스텀해서 예시코드를 작성하게 되었습니다.

제너레이터, bulk_create 메서드에 대해서 질문이 있습니다.

1. 제너레이터는 yield 한 다음 반복문을 이용해 호출합니다. 제너레이트를 호출 한다면 호출할 때 반복문이 여러개 겹치게 되는데 이때 시간복잡도는 어떻게 되는건가요?

[generate_post_row 호출 > generate_post_instances 호출 > get_chunks > 호출] 이때 반복문을 많이 사용하게 되는데 어떤식으로 처리가 되는지 궁금합니다.

2. transaction.atomic() 에서 먼저 Post을 bulk_create로 생성한 뒤 해당 Post의 데이터들을 Comment 인스턴스에 넘겨주어 bulk_create를 사용하였는데. 혹시 더 나은 방법이 있을까요?

답변 3

·

답변을 작성해보세요.

0

ignore_conflicts=False 에서는 INSERT 쿼리가 수행되는 데요. 이때에는 RETURNING "id" 문이 지정되어, 이를 통해 INSERT된 기본키를 반환받을 수 있습니다. 그래서 bulk_create 함수의 반환값으로 받은 모델 인스턴스의 .id 에는 저장된 id값이 할당되어있습니다.

그런데, ignore_conflicts=True 에서는 INSERT OR IGNORE 쿼리가 수행되구요. 이때에는 RETURING "id" 문이 적용되지 않습니다. 그래서 bulk_create 함수의 반환값으로 받은 모델 인스턴스의. id 값은 모두 None입니다. 물론 데이터베이스에는 저장된 상황이니, 이어서 SELECT 조회를 해보실 수는 있으시겠습니다.

데이터베이스의 INSERT OR IGNORE INTO 테이블 (생략) RETURNING "id" 쿼리에서는

  • INSERT 가 되면 "id" 값을 반환하는 데, 충돌이 발생하면 "id" 값을 반환하지 않거든요. 그래서 장고에서는 일관성을 위해 ignore_conflicts=True 설정에서는 id 값이 있을 수도 있고 없을 수도 있기에, id 값을 모두 할당하지 않는 방향으로 수행되는 듯 보입니다. (뇌피셜입니다.)

     

그리고, 많은 양의 데이터를 INSERT하실 때, 충돌이 발생할 수 있는 unique 필드가 있다면 데이터베이스에서의 충돌 검사 작업이 추가적인 부하를 발생시켜, INSERT 성능이 느릴 수 있습니다. unique 제약사항이나 인덱스 설정이 없는 상황에서 모든 데이터를 추가하신 후에 unique 제약사항이나 인덱스 설정을 켜시는 것이 성능에 도움이 되실 수 있습니다.


강의에서는 장고 기본 기능으로서 insert 쿼리를 모아서 수행함으로서 insert 속도를 빠르게 올리는 방법을 안내해드린 것이구요. generator는 공간 복잡도를 줄이는 접근이구요.

대용량의 기준은 상대적이라서요. 흠. 수GB 이상, 저장에 수분, 수십분 이상 걸리신다면 데이터베이스의 기능으로 데이터를 밀어넣으시는 것이 낫지 않을까 싶습니다. 훨씬 빠르게 처리되거든요.

PostgreSQL에서는 csv 데이터를 copy 하는 명령을 지원해주구요.
https://www.postgresql.org/docs/current/sql-copy.html

장고에서는 django-postgres-copy 와 같은 라이브러리도 있습니다. 이 라이브러리의 공식문서를 꼭 참고해보세요. :-)

yezi9733님의 프로필

yezi9733

질문자

2024.05.14

자세한 설명 감사합니다. 아직은 하나의 클래스, 메서드에서 다루는 코드자체를 깊게 파악하는것보단 해당 클래스, 메서드를 사용해서 기능을 먼저 구현해보는 쪽으로 초점을 맞췄었는데 공식문서를 읽으면서 옵션들을 파악해보는 습관을 가져야 덜 헤매겠네요.

덕분에 이해되었습니다. 감사합니다~

0

안녕하세요.

Generator를 사용하더라도 2중~3중 반복문이니깐요. 시간복잡도는 O(M*N)으로 보여집니다.
하지만 Generator는 지정 개수 만큼의 메모리만 소모할테니 공간복잡도는 O(1)일 것입니다.

보여주신 코드의 경우, Post 단위로 chunks를 생성하는 것이 아니라, Post 리스트에 대해서 chunks를 생성하도록 해볼 수 있지 않을까요?

image

yezi9733님의 프로필

yezi9733

질문자

2024.05.13

시간복잡도는 조금 더 높아지지만 공간 복잡도는 상수가 되는거네요.

bulk_create에서는 인스턴스 리스트를 반환하기때문에 post의 각 인스턴스에대해 comment에서 접근할 수 있는거군요.

여기서 1:N 관계에 대해서는 연관된 외래키 필드의 유효성을 보장해야돼서 ignore_conflicts 옵션은 적용할 수 없는건가요?
ValueError: bulk_create() prohibited to prevent data loss due to unsaved related object 'post'
해당 에러 메세지가 발생하네요.

한 가지 더 궁금한점이 있습니다. 대용량 데이터는 어느정도의 규모를 대용량 데이터라고 하는걸까요? 규격을 알면 더 좋을것 같습니다!
답변 감사합니다~

0

인프런 AI 인턴님의 프로필

인프런 AI 인턴

2024.05.12

안녕하세요, 인프런 AI 인턴입니다. yezi9733 님의 질문에 대해 관련 정보를 찾아봤습니다. 질문하신 내용은 대용량 데이터 처리와 관련하여, 제너레이터와 bulk_create 메서드의 사용법에 초점을 맞춘 것으로 보입니다. 이러한 문제에 대해 유사한 상황을 다룬 예시가 있어 소개해 드리겠습니다.

메모리 효율성을 개선한 버전 질문 있습니다.

yezi9733 님이 작성한 질문: 메모리 효율성을 개선한 버전 질문 있습니다.에서는 bulk_create 사용 시 제너레이터와 함께 데이터 처리하는 방법에 대한 질문이 있었습니다. 이 질문은 제너레이터 사용 시 데이터를 한 번에 처리하면서 발생할 수 있는 이슈와 bulk_create를 이용해 대량의 데이터를 효율적으로 처리하는 방법에 대해 다루고 있습니다. 특히, 제너레이터를 사용하여 데이터를 불러오는 과정에서 메모리 사용을 최소화하는 방법에 대한 답변이 담겨 있어 참고하시면 효율적인 데이터 처리 방법에 대해 더 깊이 이해할 수 있습니다.

중요 포인트

  • bulk_create 메서드를 이용하면 대량의 데이터를 데이터베이스에 효율적으로 삽입할 수 있습니다. 이 때, 메모리 사용량을 최소화하기 위해서는 제너레이터를 적절히 활용해야 합니다.
  • 제너레이터와 bulk_create를 사용할 때, 제너레이터의 특성상 한 번 소진된 데이터는 다시 생성되지 않기 때문에 데이터를 넘겨줄 때 이 점을 고려해야 합니다. 이 질문에서 제공되는 예시와 같이, 데이터 처리 과정에서 잘못된 접근으로 인해 예상치 못한 오류가 발생할 수 있으므로 주의해야 합니다.

이러한 방법론과 기술적인 접근은 데이터 처리 시 효율성과 성능을 크게 향상시킬 수 있기 때문에 장고에서 대규모 데이터를 다루는 상황에 매우 유용합니다. yezi9733 님께서 제시한 코드 또한 일련의 프로세스를 거쳐 데이터를 처리하는 방식에서 효율성을 높이고자 하는 목적을 가지고 있습니다. 이에 대해 더 깊은 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.

현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

채널톡 아이콘