[JPA] 5개 데이터가 4+1개 쿼리로 나누어지다.

2025. 1. 19. 16:43·Spring Framework/JPA
반응형
  • 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의 제곱수로 설계하면 분할 오버헤드를 최소화할 수 있다.
  • 안정성 > 약간의 성능 오버헤드라는 설계 철학
반응형
저작자표시 비영리 변경금지 (새창열림)
'Spring Framework/JPA' 카테고리의 다른 글
  • [JPA] Batch Insert
  • [JPA] orphanRemoval, CasecaseType.REMOVE
덴마크초코우유
덴마크초코우유
IT, 알고리즘, 프로그래밍 언어, 자료구조 등 정리
    반응형
  • 덴마크초코우유
    이것저것끄적
    덴마크초코우유
  • 전체
    오늘
    어제
    • 분류 전체보기 (124)
      • Spring Framework (10)
        • Spring (5)
        • JPA (3)
        • Spring Security (0)
      • Language (51)
        • Java (11)
        • Python (10)
        • JavaScript (5)
        • NUXT (2)
        • C C++ (15)
        • PHP (8)
      • DB (16)
        • MySQL (10)
        • Reids (3)
        • Memcached (2)
      • 개발 (3)
      • 프로젝트 (2)
      • Book (2)
      • PS (15)
        • 기타 (2)
        • 백준 (2)
        • 프로그래머스 (10)
      • 딥러닝 (8)
        • CUDA (0)
        • Pytorch (0)
        • 모델 (0)
        • 컴퓨터 비전 (4)
        • OpenCV (1)
      • 기타 (16)
        • 디자인패턴 (2)
        • UnrealEngine (8)
        • ubuntu (1)
        • node.js (1)
        • 블로그 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    웹
    FPS
    Unreal Engine
    JS
    C++
    php
    블루프린트
    PS
    C
    게임 개발
    map
    딥러닝
    MySQL
    파이썬
    언리얼엔진4
    Python
    게임
    redis
    프로그래머스
    select
    CPP
    JavaScript
    클래스
    pytorch
    알고리즘
    자바
    mscoco
    memcached
    Unreal
    NUXT
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
덴마크초코우유
[JPA] 5개 데이터가 4+1개 쿼리로 나누어지다.
상단으로

티스토리툴바