| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 31 |
- 2024회고
- MySQL
- 직장인 회고
- proxyFactory
- Test code
- Junit 5
- mapstruct
- Test Doulbe
- 소프티어
- Test
- db
- Coputer Science
- Service 계층 테스트
- modelmapper
- Server
- JPA
- 인프콘2023
- Spring
- enumSet
- 일상
- 2025 계획
- softeer
- parallelconsumer
- 테크쇼
- FCM
- OS
- 자바
- ExceptionResolver
- 공룡책
- Java
- Today
- Total
공부내용공유
MySQL index 설계, 튜닝시 고려사항 (covering, index dive, prefix index) 본문
MySQL index 설계, 튜닝시 고려사항 (covering, index dive, prefix index)
forfun 2024. 2. 25. 21:56서론
어플리케이션의 성능을 고려했을 때 알고리즘, 쿼리 자체도 영향을 많이 끼치지만 인덱스는 단연 굉장히 높은
비중을 차지한다.
인덱스 설계, 튜닝을 할 때 어떤 부분들을 고려해야 하고 개선할 수 있는지 공부하기 위해
인프런의 MySql 성능 최적화 라는 강의와 MySQL 도큐먼트를 공부하고 정리하기 위해 이 글을 작성하였다.
(해당 강의에는 다양한 최적화 방법을 다루고 있지만 이 글에서는 index 파트에 대해 다룰 것이다.)
본론
카디널리티 확인 방법
카디널리티는 다들 잘 알고있겠지만 다시 한번 정의를 설명하자면 특정 데이터 집합에서 유니크한 값의 개수이다.
그냥 특정 컬럼의 카디널리티는 select count(distinct(column)) from table 이러한 쿼리로 구할 수 있다.
만약 인덱스를 생성하게 되고 (보통 복합 인덱스) 해당 인덱스의 카디널리티를 확인하고 싶다면
인덱스 생성이 안되있을 경우
SELECT *
FROM mysql.innodb_index_stats
WHERE `database_name` = 'test'
AND `table_name` = 'orders';
인덱스 생성을 했을 경우
show index from orders;
이런식으로 확인할 수 있다.
복합 인덱스를 설계할 때 고려해야 하는 요소
높은 카디널리티를 선행으로 배치하는 것도 일반적으로 좋은 방식이지만 아래 사항들도 같이 고려를 해야한다.
- 자주 사용하는 쿼리들의 컬럼인가
- 조인에 사용되는 컬럼인가
- 인덱스의 선행 칼럼이 범위 기반의 쿼리로 많이 사용되는가
- 슬로우 쿼리에 사용되는 컬럼들
이 중 범위 기반으로 사용되는 필드가(order_date) 선행 칼럼으로 사용되었을 때, 비교적 후행 칼럼으로 사용되었을 때를 직접 비교해보자.
데이터 셋은 카디널리티를 유사하게 하기 위해 날짜는 1000년으로 범위를 만들었고 사람수도 3000명을 기준으로 100만개의 데이터를
생성했다. (보통은 user_id가 카디널리티가 높지 않을까)
//실행 계획문
explain analyze select * from orders
where customer_id = 3905
and order_date > '2023-11-04'
order by order_date desc
limit 10;
//범위 인덱스 선행
create index idx_order_date_customer_id on orders(order_date, customer_id);
-> Limit: 10 row(s) (cost=100600 rows=10) (actual time=140..140 rows=0 loops=1)
-> Index range scan on orders using idx_order_date_customer_id over ('2020-11-04' < order_date)
//범위 인덱스 후행
create index idx_customer_id on orders(customer_id, order_date);
-> Limit: 10 row(s) (cost=0.71 rows=1) (actual time=0.0153..0.0153 rows=0 loops=1)
-> Index range scan on orders using idx_customer_id over (customer_id = 3905 AND '2020-11-04' < order_date)
당연히 기간의 범위에 따라 달리지고 데이터에 따라 달라지겠지만 order_date가 카디널리티가 10배가 높은데도 불구하고 해당 예시에서도 굉장히 큰 차이가 있음을 볼 수 있다.
무조건 카디널리티가 높은 컬럼을 선행으로 하는 것이 아닌 다양한 상황들을 고려하여 인덱스를 설계해야함을 알 수 있다.
커버링 인덱스
인덱스를 사용해서 조회할 때 필요한 값들이 모두 인덱스 테이블에 존재한다면 추가적인 데이터 접근을 할 필요가 없어진다.
이러한 조회 쿼리의 모든 필드를 가지고 있는 인덱스를 커버링 인덱스라고 한다.
간단하게 일반 인덱스 사용과, 커버링 인덱스 사용을 비교해 보자 (해당 데이터는 다른 차이날 조건을 없애기 위해 customer_id를
제외하고 모두 unique하게 만들었다.)
create table orders(
id int auto_increment primary key,
customer_id int not null ,
order_date date not null,
total_amount decimal(10,2) not null
);
//커버링 적용 x
create index idx_customer_order_date on orders(customer_id, order_date);
-> Limit: 10 row(s) (cost=3.85 rows=10) (actual time=0.377..0.38 rows=10 loops=1)
-> Index lookup on orders using idx_customer_order_date (customer_id=84831) (reverse)
(cost=3.85 rows=11) (actual time=0.376..0.378 rows=10 loops=1)
//커버링 적용
create index idx_covering on orders(customer_id, order_date, total_amount);
-> Limit: 10 row(s) (cost=2.12 rows=10) (actual time=0.0173..0.0195 rows=10 loops=1)
-> Covering index lookup on orders using idx_covering (customer_id=84831) (reverse)
(cost=2.12 rows=11) (actual time=0.0165..0.0181 rows=10 loops=1)
결과에서 볼 수 있듯이 디스크 접근을 안함으로 대략 30배가 넘는 차이가 남을 볼 수 있다.
index dive
index dive에 대한 최적화를 얘기하기 전에 index dive를 간단히 설명하자면
쿼리가 실행될 때 여러가지 과정을 거치는데
- 쿼리 분석 및 파싱 - MySQL 서버는 쿼리를 분석하고 파싱하여 문법적으로 올바른지 검사한다.
- 최적화 - 옵티마이저는 가능한 여러 실행 계획을 고려하여 가장 효율적인 실행 계획을 선택한다.
이때 인덱스, 조인 순서, 조인 방법 등을 고려하여 최적의 실행 계획을 결정한다. - 실행 계획 수립 - 선택된 최적의 실행 계획에 따라 MySQL은 실제로 쿼리를 실행할 방법을 결정한다.
이 단계에서 테이블 액세스 방법, 인덱스 사용, 조인 순서 등이 결정된다. - 실행
대략적으로 이러한 과정이다.
이때 옵티마저는 실행 계획을 선택할 때 통계 정보도 활용하지만 실제 데이터를 샘플링 하여 계획을 수립하는데
이를 index dive라고 한다.
보통 index dive는 큰 리소스를 소모하지 않지만 특정한 경우 굉장히 많은 리소스를 소모할 수 있고 이때
튜닝이 필요해질 수 있다.
With index dives, the optimizer makes a dive at each end of a range and uses
the number of rows in the range as the estimate. For example,
the expression col_name IN (10, 20, 30) has three equality ranges
and the optimizer makes two dives per range to generate a row estimate.
Each pair of dives yields an estimate of the number of rows that have the given value.
설명에서 볼 수 있듯이 index dive는 in 절에서 각 값마다 첫번째 값부터 끝까지 값을 각각 조회하고
해당 값들로 데이터의 양을 추정한다.
in절에 1~2개만 있다면 문제가 없겠지만 100개, 200개가 넘어가면 굉장히 큰 리소스를 소비하게된다.
이런 상황에서는 어떻게 개선할 수 있을까?
공식문서를 보면 MySQL 8.0 이전에는 eq_range_index_dive_limit 라는 환경변수를 통한 것이 아니면index dive를 스킵할 수 없었다.
8.0 이후로는
- 1개의 테이블에 쿼리를 날리는 경우
FORCE INDEX를 사용하는 경우- 서브 쿼리가 없을경우
DISTINCT,GROUP BY,ORDER BY가 없을 경우- 인덱스가
FULL TEXT인덱스가 아니고Unique Constraint가 아닐경우
index dive가 스킾될 수 있다고 한다.
그 외로는
eq_index_range_dive_limit변수를 조정해서 특정 값을 넘어가면index dive스킵이 가능range_optimizer_max_mem_size의 경우 지정 값 보다 메모리에 더 많은in값이 오면 실행계획이 포기되고 인덱스가 사용되지 않는다.
간단하게 index dive가 일어날 때 안일어 날 때의 성능을 비교해보자 (in안에 숫자가 굉장히 많다는 가정)
SELECT *
FROM orders
WHERE customer_id IN (101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111...200)
SELECT *
FROM orders FORCE INDEX (idx_customer_order)
WHERE customer_id IN (101, 102, 103, 104, 105, 106, 107, 108, 109....200)
극단적인 예시긴 하지만 이렇게 100개 가량의 수가 있을 때 index dive가 일어났을 때와
안일어났을 때를 비교하면

