인덱스란, 책 내용의 메타정보를 가지고있는 목차와도 같은 자료구조입니다.
데이터 획득을 위해 특정 조건으로 쿼리할 때, 특정 조건이 인덱스에 포함된다면 자료구조에 따라 O(LogX)의 시간에 원하는 데이터를 얻을 수있습니다.
위 그림은 MySQL의 기본 스토리지 엔진의 인덱스의 간략한 구조입니다.
InnoDB B-Tree 인덱스 특징
MySQL의 기본 스토리지 엔진인 InooDB인덱스는 B-Tree자료구조로 이뤄져있으며, 스토리지 엔진에따라 Hash알고리즘으로 구현된 인덱스도 있습니다.
Workbench 또는 CLI에서 인덱스를 조회하는 쿼리를 통해 인덱스의 존재를 확인할 수 있습니다. 인덱스의 주요 정보입니다.
인덱스 속성 | 설명 |
---|---|
Non-Unique | **컬럼이 유니크 제약조건을 갖는지를 의미합니다. (유니크하다면 0) |
index Range Scan등에서, 유니크 여부는 인덱스 탐색에 영향을 미칩니다. 최초의 칼럼을 찾았을 때, 유니크하다면 탐색을 멈춰도 되기 때문.** | |
Seq_in_index | 인덱스 내에서의 순서 (매우 중요. 인덱스는 첫번째 인덱스 기준으로 정렬됨) |
Key_name | 인덱스 키 이름 |
Collation | 인덱스의 정렬방식 A(Asc), D(Desc-지원안함) 이나 Null(중복 없음) |
Cardinality | 카디널리티(또는 선택도, Selectivity - 유니크한 값의 개수) |
Sub_part | 컬럼이 부분적으로 인덱싱 되었을때 길이 |
Null | 컬럼이 Null값을 가질수 있으면 Yes, 아니면 NO |
Index_type | 인덱스의 타임(RTREE,FULL TEXT,HASH,BTREE) |
실제 쿼리를 통해 데이터를 조회할 때는 다양한 쿼리를 사용해 데이터를 조회합니다. 쿼리 옵티마이저는 최대한 디스크 I/O를 줄이는 방향으로 인덱스 사용을 최적화해 쿼리 실행기에 넘겨줍니다.
인덱스가 실행되는 방법은 아래와 같습니다.
쿼리가 필요로하는 칼럼이 모두 Index Record에 포함되면 굳이 3번 Fetch Record를 통해 실제 데이터 페이지를 불러오지 않아도 됩니다. 이것을 이용해 인덱스가 존재하는 컬럼만으로 동작하는 쿼리를 ‘커버링 인덱스’를 사용한다’ 라고 합니다.
만약, (성별,이름)이 인덱스이고, 쿼리가 아래와 같다면, 커버링 인덱스를 통해 인덱스 레코드만으로 쿼리 데이터를 반환할 수 있습니다.
SELECT 성별,이름 FROM MEMBER WHERE 이름='이명규';
위 그림처럼, 쿼리 앞에 EXPLAIN키워드를 붙여주면 인덱스가 어떻게 처리되는지에 대한 정보를 확인할 수 있습니다.
타입 설명은 아래와 같습니다.
Full Scan은 인덱스 리프 노드를 전부 탐색함을 의미합니다. 인덱스 칼럼은 (A,B,C)순서이지만, 쿼리 조건절은 B,C컬럼으로만 이뤄져있는 경우에 발생합니다. (이는 추후에 개선됩니다. Skip, Loose Index Scan 참고)
인덱스 레코드는 인덱스로 지정한 컬럼만으로 구성되기에, 일반적으로 테이블 레코드보다 사이즈가 작아 디스크에서 디스크 랜덤 I/O를 통해 페이지를 불러오는 횟수가 적습니다.
또한, 조건절에 있는 비교를 위해 테이블 레코드를 모두 조회하는 것을 Full Table Scan이라고 하는데, 이는 더 많은 페이지 Load가 필요하므로, Full Index Scan보다 훨씬 더 느립니다.
→ 조건절에 인덱스가 아닌 컬럼이 있다: (Full 또는 Range)Table Scan
→ 조건절은 인덱스 컬럼으로 구성되어있지만, 인덱스 컬럼 외 가져와야 할 데이터가 있다: Index Scan
→ 조건절도 인덱스 컬럼에, 가져와야 할 컬럼도 인덱스 내에 있다: Covering Index
Range Scan의 경우, 쿼리에서 첫 번째 인덱스를 통해 범위가 특정되어 리프노드 인덱스 페이지들 중, 일부 페이지만 탐색 및 로드하는것을 의미합니다.
앞서 설명한, Index Seek, Index Scan, Fetch Record과정을 통해 데이터를 가져오지만, Index Scan과정에서 특정 범위의 인덱스 페이지만 가져오는 차이가 있습니다.
전체 데이터의 20~25%정도의 데이터를 Index Scan할 때 Index Range Scan보다 Full Table Scan의 효율이 더 좋다고 합니다.
[막간 실험] 책에서 위의 경우에 Full Table Scan이 성능이 더 좋다는게 살짝 이해가 안되어서, 아래의 “인덱스 성능에 영향을 주는 요인”에서그 이유를 고민해보겠습니다.
Index Loose Scan은 Group By에서사용하는 정렬과 관련된 Max,Min 집계함수의 최적화에 사용됩니다.
앞서 Full Index Scan에서 조건절에 두 번째 인덱스를 통해 쿼리할 때 Full Index Scan을 한다고 설명드렸는데요, 아래의 쿼리에 대해 생각해보겠습니다.
인덱스가 (성별, 학번, 이름)으로 구성되었다고 가정합니다.
//사실 성별은 카디널리티가 낮아 좋은 키는 아닙니다. 단지 예시를 위해..
SELECT MIN(학번) FROM STUDENT GROUP BY 성별
Index Loose Scan으로 동작하는 쿼리는 ‘남자’의 첫 번째 리프 페이지로 도착해 최댓값을 읽고, 나머지 ‘남자’인 인덱스 레코드는 읽지 않고 ‘여자’인덱스 페이지로 이동해 최댓값을 읽고 종료합니다.
Index Skip Scan은 MySQL8.0부터 도입된 기능입니다. Loose Scan이 Group By절만 최적화 했다면, Skip Scan에서는 첫 번째 인덱스 컬럼에서 두 번째 인덱스 컬럼과 매칭되는 레코드를 탐색하고, 이외의 레코드를 스킵합니다.
이를 통해 조건절에서 인덱스 첫 번째 조건절을 누락하고 쿼리하더라도 Full Index Scan보다 빠르게 데이터를 가져올 수 있습니다.
다시 비슷한 예시를 들어보겠습니다. 인덱스는 (성별, 학번, 이름)으로 구성되어 있습니다.
SELECT * FROM STUDENT WHERE 학번 > 2017000;
학번은 인덱스 테이블의 두 번째 칼럼이기에 Index Skip Scan이 없다면, Full Index Scan을 통해 데이터를 찾아야됩니다.
Index Skip Scan에서는 아래와 같이 동작합니다.
이를 통해 로드해야 하는 인덱스 페이지 수가 줄어듭니다.
Q. 브랜치노드로의 참조가 없는데, 어떻게 SKIP이 가능할까..?
위의 요소 외에도, 인덱스 컬럼을 지정할 때는 굉장히 다양한 요인을 생각해 설계해야합니다.
인덱스 개념을 아는것과 인덱스를 설계할 줄 아는것은 완전 다른 내용같습니다.
위의 주요 요소 외에도 SQL 패턴별로 다양한 인덱스 설계가 나올 수 있으니 실제 업무에서 사용할때는 꼭 따로 공부해봐야겠습니다!
앞서 설명한 내용은 다 논클러스터드 인덱스입니다.
더 공부해보면 좋을것들
MySQL 공식문서 https://dev.mysql.com/doc/refman/8.0/en/optimization-indexes.html
백은빈, 이성욱 저 Real MySQL 8.0
Inpa님 블로그 https://inpa.tistory.com/entry/MYSQL-📚-인덱스index-핵심-설계-사용-문법-💯-총정리