InfoGrab DocsInfoGrab Docs

테이블에 일괄 삽입하기

요약

대량의 레코드를 한 번에 저장해야 할 때가 있으며, 컬렉션을 순회하면서 각 레코드를 개별적으로 저장하면 비효율적입니다. 모델 클래스가 대량 삽입 API를 활용하려면 먼저 BulkInsertSafe concern을 포함해야 합니다:

대량의 레코드를 한 번에 저장해야 할 때가 있으며, 컬렉션을 순회하면서 각 레코드를 개별적으로 저장하면 비효율적입니다. 행 수준(즉, Hash 객체 사용)에서 동작하는 insert_all 이 Rails 6에 도입되면서, GitLab은 ActiveRecord 객체를 안전하고 간단하게 대량으로 삽입할 수 있는 API 세트를 추가했습니다.

대량 삽입을 위한 ApplicationRecord 준비#

모델 클래스가 대량 삽입 API를 활용하려면 먼저 BulkInsertSafe concern을 포함해야 합니다:

class MyModel < ApplicationRecord
  # other includes here
  # ...
  include BulkInsertSafe # include this last

  # ...
end

BulkInsertSafe concern은 두 가지 기능을 합니다:

  • 대량 삽입과 관련해 안전하게 사용할 수 없는 ActiveRecord API를 모델 클래스가 사용하는지 검사합니다(자세한 내용은 아래를 참고합니다).
  • 한 번에 많은 레코드를 삽입할 수 있는 새 클래스 메서드 bulk_insert!와 bulk_upsert!를 추가합니다.

bulk_insert! 및 bulk_upsert!로 레코드 삽입#

대상 클래스가 BulkInsertSafe의 검사를 통과하면, 다음과 같이 ActiveRecord 모델 객체 배열을 삽입할 수 있습니다:

records = [MyModel.new, ...]

MyModel.bulk_insert!(records)

bulk_insert! 호출은 항상 새 레코드 삽입을 시도합니다. 기존 레코드를 새 값으로 대체하면서 아직 존재하지 않는 레코드도 함께 삽입하려면 bulk_upsert!를 사용할 수 있습니다:

records = [MyModel.new, existing_model, ...]

MyModel.bulk_upsert!(records, unique_by: [:name])

이 예에서 unique_by는 레코드를 고유한 것으로 판단하는 칼럼을 지정하며, 해당 레코드가 삽입 이전에 이미 존재했다면 업데이트됩니다. 예를 들어 existing_model에 name 속성이 있고 같은 name 값을 가진 레코드가 이미 존재하면, 그 필드는 existing_model의 값으로 업데이트됩니다.

unique_by 파라미터는 Symbol로도 전달할 수 있으며, 이때는 칼럼을 고유한 것으로 판단하는 데이터베이스 인덱스를 지정합니다:

MyModel.bulk_insert!(records, unique_by: :index_on_name)

레코드 유효성 검사#

bulk_insert! 메서드는 records가 트랜잭션 방식으로 삽입되도록 보장하며, 삽입 전에 각 레코드에 대해 유효성 검사를 실행합니다. 유효성 검사에 실패한 레코드가 있으면 오류가 발생하고 트랜잭션이 롤백됩니다. :validate 옵션으로 유효성 검사를 끌 수 있습니다:

MyModel.bulk_insert!(records, validate: false)

배치 크기 설정#

records 수가 특정 임계값을 넘으면 삽입이 여러 배치로 나뉘어 수행됩니다. 기본 배치 크기는 BulkInsertSafe::DEFAULT_BATCH_SIZE 에 정의되어 있습니다. 기본 임계값이 500이라고 가정하면, 950개의 레코드를 삽입할 때 배치 두 개(크기 500과 450)가 순차적으로 기록됩니다. :batch_size 옵션으로 기본 배치 크기를 재정의할 수 있습니다:

MyModel.bulk_insert!(records, batch_size: 100)

같은 950개 레코드를 기준으로 하면 이때는 배치가 10개 기록됩니다. 이는 발생하는 INSERT 문의 수에도 영향을 주므로, 코드에 미치는 성능 영향을 반드시 측정합니다. 데이터베이스가 처리해야 하는 INSERT 문의 수와 각 INSERT의 크기 및 비용 사이에는 트레이드오프가 있습니다.

중복 레코드 처리#

Note

이 파라미터는 bulk_insert!에만 적용됩니다. 기존 레코드를 업데이트하려면 bulk_upsert!를 대신 사용합니다.

