5. B-Tree 인덱스의 정렬과 스캔 방향
인덱스는 정렬되어 있다. 이제까지 보인 예시들에선 오름차순 정렬만이 존재했지만, 우리가 생성할 때 조건을 설정함에 따라 오름차순으로도, 내림차순으로도 생성할 수 있따.
거기에, 읽는 것도 조절할 수 있다. 오름차순 테이블이여도 데이터를 내림차순으로 가져와야 한다면 내림차순으로도 읽을 수 있다! 모든 것은 옵티마이저가 결정한다.
정렬 설정법
대부분의 상용 DBMS는 인덱스 생성시 정렬을 결정할 수 있게 해준다. MySQL은 8.0 버전 부터 컬럼 단위로 설정할 수 있게 해주었다.
CREATE INDEX ix_teamname_userscore ON employees (team_name ASC, user_score DESC);
위 쿼리는 team_name은 오름차순, user_score는 내림차순 정렬이 되어 있는 인덱스를 생성할 수 있게 해준다.
5.1 스캔 방향
인덱스가 스캔되는 방향에 대해 이야기해보자.
SELECT *
FROM employees
ORDER BY first_name DESC
LIMIT 3;
위 쿼리를 수행할 때, 인덱스에서 first_name이 오름차순으로 정렬 되어 있다면, 옵티마이저는 순진하게 인덱스 전체를 읽고 가장 큰 3개의 값을 반환할까?
당연히 아니다, 옵티마이저는 인덱스를 역순으로 읽어 내림차순으로 레코드를 가져온다!
그리고 내림차순 정렬된 상태로 가져온다.
그렇다고 첫 칼럼은 역순으로 읽고, 두 번째 칼럼은 정순, 세 번째 칼럼은 다시 역순.. 이런 식으로 읽을 수는 없으니, 인덱스를 설계할 때 잘 고려해보아야 한다.
5.2 그렇다면 내림차순 인덱스는 왜 필요한걸까?
애초에 역순 읽기가 있다면, 왜 내림차순 인덱스가 필요한걸까? 그냥 오름차순 인덱스만 존재하고 거꾸로 읽어내면 그게 내림차순 인덱스인것 아닐까?
다음과 같은 두 가지 이유 때문에 거꾸로 읽는 것은 조금 느리게 진행된다.
- 페이지 잠금은 인덱스를 정순으로 읽는 것에 적합한 구조를 가지고 있다. (Forward Index Scan)
- 레코드들은 단방향 링크를 가졌다.
데이터가 많을 때, (약 천건) 28.9%정도의 속도 차이가 있다. 단순히 노드들을 역순으로 탐색하는 것 뿐만이라면 속도가 비슷해야겠지만 위의 두 이유로 인해 역순 스캔은 느리게 동작한다.
6. 인덱스의 효율성과 작업 범위 결정
효율적인 인덱스를 만들고, 쿼리를 "제대로" 최적화 하기 위해선, 쿼리의 WHERE
과 GROUP BY
, ORDER BY
절들이 "언제", "어떻게" 인덱스를 사용하는지 알아야만 한다.
6.1 비교 조건의 종류와 효율
비교 조건에 따라 인덱스 칼럼의 활용 형태와 효율이 달라질 수 있다. 비교 조건이란 예를 들어 =
동등 비교나, >
, <
등의 범위 비교 조건 등을 의미한다.
예를 들어 컬럼의 순서만 다른 두 인덱스를 보자. 부서 번호를 dept_no
로, 직원 번호를 emp_no
라고 표현했다.
- 인덱스 1 : INDEX (dept_no, emp_no)
- 인덱스 2 : INDEX (emp_no, dept_no)
그리고 아래와 같은 쿼리를 보자.
SELECT *
FROM dept_emps
WHERE dept_no = 'd002' AND emp_no >= 10114;
어떤 부서 안에 10114 보다 id가 높은 회원들을 찾는 쿼리이다!
인덱스 1
은 순조롭게 dept_no가 d002
인 값을 찾고, emp_no가 10114
이상인 첫 값을 찾은 다음 d002
의 끝까지 쭉~읽어낼 것이다.
반면 인덱스 2
는 emp_no로 우선 찾을 수 밖에 없기 때문에, emp_no가 10114
이상인 그 모든 값에 대해서 dept_no를 하나 하나 비교해볼 수 밖에 없다 ㅠㅠ
인덱스를 통해 읽은 레코드가 나머지 조건에 맞는지 비교하며 취사선택하는 작업을 "필터링"이라고 부른다. 위 그림의 오른쪽 인덱스가 인덱스 2
인데, 여기서는 어쩔 수 없이 7개의 레코드를 가져온 다음에 맨 아래 d001
두개를 필터링 하여 버렸다.
결국 두 번째 인덱스에서 WHERE절 앞에 나온 dept_no = 'd002'
조건은 전혀 작업 범위를 줄이지 못했다.
앞서 다중 컬럼 인덱스에서 언급한 것과 같이, 작업의 범위를 좁히기 위해선 인덱스 순서를 신중하게 결정해야 한다.
6.2 B-Tree 인덱스의 가용성과 효율성 판단
6.2.1 B-Tree 인덱스를 사용할 수 없는 곳
B-Tree 인덱스의 특성상 아래 조건들은 인덱스의 범위를 결정하는데 사용될 수 없다.
- NOT-EQUAL 비교
<>
NOT IN
NOT BETWEEN
IS NOT NULL
- LIKE 절을 사용할 때, 뒷 부분 일치 패턴을 사용하는 경우!
- 예를 들어 이름 칼럼에 인덱싱 걸려 있다고 생각해보자. 만약 이름이 3글자라고 가정하면, 첫 번째 글자 부터 ㄱ, ㄴ, ㄷ..순으로 비교해 나갈 것이다.
앞에서 부터 비교하기 때문에, 앞 부분에 패턴이 걸려있다면, ex)LIKE '이%'
범위 검색이 아주 아주 쉽다.
"이"씨 이전임 김씨 곽씨 등은 싹 넘어가고, "이"씨만 체크하면 되기 때문이다. 하지만 뒷 부분에 걸려있는 경우.. ->LIKE '%진호'
와 같이 검색하는 경우, 이름의 뒷부분만을 비교해야 하기 때문에, 제대로 된 검색을 할 수 없게 되는 것이다.
- 예를 들어 이름 칼럼에 인덱싱 걸려 있다고 생각해보자. 만약 이름이 3글자라고 가정하면, 첫 번째 글자 부터 ㄱ, ㄴ, ㄷ..순으로 비교해 나갈 것이다.
- 데이터 타입이 서로 다른데 비교하는 경우
-> 인덱스 칼럼의 타입을 변환해야 비교가 가능한 경우 인덱스 사용이 불가능하다. ex) char column인데 숫자를 사용하는 경우.. 보통의 프로그래밍 언어라면 아스키 코드로 치환해주겠지만, DB에서 인덱싱은 사용할 수 없다. - 스토어드 함수나 다른 연산자로 인덱스 칼럼이 변형된 후 비교되는 경우
- 예를 들어
column
에 인덱스가 걸려 있을 떄DAYOFMONTH(column)
과 같은 연산을 통하면 당연히 사용할 수 없다.
- 예를 들어
- 문자열 데이터 타입의 콜레이션이 다른 경우.
-> 같은 utf8이어도 utf8_bin, utf8_general_ci 등은 내부적으로 정렬 방법이 내부적으로 다르다.
- NOT-DETERMINISTIC 속성의 스토어드 함수가 비교 조건에 사용되는 경우
6.2.2 MySQL은 인덱스에 NULL을 쓸 수 있다!
B-Tree 인덱스를 사용 가능한 곳
NULL
: NULL 값이 인덱스에 저장되지 않는 상용 DBMS가 많지만, MySQL에서는 NULL 값도 인덱스에 저장된다! 아래 쿼리의WHERE
조건도 작업 범위를 결정할 때 인덱스를 사용!
...
WHERE name IS NULL;
Postgresql에서는 특이하게도 Unique 인덱스에도 Null값을 여러개 넣을 수 있다; -> 공식 문서 (opens in a new tab)
6.2.3 다중 칼럼으로 만들어진 인덱스가 작업 범위를 결정하지 못하는 경우
예를 들어 아래와 같은 인덱스가 있다.
INDEX ix_test (col_1, col_2, col_3 ... )
col_1
에 대한 조건이 없는 경우 작업 범위를 결정할 수 없음col_1
칼럼의 비교 조건이6.2.1
에서 언급한 인덱스 사용 불가 조건들 중 하나인 경우!
6.2.4 다중 칼럼으로 만들어진 인덱스가 작업 범위를 결정할 수 있는 경우!
- col_1 ~ col_(i - 1) 칼럼까지 동등 비교 하는 경우 (
=
,IN
)
-> 말을 복잡하게 해두었는데, 인덱싱이 걸린 앞 부분 부터 특정 칼럼까지 동등 비교가 되어 있는 쿼리를 뜻한다! - 1번 조건 && col_i를 아래의 연산자중 하나로 비교하는 경우
- 동등 비교 :
=
,IN
- 대소 비교 :
>
,<
- LIKE 왼쪽 비교 패턴 :
LIKE '진호%'
- 동등 비교 :