Skip to content

Latest commit

ย 

History

History
196 lines (159 loc) ยท 9.37 KB

2022-11-08-bulk-update.md

File metadata and controls

196 lines (159 loc) ยท 9.37 KB

MySQL Batch Update ์„ฑ๋Šฅ ์ธก์ • ๋ฐ ๋ถ„์„

MySQL ๊ธฐ๋ฐ˜์œผ๋กœ ๋Œ€๋Ÿ‰ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ JPA, Exposed ํ”„๋ ˆ์ž„์›Œํฌ ๊ธฐ๋ฐ˜์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋ง์”€๋“œ๋ฆฌ๋ฉด Exposed ๊ธฐ๋ฐ˜ Batch Update๊ฐ€ ๊ฐ€์žฅ ๋นจ๋ž์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  JPA์—์„œ๋„ addBatch ๋ฐฉ์‹์„ ์ง„ํ–‰ํ•˜๋ฉด ์œ ์˜๋ฏธํ•œ ์†๋„ ์ฐจ์ด๋Š” ์—†์„ ๊ฒƒ ๊ฐ™์•„ ๋ณด์ด๋‚˜ Exposed๊ฐ€ addBatch ๊ธฐ๋Šฅ์„ ์ง๊ด€์ ์œผ๋กœ ์ง€์›ํ•˜๊ณ  ์žˆ์–ด addBatch ๋ฐฉ์‹์€ Exposed๋ฅผ ์‚ฌ์šฉํ–ˆ์œผ๋ฉฐ JPA๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์ธ Dirty Checking Update, ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ํ•„์š” ์—†๋Š” ID ๊ธฐ๋ฐ˜ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

Update With JPA

// JPA Object
@Entity
@Table(name = "writer")
class Writer(
    @Column(name = "name", nullable = false)
    var name: String,
    @Column(name = "email", nullable = false)
    var email: String,
) {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null
        internal set

    @CreationTimestamp
    @Column(name = "created_at", nullable = false, updatable = false)
    lateinit var createdAt: LocalDateTime
        internal set

    @UpdateTimestamp
    @Column(name = "updated_at", nullable = false)
    lateinit var updatedAt: LocalDateTime
        internal set
}

internal class WriterTest(
    private val writerService: WriterService
) : SpringBootTestSupport() {

    @Test
    internal fun `dirty checking update test`() {
        // ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
        val total = 500
        val map = (1..total).map {
            Writer(
                name = "old",
                email = "old"
            )
        }
        // ๋ฐ์ดํ„ฐ ์…‹์—…, ์†๋„ ์ธก์ • ํฌํ•จ X
        setup(map)
        // ๋ฐ์ดํ„ฐ ์กฐํšŒ, ์†๋„ ํŠน์ • X
        val writers = writerService.findAll()

        val stopWatch = StopWatch()
        // ์—…๋ฐ์ดํŠธ ์†๋„ ์ธก์ •
        stopWatch.start()
        writerService.update(writers)
        stopWatch.stop()

        println("${map.size}, ${stopWatch.lastTaskTimeMillis}")
    }

    @Test
    internal fun `none persistence context update test`() {
        // ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
        val total = 500
        val map = (1..total).map {
            Writer(
                name = "old",
                email = "old"
            )
        }
        // ๋ฐ์ดํ„ฐ ์…‹์—…, ์†๋„ ์ธก์ • ํฌํ•จ X
        setup(map)
        val findAll = writerService.findAll()

        // ์—…๋ฐ์ดํŠธ ์†๋„ ์ธก์ •
        val stopWatch = StopWatch()
        stopWatch.start()
        writerService.nonPersistContestUpdate(findAll.map { it.id!! })
        stopWatch.stop()

        println("${map.size}, ${stopWatch.lastTaskTimeMillis}")

    }
}

class WriterCustomRepositoryImpl : QuerydslCustomRepositorySupport(Writer::class.java), WriterCustomRepository {

    // ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ ์—†๋Š” ์—…๋ฐ์ดํŠธ
    @Transactional
    override fun update(ids: List<Long>) {
        for (id in ids) {
            update(qWriter)
                .set(qWriter.name, "update")
                .where(qWriter.id.eq(id))
                .execute()
        }
    }
}

JPA์—์„œ๋Š” Persistence Context ๊ธฐ๋ฐ˜์ธ Dirty Checking์„ ํ†ตํ•œ ์—…๋ฐ์ดํŠธ์™€, Persistence Context ์—†์ด ์ƒํƒœ์˜ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

Update With Exposed