삽입하려는 레코드 중 일부가 이미 존재해서 기본 키 충돌이 발생할 수 있습니다. 이 문제를 해결하는 방법은 두 가지로, 오류를 발생시켜 즉시 실패하거나 중복 레코드를 건너뛰는 것입니다. bulk_insert!의 기본 동작은 즉시 실패하고 ActiveRecord::RecordNotUnique 오류를 발생시키는 것입니다.

이 동작이 적절하지 않다면 skip_duplicates 플래그로 중복 레코드를 건너뛸 수 있습니다:

MyModel.bulk_insert!(records, skip_duplicates: true)

안전한 대량 삽입 요건#

ActiveRecord의 퍼시스턴스 API는 상당 부분이 콜백 개념을 중심으로 만들어져 있습니다. 이러한 콜백 중 다수는 save나 create 같은 모델 라이프사이클 이벤트에 반응해 실행됩니다. 이러한 콜백은 저장되거나 생성되는 인스턴스마다 호출되도록 만들어졌기 때문에 대량 삽입에는 사용할 수 없습니다. 레코드를 대량으로 삽입할 때는 이러한 이벤트가 발생하지 않으므로 사용을 막고 있습니다.

명시적으로 허용되는 콜백은 BulkInsertSafe 에 정의되어 있습니다. 자세한 내용은 모듈 소스 코드를 참고합니다. 안전하다고 명시되지 않은 콜백을 사용하는 클래스에 include BulkInsertSafe를 추가하면 애플리케이션은 오류와 함께 실패합니다.

BulkInsertSafe와 InsertAll 비교#

BulkInsertSafe는 내부적으로 InsertAll을 기반으로 하므로, 둘 중 무엇을 선택해야 할지 고민될 수 있습니다. 판단에 도움이 되도록 두 클래스의 주요 차이를 아래 표에 정리했습니다.

입력 타입 입력 유효성 검사 배치 크기 지정 콜백 우회 가능 트랜잭션
bulk_insert! ActiveRecord 객체 예 (선택 사항) 예 (선택 사항) 아니요 (안전하지 않은 콜백 사용 방지) 예
insert_all! 속성 해시 아니요 아니요 예 예

정리하면, BulkInsertSafe는 대량 삽입을 ActiveRecord 객체와 일반적인 삽입 동작에 더 가깝게 만듭니다. 다만 원시 데이터를 대량으로 삽입하기만 하면 되는 경우에는 insert_all이 더 효율적입니다.

has_many 연관 관계 대량 삽입#

흔한 사용 사례는 연관된 관계의 컬렉션을 관계의 소유자 쪽을 통해 저장하는 것이며, 소유된 관계는 has_many 클래스 메서드로 소유자와 연결됩니다:

owner = OwnerModel.new(owned_relations: array_of_owned_relations)
# saves all `owned_relations` one-by-one
owner.save!

이 방식은 owned_relations의 레코드마다 INSERT와 트랜잭션을 하나씩 발생시키므로 array_of_owned_relations가 크면 비효율적입니다. 이를 개선하려면 BulkInsertableAssociations concern으로 소유자가 대량 삽입에 안전한 연관 관계를 정의한다고 선언할 수 있습니다:

class OwnerModel < ApplicationRecord
  # other includes here
  # ...
  include BulkInsertableAssociations # include this last

  has_many :my_models
end

대량 삽입이 이루어지려면 여기서 my_models가 앞에서 설명한 대로 BulkInsertSafe로 선언되어 있어야 합니다. 이제 아직 저장되지 않은 레코드를 다음과 같이 삽입할 수 있습니다:

BulkInsertableAssociations.with_bulk_insert do
  owner = OwnerModel.new(my_models: array_of_my_model_instances)
  # saves `my_models` using a single bulk insert (possibly via multiple batches)
  owner.save!
end

이 블록 안에서 BulkInsertSafe가 아닌 관계도 저장할 수 있습니다. 이러한 관계는 블록 바깥에서 save를 호출한 것과 동일하게 처리됩니다.

알려진 제한 사항#

이러한 API의 사용 방식에는 몇 가지 제약이 있습니다:

  • BulkInsertableAssociations:
    • 현재 has_many 관계와만 호환됩니다.
    • has_many through: ... 관계는 아직 지원하지 않습니다.

또한 입력 데이터는 최대 1000개 정도의 레코드로 제한하거나, 대량 삽입을 호출하기 전에 미리 배치로 나누어야 합니다. INSERT 문은 단일 트랜잭션에서 실행되므로 레코드 수가 많으면 데이터베이스 안정성에 부정적인 영향을 줄 수 있습니다.

