사붐이개발일기

[MyBatis] Batch Insert - 대용량 데이터 insert 본문

Spring/Mybatis

[MyBatis] Batch Insert - 대용량 데이터 insert

sabeom 2024. 5. 2. 13:59

 

내용

1. SqlSession 의 ExecutorType.BATCH
ExecutorType.BATCH 를 사용하면 바로 DB 로 쿼리를 날리지 않고 쿼리를 미리 쌓아뒀다가 commit 시 connection에 모든 쿼리를 넘겨준다.
2. flushStatements
MyBatis에서는 flushStatements라는 설정 옵션을 제공합니다. 이 옵션을 활용하면 일정량의 SQL 문을 배치 처리하고 커밋하기 전에 임시로 버퍼에 저장하는 방식으로 대용량 데이터 처리 작업의 성능을 개선할 수 있습니다. 대용량 데이터 작업을 수행하는 동안 많은 SQL문을 생성하고 실행할 수 있습니다. 이때 flushStatement를 사용하면, 일정한 개수나 시간 간격으로 SQL 문을 배치로 처리하고 커밋하므로 다음과 같은 장점이 있습니다.

  • 메모리 사용 최적화: 많은 SQL 문을 한 번에 메모리에 보관하지 않고 일부분씩 처리하므로 메모리 부하가 줄어듭니다.
  • 작업 속도 향상: 일정한 간격마다 SQL문을 배치로 처리하기 때문에 작업이 빨라집니다.
  • 트랜잭션 관리: 일정한 간격마다 커밋하므로 큰 트랜잭션 범위에서 발생하는 장애의 영향을 줄일 수 있습니다.

 

예시

1. 기본 insert