class BatchInsertServiceTest(
    ...
) : ExposedTestSupport() {

    @Test
    fun `update`() {
        // ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
        val totalCount = 500
        val ids = (1..totalCount).map { it.toLong() }
        // ๋ฐ์ดํ„ฐ ์…‹์—…, ์†๋„ ์ธก์ • ํฌํ•จ X
        setup(ids)

        // ๋ฐ์ดํ„ฐ ์…‹์—…, ์†๋„ ์ธก์ • ํฌํ•จ X
        val stopWatch = StopWatch()
        stopWatch.start()
        for (writerId in ids) {
            Writers
                .update({ Writers.id eq writerId })
                {
                    it[email] = "update"
                }
        }
        stopWatch.stop()
        println("${ids.size} update, ${stopWatch.lastTaskTimeMillis}")
    }

    @Test
    fun `bulk update`() {
        // ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
        val totalCount = 500
        val ids = (1..totalCount).map { it.toLong() }
        // ๋ฐ์ดํ„ฐ ์…‹์—…, ์†๋„ ์ธก์ • ํฌํ•จ X
        setup(ids)

        // ์—…๋ฐ์ดํŠธ ์†๋„ ์ธก์ •
        val stopWatch = StopWatch()
        stopWatch.start()
        BatchUpdateStatement(Writers).apply {
            ids.forEach {
                addBatch(EntityID(it, Writers))
                this[Writers.email] = "update"
            }
        }
            .execute(TransactionManager.current())

        stopWatch.stop()
        println("${ids.size} update, ${stopWatch.lastTaskTimeMillis}")
    }
}

Exposed๋Š” ์ผ๋ฐ˜ ์—…๋ฐ์ดํŠธ์™€, addBatch๋ฅผ ํ†ตํ•œ batch update๋ฅผ ์ง„ํ–‰ ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

addBatch๋ž€ ?

JDBC ๋“œ๋ผ์ด๋ฒ„์—์„œ๋Š” addBatch()๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ rewriteBatchedStatements ์˜ต์…˜์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด MySQL Connector/J๊ฐ€ addBatch() ํ•จ์ˆ˜๋กœ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ชจ์•„ MySQL ์„œ๋ฒ„๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ Batch Insert๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ์˜ต์…˜์œผ๋กœ Batch Insert ์„ฑ๋Šฅ ํ–ฅ์ƒ๊ธฐ 1ํŽธ - With JPA, Batch Insert ์„ฑ๋Šฅ ํ–ฅ์ƒ๊ธฐ 2ํŽธ - ์„ฑ๋Šฅ ์ธก์ •์—์„œ ๋‹ค๋ฃฌ ์  ์žˆ์Šต๋‹ˆ๋‹ค. Insert ์ฟผ๋ฆฌ ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” addBatch()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๋ฌถ์–ด์„œ ์‹คํ–‰์‹œ์ผœ ์ค๋‹ˆ๋‹ค.

-- addBatch() ์‚ฌ์šฉ์‹œ ๋‹จ์ผ insert์—์„œ ์•„๋ž˜ SQL ํ˜•ํƒœ๋กœ ๋ณ€๊ฒฝ
insert into writer (`name`, `email`, `created_at`, `updated_at`)
values ('old', 'old', '2022-11-06 13:48:14.135442', '2022-11-06 13:48:14.135442'),
       ('old', 'old', '2022-11-06 13:48:14.135442', '2022-11-06 13:48:14.135442'),
        ...
       ('old', 'old', '2022-11-06 13:48:14.135442', '2022-11-06 13:48:14.135442');

Update ์ฟผ๋ฆฌ๋Š” ํ˜•์‹์˜ ๋ณ€๊ฒฝ์€ ์—†์ง€๋งŒ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ชจ์•„์„œ ํ•œ ๋ฒˆ์— MySQL ์„œ๋ฒ„๋กœ ์ „๋‹ฌํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Performance ์ธก์ • ๋ฐ ๋ถ„์„

rows JPA Dirty Checking Update JPA None Persistence Context Exposed Update Exposed Bulk Update
50 115 ms 167 ms 80 ms 23 ms
100 206 ms 242 ms 130 ms 40 ms
500 71 8ms 994 ms 596 ms 135 ms
1,000 1,388 ms 1,540 ms 1,130 ms 381 ms
5,000 6,204 ms 6,441 ms 5,121 ms 1,127 ms
10,000 12,151 ms 12,209 ms 10,094 ms 2,227 ms
50,000 65,309 ms 56,295 ms 46,506 ms 10,355 ms
100,000 120,906 ms 11,3194 ms 99,349 ms 21,370 ms