테이블에 일괄 삽입하기

GitLab v19.4
원문 보기

요약

대량의 레코드를 한 번에 저장해야 할 때가 있으며, 컬렉션을 순회하면서 각 레코드를 개별적으로 저장하면 비효율적입니다. 모델 클래스가 대량 삽입 API를 활용하려면 먼저 BulkInsertSafe concern을 포함해야 합니다:

대량의 레코드를 한 번에 저장해야 할 때가 있으며, 컬렉션을 순회하면서 각 레코드를 개별적으로 저장하면 비효율적입니다. 행 수준(즉, Hash 객체 사용)에서 동작하는 insert_all 이 Rails 6에 도입되면서, GitLab은 ActiveRecord 객체를 안전하고 간단하게 대량으로 삽입할 수 있는 API 세트를 추가했습니다.

대량 삽입을 위한 ApplicationRecord 준비#

모델 클래스가 대량 삽입 API를 활용하려면 먼저 BulkInsertSafe concern을 포함해야 합니다:

class MyModel < ApplicationRecord
  # other includes here
  # ...
  include BulkInsertSafe # include this last

  # ...
end

BulkInsertSafe concern은 두 가지 기능을 합니다:

  • 대량 삽입과 관련해 안전하게 사용할 수 없는 ActiveRecord API를 모델 클래스가 사용하는지 검사합니다(자세한 내용은 아래를 참고합니다).
  • 한 번에 많은 레코드를 삽입할 수 있는 새 클래스 메서드 bulk_insert!와 bulk_upsert!를 추가합니다.

bulk_insert! 및 bulk_upsert!로 레코드 삽입#

대상 클래스가 BulkInsertSafe의 검사를 통과하면, 다음과 같이 ActiveRecord 모델 객체 배열을 삽입할 수 있습니다:

records = [MyModel.new, ...]

MyModel.bulk_insert!(records)

bulk_insert! 호출은 항상 새 레코드 삽입을 시도합니다. 기존 레코드를 새 값으로 대체하면서 아직 존재하지 않는 레코드도 함께 삽입하려면 bulk_upsert!를 사용할 수 있습니다:

records = [MyModel.new, existing_model, ...]

MyModel.bulk_upsert!(records, unique_by: [:name])

이 예에서 unique_by는 레코드를 고유한 것으로 판단하는 칼럼을 지정하며, 해당 레코드가 삽입 이전에 이미 존재했다면 업데이트됩니다. 예를 들어 existing_model에 name 속성이 있고 같은 name 값을 가진 레코드가 이미 존재하면, 그 필드는 existing_model의 값으로 업데이트됩니다.

unique_by 파라미터는 Symbol로도 전달할 수 있으며, 이때는 칼럼을 고유한 것으로 판단하는 데이터베이스 인덱스를 지정합니다:

MyModel.bulk_insert!(records, unique_by: :index_on_name)

레코드 유효성 검사#

bulk_insert! 메서드는 records가 트랜잭션 방식으로 삽입되도록 보장하며, 삽입 전에 각 레코드에 대해 유효성 검사를 실행합니다. 유효성 검사에 실패한 레코드가 있으면 오류가 발생하고 트랜잭션이 롤백됩니다. :validate 옵션으로 유효성 검사를 끌 수 있습니다:

MyModel.bulk_insert!(records, validate: false)

배치 크기 설정#

records 수가 특정 임계값을 넘으면 삽입이 여러 배치로 나뉘어 수행됩니다. 기본 배치 크기는 BulkInsertSafe::DEFAULT_BATCH_SIZE 에 정의되어 있습니다. 기본 임계값이 500이라고 가정하면, 950개의 레코드를 삽입할 때 배치 두 개(크기 500과 450)가 순차적으로 기록됩니다. :batch_size 옵션으로 기본 배치 크기를 재정의할 수 있습니다:

MyModel.bulk_insert!(records, batch_size: 100)

같은 950개 레코드를 기준으로 하면 이때는 배치가 10개 기록됩니다. 이는 발생하는 INSERT 문의 수에도 영향을 주므로, 코드에 미치는 성능 영향을 반드시 측정합니다. 데이터베이스가 처리해야 하는 INSERT 문의 수와 각 INSERT의 크기 및 비용 사이에는 트레이드오프가 있습니다.

중복 레코드 처리#

Note

이 파라미터는 bulk_insert!에만 적용됩니다. 기존 레코드를 업데이트하려면 bulk_upsert!를 대신 사용합니다.

