반응형
- Spring Data JPA의 saveAll()로 대량 데이터를 삽입할 때 예상과 다른 SQL이 실행되는 것을 발견
- 누가 배치 쿼리를 어떻게, 왜 분할하는지 코드를 찾아본 과정
예상 vs 실제 쿼리 실행
5개의 프로젝트 데이터를 saveAll()로 저장했을 때:
// 5개 엔티티를 한 번에 저장
List<Project> projects = generateProjects(5);
projectRepository.saveAll(projects);
예상한 쿼리:
# 예상했던 쿼리
INSERT INTO project (title,id) VALUES ($1,$2),($3,$4),($5,$6),($7,$8),($9,$10);
# 실제 실행된 쿼리
# - 4개 + 1개로 분할됨
INSERT INTO project (title,id) VALUES ($1,$2),($3,$4),($5,$6),($7,$8);
INSERT INTO project (title,id) VALUES ($1,$2);
# 6개 추가하면 - 4개 + 2개
# 16개 추가하면 - 16개
# 24개 추가하면 - 16개 + 8개
# ...
데이터 개수를 조정하며 확인해본 결과 2의 제곱수들의 합으로 분할되고 있었다.
코드 분석
insert 과정을 따라가보니 PgPrepraedStatement 클래스에서 쿼리를 분할하고 있었다.
- PreparedSatatement 인터페이스가 각 드라이버별로 구현되어있다.
- postgresql의 구현이 PgPrepraedStatement다.
스택 트레이스를 따라들어가며 분석해본 결과 PostgreSQL JDBC 드라이버의 PgPreparedStatement 클래스에서 분할이 일어나고 있었다.
핵심 분할 로직
javaprotected void transformQueriesAndParameters() throws SQLException {
final int bindCount = originalQuery.getBindCount();
final int highestBlockCount = 128;
// 최대 배치 크기 계산 (2의 제곱수로 제한)
final int maxValueBlocks = bindCount == 0 ? 1024
: Integer.highestOneBit(
Math.min(Math.max(1, maximumNumberOfParameters() / bindCount),
highestBlockCount)
);
int unprocessedBatchCount = batchParameters.size();
// 분할 개수 계산
final int fullValueBlocksCount = unprocessedBatchCount / maxValueBlocks;
final int partialValueBlocksCount = Integer.bitCount(unprocessedBatchCount % maxValueBlocks);
final int count = fullValueBlocksCount + partialValueBlocksCount;
// count만큼 쿼리 생성
for (int i = 0; i < count; i++) {
// 각 배치 쿼리 생성
}
}
- Integer.highestOneBit(50): 32 (2⁵)
- Integer.highestOneBit(100): 64 (2⁶)
왜 이렇게 설계했을까?
- PostgreSQL의 경우 쿼리당 바인드 가능 매개변수가 65535개로 제한된다. 즉 INSERT INTO VALUE(?,?)를 실행할 때 바인드되는 매개변수의 수를 초과하지 않도록 INSERT문을 분할하는 것이다.
- 또 위 코드에서 count를 계산하는 부분의 주석에 이런 내용이 있다.
- 단일 쿼리는 32767를 초과할 수 없습니다. 바인딩을 초과할 수 없으므로 다중 값 블록의 개수에 제한을 두어야 합니다.
- 일반적으로 128개 이상의 행을 일괄 처리하는 것은 큰 의미가 없습니다. : 그래서 final int highestBlockCount = 128; 라는 지역 변수를 사용한 것으로 보인다.
알아본 내용
- PostgreSQL JDBC 드라이버는 배치 쿼리를 2의 제곱수 단위로 분할
- 매개변수 제한과 성능 최적화를 위한 설계
- 배치 크기를 2의 제곱수로 설계하면 분할 오버헤드를 최소화할 수 있다.
- 안정성 > 약간의 성능 오버헤드라는 설계 철학
반응형