일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- modelmapper
- 공룡책
- Test code
- 소프티어
- Service 계층 테스트
- 일상
- JPA
- backend
- Junit 5
- ObjectMapper
- Spring
- Test Doulbe
- Server
- proxyFactory
- 테크쇼
- OS
- db
- Test
- Java
- FCM
- JPQL
- mapstruct
- enumSet
- 자바
- Coputer Science
- MySQL
- 인프콘2023
- ExceptionResolver
- softeer
- RequestBody
- 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 |