삽입하려는 레코드 중 일부가 이미 존재해서 기본 키 충돌이 발생할 수 있습니다. 이 문제를 해결하는 방법은 두 가지로, 오류를 발생시켜 즉시 실패하거나 중복 레코드를 건너뛰는 것입니다. bulk_insert!의 기본 동작은 즉시 실패하고 ActiveRecord::RecordNotUnique 오류를 발생시키는 것입니다.

이 동작이 적절하지 않다면 skip_duplicates 플래그로 중복 레코드를 건너뛸 수 있습니다:

MyModel.bulk_insert!(records, skip_duplicates: true)

안전한 대량 삽입 요건#

ActiveRecord의 퍼시스턴스 API는 상당 부분이 콜백 개념을 중심으로 만들어져 있습니다. 이러한 콜백 중 다수는 save나 create 같은 모델 라이프사이클 이벤트에 반응해 실행됩니다. 이러한 콜백은 저장되거나 생성되는 인스턴스마다 호출되도록 만들어졌기 때문에 대량 삽입에는 사용할 수 없습니다. 레코드를 대량으로 삽입할 때는 이러한 이벤트가 발생하지 않으므로 사용을 막고 있습니다.

명시적으로 허용되는 콜백은 BulkInsertSafe 에 정의되어 있습니다. 자세한 내용은 모듈 소스 코드를 참고합니다. 안전하다고 명시되지 않은 콜백을 사용하는 클래스에 include BulkInsertSafe를 추가하면 애플리케이션은 오류와 함께 실패합니다.

BulkInsertSafe와 InsertAll 비교#

BulkInsertSafe는 내부적으로 InsertAll을 기반으로 하므로, 둘 중 무엇을 선택해야 할지 고민될 수 있습니다. 판단에 도움이 되도록 두 클래스의 주요 차이를 아래 표에 정리했습니다.

입력 타입 입력 유효성 검사 배치 크기 지정 콜백 우회 가능 트랜잭션
bulk_insert! ActiveRecord 객체 예 (선택 사항) 예 (선택 사항) 아니요 (안전하지 않은 콜백 사용 방지) 예
insert_all! 속성 해시 아니요 아니요 예 예

정리하면, BulkInsertSafe는 대량 삽입을 ActiveRecord 객체와 일반적인 삽입 동작에 더 가깝게 만듭니다. 다만 원시 데이터를 대량으로 삽입하기만 하면 되는 경우에는 insert_all이 더 효율적입니다.

has_many 연관 관계 대량 삽입#

흔한 사용 사례는 연관된 관계의 컬렉션을 관계의 소유자 쪽을 통해 저장하는 것이며, 소유된 관계는 has_many 클래스 메서드로 소유자와 연결됩니다:

owner = OwnerModel.new(owned_relations: array_of_owned_relations)
# saves all `owned_relations` one-by-one
owner.save!

이 방식은 owned_relations의 레코드마다 INSERT와 트랜잭션을 하나씩 발생시키므로 array_of_owned_relations가 크면 비효율적입니다. 이를 개선하려면 BulkInsertableAssociations concern으로 소유자가 대량 삽입에 안전한 연관 관계를 정의한다고 선언할 수 있습니다:

class OwnerModel < ApplicationRecord
  # other includes here
  # ...
  include BulkInsertableAssociations # include this last

  has_many :my_models
end

대량 삽입이 이루어지려면 여기서 my_models가 앞에서 설명한 대로 BulkInsertSafe로 선언되어 있어야 합니다. 이제 아직 저장되지 않은 레코드를 다음과 같이 삽입할 수 있습니다:

BulkInsertableAssociations.with_bulk_insert do
  owner = OwnerModel.new(my_models: array_of_my_model_instances)
  # saves `my_models` using a single bulk insert (possibly via multiple batches)
  owner.save!
end

이 블록 안에서 BulkInsertSafe가 아닌 관계도 저장할 수 있습니다. 이러한 관계는 블록 바깥에서 save를 호출한 것과 동일하게 처리됩니다.

알려진 제한 사항#

이러한 API의 사용 방식에는 몇 가지 제약이 있습니다:

  • BulkInsertableAssociations:
    • 현재 has_many 관계와만 호환됩니다.
    • has_many through: ... 관계는 아직 지원하지 않습니다.

또한 입력 데이터는 최대 1000개 정도의 레코드로 제한하거나, 대량 삽입을 호출하기 전에 미리 배치로 나누어야 합니다. INSERT 문은 단일 트랜잭션에서 실행되므로 레코드 수가 많으면 데이터베이스 안정성에 부정적인 영향을 줄 수 있습니다.