Post

๐ŸŽฑ MySQL ์ธ๋ฑ์Šค ์‹ค์Šต - ์ „๋ฌธ ์ธ๋ฑ์Šค๋กœ B-tree ์ธ๋ฑ์Šค์—์„œ ์ปค๋ฒ„ ๋ชปํ•˜๋Š” ๋ถ€๋ถ„ ๋ณด์™„ํ•˜๊ธฐ

๐ŸŽฑ MySQL ์ธ๋ฑ์Šค ์‹ค์Šต - ์ „๋ฌธ ์ธ๋ฑ์Šค๋กœ B-tree ์ธ๋ฑ์Šค์—์„œ ์ปค๋ฒ„ ๋ชปํ•˜๋Š” ๋ถ€๋ถ„ ๋ณด์™„ํ•˜๊ธฐ

๐Ÿ–ค Intro

์ €๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” B-Tree ์ธ๋ฑ์Šค๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•ด์„œ ์–ด๋А์ •๋„์˜ ์„ฑ๋Šฅ ์ฐจ์ด๋ฅผ ๋งŒ๋“ค์–ด ๋‚ผ ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ ์ง์ ‘ ์‚ดํŽด๋ณด์•˜๋‹ค. ์ด๋ฒˆ ์‹œ๊ฐ„์—๋Š” ์ „๋ฌธ ์ธ๋ฑ์Šค (Full Text Index)๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜๊ณ , ์–ด๋А์ •๋„์˜ ์„ฑ๋Šฅ ์ฐจ์ด๊ฐ€ ๋‚˜๋Š”์ง€๋ฅผ ์ง์ ‘ ์‚ดํŽด๋ณด๋„๋ก ํ•˜์ž.

๐Ÿฉถ Start

์ „๋ฌธ ์ธ๋ฑ์Šค ์‹ค์Šต์„ ์œ„ํ•œ ์ธ๋ฑ์Šค ์ƒ์„ฑ

1
ALTER TABLE logs ADD FULLTEXT INDEX ft_idx_message (message) WITH PARSER ngram;

์ „๋ฌธ ๊ฒ€์ƒ‰์„ ์œ„ํ•ด์„œ๋Š” โ€œ์–ด๋–ค ์ปฌ๋Ÿผโ€์— FULLTEXT INDEX๋ฅผ ์ƒ์„ฑํ• ์ง€๋ฅผ ์ธ๋ฑ์Šค ์ƒ์„ฑํ•ด์•ผ ๊ฒ€์ƒ‰์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ฐธ๊ณ ๋กœ ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ n-gram์„ ์ด์šฉํ•˜๋„๋ก ํ•˜์ž.

์ „๋ฌธ์ธ๋ฑ์Šค.png

DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * full index test๋ฅผ ์œ„ํ•œ ์‘๋‹ต์„ ๋‹ด์€ ๊ฐ„๋‹จํ•œ dto
 */
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FullIndexPerfomanceTestResponse {
    // ๊ฒ€์ƒ‰ ์†Œ์š” ์‹œ๊ฐ„
    private Long likeSearchTime;           // LIKE '%keyword%' ๊ฒ€์ƒ‰ ์‹œ๊ฐ„
    private Long fullTextSearchTime;       // MATCH AGAINST ๊ฒ€์ƒ‰ ์‹œ๊ฐ„

    // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
    private Integer likeResultCount;       // LIKE ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜
    private Integer fullTextResultCount;   // ์ „๋ฌธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ฐœ์ˆ˜

    // ์„ฑ๋Šฅ ๋น„๊ต
    private Double performanceImprovement; // ์„ฑ๋Šฅ ํ–ฅ์ƒ ๋น„์œจ (LIKE ์‹œ๊ฐ„ / FULL TEXT ์‹œ๊ฐ„)

    // ํ…Œ์ŠคํŠธ ์ •๋ณด
    private String testDescription;        // "์ธ๋ฑ์Šค ์ „" or "์ธ๋ฑ์Šค ํ›„"
    private String keyword;                // ๊ฒ€์ƒ‰์–ด
}

