테이블에 일괄 삽입하기
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의 크기 및 비용 사이에는
트레이드오프가 있습니다.
중복 레코드 처리#
이 파라미터는 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 문은 단일 트랜잭션에서 실행되므로 레코드 수가
많으면 데이터베이스 안정성에 부정적인 영향을 줄 수 있습니다.