Search

Index를 이용한 DB 성능 개선 일지

주제
Database
날짜
2022/12/19

Index를 이용한 통계 데이터 GET API 개선

저번 시간에 검색 성능을 개선했지만 여전히 k6의 http_req_duration이 약 3초 정도 걸린다.
이를 해결하기 위해 API 성능을 다 조사해본 결과 통계 데이터를 가져오는 API가 상당히 오래 걸리는 것을 확인했다.

기존 방식

const statistics = await this.statisticsRepository.find({ where: { gender, weight: betweenWeight }, order: { SBD_volume: "ASC" }, }); ...
TypeScript
복사
SELECT * FROM `SBD_statistics` `SBD_statistics` WHERE (`SBD_statistics`.`gender` = 0 AND `SBD_statistics`.`weight` BETWEEN 93.01 AND 105)
TypeScript
복사
사용자의 성별이 남성, 체중이 100kg 일 경우, 성별이 남자면서 체중이 93~105kg 인 모든 사용자의 3대 운동 총량을 가져옵니다. 그 후에, 3대 운동 총량을 기준으로 정렬합니다.
하지만 이럴 경우 체중이 93~105kg인 사용자들이 테이블에 여기저기 흩어져 있기 때문에 Full Table Scan을 통해 모든 사용자 정보를 가져옵니다.
Query cost: 13815.59

개선 방법

아이디어

처음에는 체중을 정렬 시킨 상태로 between으로 가져오는 것이 휠씬 빠를 것이라 생각했다.
between 93~105를 Index Range Scan으로 쭉 가져오는게 빠르고, 후에 정렬을 안해줘도 되기 때문이고 생각했다.

Single Column Index (weight)

하지만 실험 결과 성별로 인해 Index Range Scan을 과도하게 길게 한다는 것을 파악했다.
Query cost: 10190.21
Full Table Scan보단 나아졌지만 여전히 느린 모습.

Single Column Index (gender, weight)

오히려 성별을 인덱싱 시킨 상태로 가져오는 것이 휠씬 빠른 모습을 확인했다.
non-unique index로 가져오기 때문인거같다. 하지만 성별의 경우 0, 1 두개로만 나뉘는데 아무리 non-unique index라지만 이게 정말 빠를까라는 생각을 했다.
Query cost: 6951.00

Multi Column Index (gender, weight)

그래서 성별과 체중의 Multi Column Index을 이용해봤다.
ALTER TABLE SBD_statistics ADD INDEX gender_weight_idx (gender, weight);
SQL
복사
const statistics = await this.statisticsRepository.find({ where: { gender, weight: betweenWeight }, // order: { SBD_volume: "ASC" }, index로 인해 정렬이 필요 없어진다. }); ...
TypeScript
복사
Query cost: 3253.21
하지만 실험 결과, 가져오는 데이터의 rows가 많아질수록 속도가 급격히 느려지는 것을 확인할 수 있었다.
심지어 가져오는 데이터의 rows가 일정 수치를 넘어갈 경우 Full Table Scan으로 가져오는 것을 확인할 수 있다.
gender
weight
query cost
rows
duration
Scan Type
0
59~66kg
3253
2.32k
0.0047
Index Range
0
66~74kg
6919
4.94k
0.0088
Index Range
0
74~83kg
9728
6.94k
0.0117
Index Range
0
83~93kg
10459
7.47k
0.0135
Index Range
0
93~105kg
8704
6.22k
0.0120
Index Range
0
105~120kg
5525
3.95k
0.0069
Index Range
0
83~105kg
13097
64.68k
0.0356
Full Table
Query cost: 13097

Multi-Block Read

왜 Full Table Scan으로 가져올까?
알아본 결과 Multi-Block Read 관련 이슈였다.
특정 call이 일어나면 디스크에서 Block들을 읽어보고, 버퍼 캐시에 저장한다. 하지만 읽어올 때 Block을 하나씩 읽어오는 건 아니고 경우에 따라 다르다.
Single-Block은 한번의 call에서 하나의 데이터 Block만 읽어와 메모리에 적재하는 방법이다. 인덱스를 통한 table access일 때 일어난다.
Multi-Block은 한번의 call에서 인접한 블록들을 다 같이 읽어버린다. extent 범위(OS가 지원하는 최대 범위는 1MB)내면 다 읽어 들인다.
DB의 Block은 일반적으로 8KB이고 Multi-Block Read는 한번에 1MB(128개의 8KB Block)을 읽어오는 반면에 Single-Block Read은 8KB → 8KB → 8KB 이런식으로 하나씩 조회하기 때문에 느리다.
Index Scan으로 작은 용량의 데이터를 가져올 땐 Index를 이용해 실제 row가 포함된 하나의 데이터베이스 Block만 조회하기 때문에 상관없지만, 대용량 데이터를 가져올 땐 여러 Block을 조회하기 때문에 느리다.
요약
Full Table ScanMulti-Block Read가 가능하지만 Index Full ScanLeaf Block을 일일이 방문하기 때문에 Single-Block Read 밖에 할 수 없다. 그래서 때로는 Full Table Scan의 성능이 더 좋을 때가 있다고 한다.

실험 결과

Query cost

No Index → Single Column Index (gender)
13815.59 → 6951.00
약 49.6%%의 성능 향상
Query cost: 13815.59
Query cost: 6951.00

k6 (VUs : 150)

No Index
http_req_duration
avg = 293.31ms
p(90) = 972.96ms
p(95) = 1.87s
Multi Column Index (gender, weigh)
http_req_duration
avg = 147ms
p(90) = 438.79ms
p(95) = 950.12ms
Single Column Index (gender)
http_req_duration
avg = 3.33s
p(90) = 12.95s
p(95) = 21.29s
Single Column Index (gender, weight)
http_req_duration
avg = 186.6ms
p(90) = 517.02ms
p(95) =1.24s
http_req_duration 요약 (No Index → Multi Column Index)
avg
293.31ms → 147ms
약 49.8%의 성능 향상
p(90)
972.96ms → 438.79ms
약 54.9%의 성능 향상
p(95)
1.87s → 950.12ms
약 49.1%의 성능 향상
http_req_duration 요약 (Single Column Index → Multi Column Index)
avg
186.6ms → 147ms
약 21.2%의 성능 향상
p(90)
517.02ms → 438.79ms
약 15.1%의 성능 향상
p(95)
1.24s → 950.12ms
약 23.3%의 성능 향상

결론

Multi Column Index를 이용해 Index Range Scan을 하는 경우가 특정 조건에선 빠르지만, 추후 통계 데이터가 계속 쌓이며 데이터의 수가 많아질수록 Multi Column Index가 불리하다고 생각된다.
그러므로 데이터 수와 상관 없이 항상 일정한 속도를 보장하는 Single Column Index(gender, weight)가 타당하지만 데이터 수가 적은 현재로서는 Multi Column Index(gender, weight)가 Single Column Index보다 약 15~23%가량 빠르므로 Multi Column Index를 채택했다.
추후 서비스가 확장되어 데이터가 많아질 경우 Single Column Index를 고려해볼 수 있을 꺼 같다.