๊ฒฐ๊ตญ ์ „๋ฌธ๊ฒ€์ƒ‰์„ ํ•˜๋Š” ๋ชฉ์ ์€, LIKE๋กœ ์ „์ฒด ํ…Œ์ด๋ธ”์„ FULL SCAN ํ•˜๋Š” ๊ฒƒ๊ณผ ์–ด๋А์ •๋„์˜ ์ฐจ์ด๊ฐ€ ๋‚˜๋Š”์ง€๋ฅผ ๋น„๊ตํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค.

์ด๋ฅผ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•„๋“œ ๊ฐ’๋“ค์„ ์ƒ์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

Repository

1
2
3
4
5
6
// ์ „๋ฌธ ๊ฒ€์ƒ‰
// mysql ์ „์šฉ ํ•จ์ˆ˜๋ผ querydsl๋กœ๋Š” ๊ตฌํ˜„์ด ์–ด๋ ค์›Œ์„œ, ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ๋กœ ์ž‘์„ฑ
@Query(value = "SELECT * FROM logs " +
        "WHERE MATCH(message) AGAINST(:keyword IN BOOLEAN MODE)",
        nativeQuery = true)
List<Logs> searchByFullText(@Param("keyword") String keyword);

mysql ์ „์šฉ ํ•จ์ˆ˜๋Š” ๊ธฐ๋ณธ querydsl ๋ฌธ๋ฒ•์œผ๋กœ๋Š” ์ƒ์„ฑ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜์—ฌ, ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ๋กœ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๋‹ค.

Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * ์ „๋ฌธ ์ธ๋ฑ์Šค์˜ ์ •ํ™•ํ•œ ์„ฑ๋Šฅ์„ ๊ฒ€์ˆ˜ํ•˜๊ธฐ ์œ„ํ•œ method
 */
public FullIndexPerfomanceTestResponse fullIndexPerfomanceTest(String keyword){
    long startTime; //์„ฑ๋Šฅ ์ธก์ •์„ ์œ„ํ•œ ์‹œ์ž‘ ์‹œ๊ฐ„ ๋ช…์‹œ

    entityManager.clear(); // ๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ๋…๋ฆฝ์  ์ธก์ •์„ ์œ„ํ•ด clear ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
    startTime = System.currentTimeMillis();
    List<Logs> likeSearchLogs = logRepository.findByMessageContaining(keyword); // LIKE '%keyword%' ์ฒดํฌ๋ฅผ ์œ„ํ•ด Containing์œผ๋กœ ํ•œ๋‹ค.
    long likeSearchTime = System.currentTimeMillis() - startTime;

    entityManager.clear(); // ๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ๋…๋ฆฝ์  ์ธก์ •์„ ์œ„ํ•ด clear ํ•ด์ค˜์•ผ ํ•œ๋‹ค.
    startTime = System.currentTimeMillis();
    List<Logs> fullTextLogs = logRepository.searchByFullText(keyword);
    long fullTextSearchTime = System.currentTimeMillis() - startTime;

    // (์ด์ „ ์‹œ๊ฐ„ - ํ˜„์žฌ ์‹œ๊ฐ„) / ์ด์ „ ์‹œ๊ฐ„ * 100
    double improvement = ((double)(likeSearchTime - fullTextSearchTime)
            / likeSearchTime) * 100;

    return FullIndexPerfomanceTestResponse.builder()
            .keyword(keyword)
            .likeSearchTime(likeSearchTime)
            .likeResultCount(likeSearchLogs.size())
            .fullTextSearchTime(fullTextSearchTime)
            .fullTextResultCount(fullTextLogs.size())
            .performanceImprovement(improvement)
            .testDescription("ํ˜„์žฌ ์ „๋ฌธ ์ธ๋ฑ์Šค ์ƒํƒœ")
            .build();
}

์„œ๋น„์Šค ๊ตฌ์„ฑ์€ ์ด์ „์— b-tree ์„ฑ๋Šฅ ์ธก์ • ํ–ˆ๋˜๊ฒƒ๊ณผ ๋น„์Šทํ•˜๋‹ค.

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, rest api ์„ค๋ช…ํ•˜๋Š” ๋‹จ๊ณ„๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ ์ž์„ธํ•œ ์„ค๋ช…์€ ์ƒ๋žตํ•œ๋‹คโ€ฆ

๊ฒฐ๊ณผ ๋ถ„์„