굉장히 큰 차이가 남을 알 수 있다.
prefix index
prefix index는 칼럼의 전체 값이 아닌 일부 접두사를 사용하여 인덱스를 생성하는 방법으로BLOB, TEXT, VARCHAR 와 같은 전체 값을 인덱싱 하기 힘들 경우 사용한다.
이때 접두사의 길이를 정해진 한도내로 설정이 가능한데 적절히 인덱스 크기를 조절해야 한다.
- 접두사가 너무 길다 -> 사실상 인덱스의 의미가 없어진다.
- 접두사가 너무 짧다 -> 한 인덱스에 값들이 너무 많이 들어가 성능이 개선되지 않는다.
접두사 길이에 따른 인덱스 크기 변화를 간단한 예시로 보면
SELECT COUNT(*) AS c, left(city,1) as prefix
FROM world_cities
GROUP BY prefix
ORDER BY c DESC;
SELECT COUNT(*) AS c, left(city,3) as prefix
FROM world_cities
GROUP BY prefix
ORDER BY c DESC;
SELECT COUNT(*) AS c, left(city,6) as prefix
FROM world_cities
GROUP BY prefix
ORDER BY c DESC;
SELECT COUNT(*) AS c, left(city,9) as prefix
FROM world_cities
GROUP BY prefix
ORDER BY c DESC;

내가 사용한 데이터 셋은 8~9부터 유의미한 차이가 발생하지 않았다.
결론
이렇게 인덱스를 생성하거나 튜닝할 시 고려할만한 요소들을 간단하게 공부하고 실제로 적용해보았다.
추후 진행할 토이 프로젝트나 실무에서도 공부한 인덱스 내용를을 적용할 수 있는 상황이 왔으면 좋겠다! 다음 포스트에서는 인덱스 외 다른 요소들을 통해 최적화 시키는 방법을 정리할 예정이다.
'ComputerScience > DataBase' 카테고리의 다른 글
| MongoDB Bulk Ops (feat: insertMany, Bulk Write) 알아보기 (0) | 2024.04.21 |
|---|---|
| Mongo DB Sharding (feat: replication, partitioning) (2) | 2024.04.05 |
| MySQL Bulk Update (feat: temporal table) (0) | 2024.03.21 |
| MySQL Order By, Select For Update 최적화 하기 (0) | 2024.03.02 |
| 데이터 베이스 트랜잭션과 격리수준 (Transaction and Isolation Level) (0) | 2023.07.17 |