B-tree 인덱스를 통한 데이터 읽기

어떤 경우에 인덱스를 사용하게 유도할지, 또는 사용하지 못하게 할지 판단하려면 InnoDB 엔진이 어떻게 인덱스를 이용해 실제 레코드를 읽는지 알아봐야 한다. 대표적인 세 가지를 알아보자.

인덱스 레인지 스캔

인덱스 레인지 스캔은 인덱스 접근 방법 가운데 가장 대표적인 접근 방식임. 사실 인덱스를 통해 레코드 한 건만 읽는 경우와 한 건 이상을 읽는 경우를 각각 다른 이름으로 구분하지만, 여기서는 그냥 다 묶어서 인덱스 레인지 스캔이라고 하겠다. 상세한 내용은 10장 실행계획에서 다시 세부적으로 구분할 것임.

다음 쿼리를 예제로 보자.

select * 
from employees
where first_name between 'jo' and 'kim'

image.png

이 방식은 검색해야 할 인덱스의 범위가 결정되었을때 사용하는 방식이다. 검색하려는 값의 수나 검색결과 레코드 건수와 상관없이 레인지 스캔이라고 한다. 스캔이 필요한 레코드의 시작지점을 리프노드를 통해 찾아내고, 시작지점부터 리프노드의 레코드를 순서대로 읽어낸다. 그러다 리프노드의 끝까지 읽으면 리프노드간의 링크를 이용해 다음 리프노드를 읽는 방식으로 스캔을 끝까지 진행한다. 이후 스캔이 끝나면 지금까지 읽은 레코드를 반환하는 식으로 조회작업이 이뤄진다.

다음 그림은 B-Tree 인덱스의 리프 노드를 스캔하면서 레코드 주소를 이용해 데이터 파일의 레코드를 읽어오는 과정까지 묘사된 그림이다.

image.png

중요한 것은 어느 방식으로 스캔하든, 해당 인덱스를 구성하는 컬럼의 정순 혹은 역순(오름차순이든, 내림차순이든)으로 정렬된 상태로 레코드를 가져온다는 것이다. 이는 별도 정렬과정이 필요한게 아니라 인덱스 자체가 정렬되어 있어서 자동으로 그렇게 된다.

위 그림에서 보이듯이 리프 페이지에 저장된 레코드 주소를 바탕으로 데이터 파일의 레코드를 읽어올 때 레코드 한건 한건 단위로 랜덤 I/O가 일어난다. 그래서 인덱스를 통해 데이터 레코드를 읽는 작업은 비용이 많이 드는 작업이다. 그리고 이런 이유로 인덱스를 통해 읽어야 할 데이터 레코드가 20%~25%를 넘으면 인덱스를 통한 읽기보다 테이블 데이터를 직접 읽는게 더 효율적인 처리 방식이 된다.

다시 정리하자면, 인덱스 레인지 스캔은 다음과 같은 3단계를 거친다.

  1. 인덱스 탐색(Index seek): 인덱스에서 조건을 만족하는 값이 저장된 시작 위치를 찾는다.
  2. 인덱스 스캔(Index scan): 탐색된 위치부터 필요한 만큼 인덱스를 쭉 스캔한다.
  3. 2번에서 읽어들인 인덱스 키와 레코드 주소를 이용해 레코드가 저장된 페이지를 가져오고, 최종 레코드를 읽어온다.

💡 MySQL 서버에서는 1번과 2번 작업이 얼마나 수행되었는지를 다음 쿼리를 통해 상태 값을 확인할 수 있다.

show status like 'Handler_%';

커버링 인덱스

쿼리가 필요로 하는 데이터에 따라 3번 과정은 필요하지 않을 수도 있다. 이를 커버링 인덱스라고 한다. 커버링 인덱스로 처리되는 쿼리는 디스크의 레코드를 읽지 않아도 되어서 랜덤 읽기가 굉장히 줄어들고, 따라서 성능도 빨라진다.