1
http://localhost:8085/api/logs/performance/full?keyword=894
1
2
3
4
5
6
7
8
9
{
    "likeSearchTime": 187,
    "fullTextSearchTime": 40,
    "likeResultCount": 300,
    "fullTextResultCount": 300,
    "performanceImprovement": 78.6096256684492,
    "testDescription": "ํ˜„์žฌ ์ „๋ฌธ ์ธ๋ฑ์Šค ์ƒํƒœ",
    "keyword": "894"
}

894๋ฅผ ํ‚ค์›Œ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ๋ฅผ postman์œผ๋กœ ๋ฝ‘์•„๋‚ธ ๊ฒƒ์ด๋‹ค.

likeSearchTime, ์ฆ‰ LIKE ์ฟผ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜๋Š” Contains์— ๋น„ํ•ด full scan์ด ๋ฌด๋ ค 78%(4.7๋ฐฐ)๋‚˜ ๋น ๋ฅด๋‹ค!!

187ms > 40ms ์˜ ์„ฑ๋Šฅ ์ฐจ์ด๋กœ, ๋ฐ์ดํ„ฐ๊ฐ€ ๋” ๋งŽ์•„์ง€๊ณ , ํ‚ค์›Œ๋“œ๊ฐ€ ๋” ๋Š˜์–ด๋‚œ๋‹ค๋ฉด ๋” ํฐ ๊ฒฉ์ฐจ๊ฐ€ ๋ณด์ผ ๊ฒƒ์ด๋‹ค.

๊ณผ์—ฐ ์™œ ์ธ๋ฑ์Šค๋ฅผ ์“ฐ๋Š”์ง€ ์•Œ๊ฒ ๋‹ฌ๊นŒโ€ฆ..

1
http://localhost:8085/api/logs/performance/full?keyword=๋กœ๊ทธ ๋ฉ”์„ธ์ง€
1
2
3
4
5
6
7
8
9
{
    "likeSearchTime": 2414,
    "fullTextSearchTime": 11985,
    "likeResultCount": 100000,
    "fullTextResultCount": 100000,
    "performanceImprovement": -396.4788732394366,
    "testDescription": "ํ˜„์žฌ ์ „๋ฌธ ์ธ๋ฑ์Šค ์ƒํƒœ",
    "keyword": "๋กœ๊ทธ ๋ฉ”์„ธ์ง€"
}

์ž, ์—ฌ๊ธฐ์„œ ํŠน์ดํ•œ ์ ์ด ์žˆ๋‹ค.

์ง€๊ธˆ ์ „๋ฌธ ์ธ๋ฑ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” message ์ปฌ๋Ÿผ์˜ ๊ฒฝ์šฐ, ๋ชจ๋“  10๋งŒ๊ฐœ์˜ ์ปฌ๋Ÿผ์ด ์ „๋ถ€ ์ € โ€œ๋กœ๊ทธ ๋ฉ”์„ธ์ง€โ€๋ผ๋Š” ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ ๋„ˆ๋ฌด ๋งŽ์€ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์„ ๊ฒฝ์šฐ ์ธ๋ฑ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด 300% ๋‚˜โ€ฆ.๋” ๋А๋ฆฌ๋‹ค.

์•ž์—์„œ ์ธ๋ฑ์Šค๋Š” ์ƒ์„ฑ ๋น„์šฉ์ด ํฌ๊ธฐ ๋•Œ๋ฌธ์—, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์ „์ฒด์˜ 25%๊ฐ€ ๋„˜์–ด๊ฐˆ ๊ฒฝ์šฐ ์ฐจ๋ผ๋ฆฌ full table scan์ด ๋” ๋‚ซ๋‹ค๊ณ  ๋งํ–ˆ๋˜ ๊ฒƒ์„ ๊ธฐ์–ตํ•˜๋Š”๊ฐ€? ๊ทธ๊ฒŒ ๋ฐ”๋กœ ์ด๋Ÿฌํ•œ ์ด์œ  ๋•Œ๋ฌธ์ด๋‹ค.

๐ŸŒŸ ๊ทธ๋ž˜์„œ ์ธ๋ฑ์Šค๋ฅผ ์‚ฌ์šฉํ• ๋•Œ๋Š”, ์ธ๋ฑ์Šค๊ฐ€ ๊ณผ์—ฐ ๊ฒฐ๊ณผ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๋„์ถœํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€?๋ฅผ ์ž˜ ์ƒ๊ฐํ•ด์•ผ ํ•œ๋‹ค!!

This post is licensed under CC BY 4.0 by the author.