<!-- 데이터를 한 건씩 Insert 하는 쿼리 -->
<insert id="insertItem" parameterType="com.example.batchjob.dto.ItemDto">
    INSERT INTO item_list
    VALUES ( #{itemCode}, #{name}, #{kindCode}, #{price})
</insert>

 
 

INSERT INTO item_list
        VALUES ( '0', '0', '0', 0)
 {executed in 6 msec}
INSERT INTO item_list
        VALUES ( '1', '1', '1', 1)
 {executed in 5 msec}
INSERT INTO item_list
        VALUES ( '2', '2', '2', 2)
 {executed in 4 msec}
INSERT INTO item_list
        VALUES ( '3', '3', '3', 3)
 {executed in 4 msec}
INSERT INTO item_list
        VALUES ( '4', '4', '4', 4)
 {executed in 4 msec}

2. Mybatis <foreach> 태그사용

<!-- foreach를 이용한 다건의 데이터 Insert -->
<insert id="insertItemList" parameterType="com.example.batchjob.dto.ItemDto">
    INSERT INTO item_list
    VALUES
    <foreach collection="list" item="item" open="(" close=")" separator="), (">
        #{item.itemCode}, #{item.name}, #{item.kindCode}, #{item.price}
    </foreach>
</insert>

하나의 insert문으로 다 건의 데이터가 입력된다.

INSERT INTO item_list
    VALUES
        ( '0', '0', '0', 0 )
        , ( '1', '1', '1', 1)
        , ( '2', '2', '2', 2)
        , ( '3', '3', '3', 3)
        , ( '4', '4', '4', 4)
 {executed in 13 msec}

3. SqlSession ExcuteType.BATCH 사용

// mapper.XML
<!-- 데이터를 한 건씩 Insert 하는 쿼리 -->
<insert id="insertItem" parameterType="com.example.batchjob.dto.ItemDto">
    INSERT INTO item_list
    VALUES ( #{itemCode}, #{name}, #{kindCode}, #{price})
</insert>
@Test
@DisplayName("SqlSession Execute.BATCH 를 이용한 배치처리")
void BulkInsertUsingSqlSession() {

    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
    ItemMapper mapper = sqlSession.getMapper(ItemMapper.class);

    for (ItemDto itemDto : inputDataList) {
        retValue = mapper.insertItem(itemDto); // 리턴값으로 row와 상관없는 값이 나옴
    }
    sqlSession.flushStatements();
    sqlSession.Commit()
}
batching 5 statements:
1:  INSERT INTO item_list#
        VALUES ( '0', '0', '0', 0)
2:  INSERT INTO item_list
        VALUES ( '1', '1', '1', 1)
3:  INSERT INTO item_list
        VALUES ( '2', '2', '2', 2)
4:  INSERT INTO item_list
        VALUES ( '3', '3', '3', 3)
5:  INSERT INTO item_list
        VALUES ( '4', '4', '4', 4)
 {executed in 13 msec}

4. 각 방식의 성능비교
sqlSession batch   <   mybatis <forEach>   <   기본 insert 순으로 속도가 느려짐
 
 
 

내가 사용한 방법 !

  • 환경
    • java 8
    • mybatis
    • postgreSQL
  • 순서
    1. sqlSession ExecutorType.BATCH 선언
    2. batchMapper 선언
    3. batch insert할 임시테이블 생성 (temporary table)
    4. batch insert 하기 전 가맹점 마다 임시테이블 데이터 모두 삭제 (truncate)
    5. batch insert
    6. batch insert를 한꺼번에 저장시 속도가 떨어지므로 1000건씩 끊어 넣기 (1000건이 퍼포먼스 제일 높게 나옴)
    7. 반드시 finally 에 sqlSession을 닫는 기능을 넣어야함. 오류가 나더라도 finally는 실행됌.
public class service {
    private final SqlSessionFactory sqlSessionFactory;
    private final PlatformTransactionManager transactionManager;
    private final SmartroPayRepo smartroPayRepo;
    private final int BATCH_SIZE = 1000;  // batch insert size 1000건씩 끊어 넣기

    
    public Boolean insertAdjmData(AdjmResVO adjmResVO, String payDate) {
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        SmartroPayRepo batchMapper = sqlSession.getMapper(SmartroPayRepo.class);
        /* Transaction Start */
        // TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            /* 임시테이블 생성 */
            // smartroPayRepo.createTempMpayAdjm();
            batchMapper.createTempMpayAdjm();	// create temporary table
            sqlSession.flushStatements();	// commit
        } catch (Exception e) {
            log.info("already createTempMpayAdjm On insertAdjmData()");
        }

        try {
            /* 임시테이블에 있는 데이터 모두 삭제 */
            // sqlSession.update("com.bit.bizportal.admin.modules.smartroPay.SmartroPayRepo.truncateTempMpayAdjm");
            batchMapper.truncateTempMpayAdjm();	// truncate temporary table

            int batchInsertCount = 0;

            List<AdjmDataVO> adjmList = batchMapper.selectAdjmList();
            for (AdjmDataVO adjmDataVO : adjmList) {
                // sqlSession.insert("com.bit.bizportal.admin.modules.smartroPay.SmartroPayRepo.insertTempPadjData", adjmDataVO);
                batchMapper.insertTempPadjData(adjmDataVO);	// batch insert

                /* BATCH_SIZE 만큼 끊어 넣기 (1000건) */
                batchInsertCount++;
                if (batchInsertCount % BATCH_SIZE == 0) {
                    sqlSession.flushStatements();	// commit
                    batchInsertCount = 0;
                }
            }

            // sqlSession.insert("com.bit.bizportal.admin.modules.smartroPay.SmartroPayRepo.insertPadjData");
            batchMapper.insertPadjData();	// batch insert
            sqlSession.flushStatements();	// commit
            /* Transaction Commit */
            // transactionManager.commit(status);
        } catch {
        /* Transaction Rollback */
        // transactionManager.rollback(status);
        } finally {
            sqlSession.flushStatements();
            sqlSession.close();	// SqlSession 종료
            sqlSession.clearCache();	// SqlSession 리소스 반납
        }

        return true;
    }
}

 
mapper.xml
insert, update, select 등 기존 방식과 동일하게 사용하면 됌

// mapper.xml
<!--    create temporary table, 임시테이블 생성 -->
<update id="createTempMpayAdjm">
    CREATE temp table tmp_mpay_adjm (
    padj_id int,
    padj_mpay_id int,
    padj_sadj_id int,
    padj_reg_dttm timestamp,
    padj_updt_dttm timestamp,
    tid text,
    otid text        
    );
</update>

<!-- truncate table, 테이블 내 모든 데이터 삭제 -->
<update id="truncateTempMpayAdjm">
    TRUNCATE TABLE tmp_mpay_adjm;
</update>

 
 
 
 
 


Reference

1. mybatis 대량 데이터 INSERT - sqlSessionFactory 이용!
https://jy-note.tistory.com/26
2. [bulk insert/update] mybatis 배치작업과 리턴값(returnValue)
https://velog.io/@aszxvcb/Bulk-InsertUpdate-%EB%B0%B0%EC%B9%98%EC%9E%91%EC%97%85%EC%97%90%EC%84%9C%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%B2%98%EB%A6%AC-%EB%A6%AC%ED%84%B4%EA%B0%92returnValue
3. myBatis에서 대량의 데이터를 넣을때 bulk insert를 사용하자
https://javairus.tistory.com/18
4. [Oracle] Spring, Mybatis에서 대량 데이터 INSERT 수행. multirow, 다건 삽입 수행하기
https://infjin.tistory.com/192