ํ•ด๋‹น ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์€ ๋กœ์ปฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋กœ์ปฌ MySQL ํ†ต์‹ ์œผ๋กœ ์ง„ํ–‰ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋„คํŠธ์›Œํฌ ๋ฆฌ์†Œ์Šค ๋น„์šฉ์ด ํฌ๊ฒŒ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜์Œ์—๋„ Exposed ๊ธฐ๋ฐ˜์˜ Batch Update ์„ฑ๋Šฅ์ด ๊ฐ€์žฅ ์ข‹์•˜์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฌผ๋ก  Exposed Bulk Update๋„ ์‹œ๊ฐ„์ด ๋” ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ฒ ์ง€๋งŒ ๋‹ค๋ฅธ ์—…๋ฐ์ดํŠธ ๋ฐฉ๋ฒ•๋“ค์€ ๋„คํŠธ์›Œํฌ ๋ฆฌ์†Œ์Šค๊ฐ€ ๋†’์•„์ง์— ๋”ฐ๋ผ ๋” ๋งŽ์€ ์‹œ๊ฐ„์ด ๋ฐœ์ƒํ•  ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  JPA์—์„œ๋Š” Dirty Checking Update, None Persistence Context์˜ ์„ฑ๋Šฅ ์ฐจ์ด๋Š” ์ƒ๊ฐ๋ณด๋‹ค ํฌ๊ฒŒ ๋ฐœ์ƒํ•˜์ง„ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋ฌผ๋ก  ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•˜๋‹ˆ ์กฐํšŒ์— ๋Œ€ํ•œ ๋ถ€๋ถ„๊นŒ์ง€ ํฌํ•จ ์‹œํ‚ค๋ฉด ์œ ์˜๋ฏธํ•œ ์ฐจ์ด๊ฐ€ ์žˆ์„ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๋Œ€๋Ÿ‰ ์กฐํšŒ์˜ ๊ฒฝ์šฐ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ํ†ตํ•˜์ง€ ์•Š๊ณ  Projections์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ด๋ผ ๊ทธ ๋ถ€๋ถ„๊นŒ์ง€ ํ…Œ์ŠคํŠธํ•˜์ง„ ์•Š์•˜์Šต๋‹ˆ๋‹ค. JPA ๊ธฐ๋ฐ˜์œผ๋กœ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ ๊ฐ€๋Šฅํ•˜๋ฉด Projections์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๋Ÿฐ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํŠน์„ฑ์ƒ ๋ฐฐ์น˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๊ตฌ์„ฑํ•˜๊ณ  Chunk ๋‹จ์œ„๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— 100,000 ์ •๋„์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ๊ถŒ์žฅํ•˜์ง„ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ชจ์ˆ˜์™€ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์— ๋Œ€ํ•œ ์ƒ๊ด€๊ด€๊ณ„๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์—…ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ์˜ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ๋น„์šฉ์— ๋”ฐ๋ผ์„œ addBatch() ๋ฐฉ์‹๊ณผ, ๊ทธ๋ ‡์ง€ ์•Š์€ ๋‹จ๊ฑด ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹์˜ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์€ ๋” ์ฐจ์ด๊ฐ€ ๋‚ ๊ฒƒ์œผ๋กœ ๋ณด์ด๋ฉฐ, ๊ตฌ์กฐ์ ์œผ๋กœ ํฐ ๋ณ€๊ฒฝ ์—†์ด ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹๋งŒ ๋ฐ”๊พธ๋Š” ๊ฒƒ์œผ๋กœ 6๋ฐฐ ๊ฐ€๊นŒ์šด ํ–ฅ์ƒ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋Œ€์šฉ๋Ÿ‰ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ๊ถŒ์žฅ ๋“œ๋ฆฝ๋‹ˆ๋‹ค. JPA๋Š” ์ •๋ง ์ข‹์€ ORM ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ƒ๊ฐ์ด ๋“ค์ง€๋งŒ ๋Œ€๋Ÿ‰ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๋„๊ตฌ๋กœ๋Š” ์ ์ ˆํ•˜์ง€ ์•Š๋‹ค๋Š” ์ƒ๊ฐ์ด ๋งŽ์ด ๋“ญ๋‹ˆ๋‹ค. MySQL ๊ธฐ๋ฐ˜์˜ ๋Œ€์šฉ๋Ÿ‰ ์ฒ˜๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ ๋‹ค๋ฅธ ์ ์ ˆํ•œ ๋„๊ตฌ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ์ด ์ข‹์„ ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค.