B-Tree는 데이터베이스의 인덱싱 알고리즘 가운데 가장 일반적으로 사용되고, 또한 가장 먼저 도입된 알고리즘이다. 하지만 아직도 가장 범용적인 목적으로 사용되는 인덱스 알고리즘이다. B-Tree에는 여러 가지 변형된 형태이 알고리즘이 있는데, 일반적으로 DBMS에는 주로 B+-Tree 또는 B*-Tree가 사용된다. 또한 B-Tree의 "B"는 "Binary(이진)"의 약자가 아니라 "Balanced"를 의미한다.
B-Tree는 칼럼의 원래 값을 변형시키지 않고 (물론 값의 앞부분만 잘라서 관리하지는 않지만) 인덱스 구조체 내에서는 항상 정렬된 상태를 유지하고 있다. 전문 검색과 같은 특수한 요건이 아닌 경우, 대부분 인덱스는 거의 B-Tree를 사용할 정도로 일반적인 용도에 적합한 알고리즘이다.
B-Tree 인덱스를 제대로 사용하려면 B-Tree의 기본적인 구조는 알고 있어야 한다. B-Tree는 트리 구조의 최상위에 하나의 "루트 노드"가 존재하고 그 하위에 자식 노드가 붙어 있는 형태다. 트리 구조의 가장 하위에 있는 노드를 "리프 노드"라 하고, 트리 구조에서 루트 노드도 아니고 리프 노드도 아닌 중간의 노드를 "브랜치 노드"라고 한다. 데이터베이스에서 인덱스와 실제 데이터가 저장된 데이터는 따로 관리되는데, 인덱스의 리프 노드는 항상 실제 데이터 레코드를 찾아가기 위한 주소 값을 가지고 있다.
위의 그림에서와 같이 인덱스의 키값은 모두 정렬돼 있지만 데이터 파일의 레코드는 정렬돼 있지 않고 임의의 순서대로 저장돼 있다. 많은 사람이 데이터 파일의 레코드는 INSERT된 순서대로 저장되는 것으로 생각하지만 그렇지 않다. 만약 테이블의 레코드를 전혀 삭제나 변경 없이 INSERT만 수행한다면 맞을 수도 있다. 하지만 레코드가 삭제되어 빈 공간이 생기면 그다음의 INSERT는 가능한 삭제된 공간을 재활용하도록 DBMS가 설계되기 때문에 항상 INSERT된 순서로 저장되는 것은 아니다.
대부분 RDBMS의 데이터 파일에서 레코드는 특정 기준으로 정렬되지 않고 임의의 순서대로 저장된다. 하지만 InnoDB 테이블에서 레코드는 클러스터되어 디스크에 저장되므로 기본적으로 프라이머리 키 순서대로 정렬되어 저장된다. 이는 오라클의 IOT(Index organized table)나 MS-SQL의 클러스터 테이블과 같은 구조를 말한다. 다른 DBMS에서는 클러스터링 기능이 선택 사항이지만, InnoDB에서는 사용자가 별도의 명령이나 옵션을 선택하지 않아도 디폴트로 클러스터링 테이블이 생성된다. 클러스터링이란 비슷한 값들은 최대한 모아서 저장하는 방식을 의미한다.
인덱스는 테이블의 키 칼럼만 가지고 있으므로 나머지 칼럼을 읽으려면 데이터 파일에서 해당 레코드를 찾아야 한다. 이를 위해 인덱스의 리프 노드는 데이터 파일에 저장된 레코드의 주소를 가지게 된다. 아래 그림인 인덱스의 리프 노드와 데이터 파일의 이러한 관계를 보여준다.
위의 그림에서 "레코드 주소"는 DBMS 종류나 MySQL의 스토리지 엔진에 따라 의미가 달라진다. 오라클은 물리적인 레코드 주소가 되지만 MyISAM 테이블에는 내부적인 레코드의 아이디(번호)를 의미한다. 그리고 InnoDB 테이블에서는 프라이머리 키에 의해 클러스터링되기 때문에 프라이머리 키값 자체가 주소 역할을 한다. 실제 MySQL 테이블의 인덱스는 항상 인덱스 칼럼 값과 주소 값(MyISAM의 레코드 아이디 값 또는 InnoDB의 프라이머리 키값)의 조합이 인덱스 레코드로 구성된다.
테이블의 레코드를 저장하거나 변경하는 경우, 인덱스 키 추가나 삭제 작업이 발생한다. 인덱스 키 추가나 삭제가 어떻게 처리되는지 알아두면 쿼리의 성능을 쉽게 예측할 수 있을 것이다. 또한, 인덱스를 사용하면서 주의해야 할 사항도 함께 살펴보자.
인덱스 키 추가
새로운 키값이 B-Tree에 저장될 때 테이블의 스토리지 엔진에 따라 새로운 키값이 즉시 인덱스에 저장될 수도 있고 그렇지 않을 수도 있다. B-Tree에 저장될 때는 저장될 키값을 이용해 B-Tree상의 적절한 위치를 검색해야 한다. 저장될 위치가 결정되면 레코드의 키값과 대상 레코드의 주소 정보를 B-Tree의 리프 노드에 저장한다. 만약 리프 노드가 꽉 차서 더는 저장할 수 없을 때는 리프 노드가 분리(Split)돼야 하는데, 이는 상위 브랜치 노드까지 처리의 범위가 넓어진다. 이러한 작업 탓에 B-Tree는 상대적으로 쓰기 작업(새로운 키를 추가하는 작업)에 비용이 많이 드는 것으로 알려졌다.
인덱스 추가로 인해 INSERT나 UPDATE 문장이 어떤 영향을 받을지 궁금해하는 사람이 많다. 하지만 이 질문에 명확하게 답변하려면 테이블의 칼럼 수, 칼럼의 크기, 인덱스 칼럼의 특성 등을 확인해야 한다. 대략적으로 계산해야 하는 방법은 테이블에 레코드를 추가하는 작업 비용을 1이라고 가정하면 해당 테이블의 인덱스에 키를 추가하는 작업 비용을 1~1.5 정도로 예측하는 것이 일반적이다. 일반적으로 테이블에 인덱스가 3개(테이블의 모든 인덱스가 B-Tree라는 가정하에)가 있다면 이때 테이블에 인덱스가 하나도 없는 경우는 작업 비용이 1이고, 3개인 경우에는 5.5 정도의 비용(1.5 * 3 + 1) 정도로 예측해 볼 수 있다. 중요한 것은 이 비용의 대부분의 메모리와 CPU에서 처리하는 시간이 아니라 디스크로부터 인덱스 페이지를 읽고 쓰기를 해야 하기 때문에 시간이 오래 걸린다는 것이다.
MyISAM이나 Memory 스토리지 엔진을 사용하는 테이블에서는 INSERT 문장이 실행되면 즉시 새로운 키값을 B-Tree 인덱스에 반영한다. 즉 B-Tree에 키를 추가하는 작업이 완료될 때까지 클라이언트는 쿼리의 결과는 받지 못하고 기다리게 된다. MyISAM 스토리지 엔진은 "delay-key-write" 파라미터를 설정해 인덱스 키 추가 작업을 미뤄서(지연) 처리할 수 있는데, 이는 동시 작업 환경에서는 적합하지 않다. InnoDB 스토리지 엔진은 이 작업을 조금 더 지능적으로 처리하는데, 아래 그림과 같이 상황에 따라 적절하게 인덱스 키 추가 작업을 지연시켜 나중에 처리할지, 아니면 바로 처리할지 결정한다.
(1) 사용자의 쿼리 실행
(2) InnoDB의 버퍼 풀에 새로운 키값을 추가해야 할 페이지(B-Tree의 리프 노드)가 존재한다면 즉시 키 추가 작업 처리
(3) 버퍼 풀에 B-Tree의 리프 노드가 없다면 인서트 버퍼에 추가할 키값과 레코드의 주소를 임시로 기록해 두고 작업 완료(사용자의 쿼리는 실행 완료됨)
(4) 백그라운드 작업으로 인덱스 페이지를 읽을 때마다 인서트 버퍼에 머지해야 할 인덱스 키값이 있는지 확인한 후, 있다면 병합함(B-Tree에 인덱스 키와 주소를 저장).
(5) 데이터베이스 서버 자원의 여유가 생기면 MySQL 서버의 인서트 버퍼 머지 스레드가 조금씩 인서트 버퍼에 임시 저장된 인덱스의 키와 주소 값을 머지(B-Tree에 인덱스 키와 주소를 저장)시킴
InnoDB 스토리지 엔진의 인서트 버퍼는 MySQL 5.1 이하에서는 INSERT로 인한 인덱스 키 추가 작업만 버퍼링 및 지연 처리를 할 수 있었다. 하지만 MySQL 5.5 이상의 버전에서는 INSERT뿐 아니라 DELETE 등에 의한 인덱스 키의 추가 및 삭제 작업까지 버퍼링해서 지연 처리할 수 있게 기능이 확장됐다. 그래서 MySQL 5.1 이하 버전에서는 이 기능을 인서트 버퍼링(Insert Buffering)이라고 했지만 MySQL 5.5 이상 버전부터는 관련 설정 파라미터로 "innodb_change_buffering"이 새롭게 도입됐다. 인서트 버퍼에 의해 인덱스 키 추가 작업이 지연되어 처리된다 하더라도, 이는 사용자에게 아무런 악영향 없이 투명하게 처리되므로 개발자나 DBA는 이를 전혀 신경쓰지 않아도 된다. MySQL 5.1 이하 버전에서는 자동으로 적용되는 기능이었지만 MySQL 5.5 이상 버전부터는 "innodb_change_buffering" 설정 값을 이용해 키 추가 작업과 키 삭제 작업 중 어느 것을 지연 처리할지 설정해야 한다.
인덱스 키 삭제
B-Tree의 키값이 삭제되는 경우는 상당히 간단하다. 해당 키값이 저장된 B-Tree의 리프 노드를 찾아서 그냥 삭제 마크만 하면 작업이 완료된다. 이렇게 삭제 마킹된 인덱스 키 공간은 계속 그대로 방치하거나 또는 재활용할 수 있다. 인덱스 키 삭제로 인한 마킹 작업 또한 디스크 쓰기가 필요하므로 이 작업 역시 디스크 I/O가 필요한 작업이다. MySQL 5.5 이상의 버전의 InnoDB 스토리지 엔진에서는 이 작업 또한 버퍼링되어 지연 처리가 될 수도 있다. 처리가 지연된 인덱스 키 삭제 또한 사용자에게는 특별한 악영향 없이 MySQL 서버가 내부적으로 처리하므로 특별히 걱정할 것은 없다. MyISAM이나 Memory 스토리지 엔진의 테이블에서는 인서트 버퍼와 같은 기능이 없으므로 인덱스 키 삭제가 완료된 후 쿼리 실행이 완료된다.
인덱스 키 변경
인덱스의 키값은 그 값에 따라 저장될 리프 노드의 위치가 결정되므로 B-Tree의 키값이 변경되는 경우에는 단순히 인덱스상의 키값만 변경하는 것은 불가능하다. B-Tree의 키값 변경 작업은 먼저 키값을 삭제한 후, 다시 새로운 키값을 추가하는 형태로 처리된다. 키 값의 변경 때문에 발생하는 B-Tree 인덱스 키값의 삭제나 추가 작업은 위에서 설명한 절차대로 처리된다.
인덱스 키 검색
INSERT, UPDATE, DELETE 작업을 할 때 인덱스 관리에 따른 추가 비용을 감당하면서 인덱스를 구축하는 이유는 바로 빠른 검색을 위해서다. 인덱스를 검색하는 작업은 B-Tree의 루트 노드부터 시작해 브랜치 노드를 거쳐 최종 리프 노드까지 이동하면서 비교 작업을 수행하는데, 이 과정을 "트리 탐색(Tree traversal)"이라고 한다. 인덱스 트리 탐색은 SELECT에서만 사용하는 것이 아니라 UPDATE나 DELETE를 처리하기 위해 항상 해당 레코드를 먼저 검색해야 할 경우에도 인덱스가 있으면 빠른 검색이 가능하다. B-Tree 인덱스를 이용한 검색은 100% 일치 또는 값의 앞부분(Left-most part)만 일치하는 경우에 사용할 수 있다. 부등호("<>")비교나 값의 뒷부분이 일치하는 경우에는 B-Tree 인덱스를 이용한 검색이 불가능하다. 또한 인덱스를 이용한 검색에서 중요한 사실은 인덱스의 키값에 변형이 가해진 후 비교되는 경우에는 절대 B-Tree의 빠른 검색 기능을 사용할 수 없다는 것이다. 이미 변형된 값은 B-Tree 인덱스에 존재하는 값이 아니다. 따라서 함수나 연산을 수행한 결과로 정렬한다거나 검색하는 작업은 B-Tree의 장점을 이용할 수 없으므로 주의해야 한다.
InnoDB 스토리지 엔진에서 인덱스는 더 특별한 의미가 있다. InnoDB 테이블에서 지원하는 레코드 잠금이나 넥스트 키 락(갭 락)이 검색을 수행한 인덱스를 잠근 후 테이블의 레코드를 잠그는 방식으로 구현돼 있다. 따라서 UPDATE나 DELETE 문장이 실행될 때 테이블에 적절히 사용할 수 있는 인덱스가 없으면 불필요하게 많은 레코드를 잠근다. 심지어 테이블의 모든 레코드를 잠글 수도 있다. InnoDB 스토리지 엔진에서는 그만큼 인덱스의 설계가 중요하고 많은 부분에 영향을 미친다는 것이다.
B-Tree 인덱스는 인덱스를 구성하는 칼럼의 크기와 레코드이 건수, 그리고 유니크한 인덱스 키값의 개수 등에 검색이나 변경 작업의 성능이 영향을 받는다.
인덱스 키값의 크기
InnoDB 스토리지 엔진은 디스크에 데이터를 저장하는 가장 기본 단위를 페이지(Page) 또는 블록(Block)이라고 하며, 디스크의 모든 읽기 및 쓰기 작업의 최소 작업 단위가 된다. 또한 페이지는 InnoDB 스토리지 엔진의 버퍼 풀에서 데이터를 버퍼링하는 기본 단위이기도 하다. 인덱스도 결국은 페이지 단위로 관리되며, 위의 B-Tree 그림에서 루트와 브랜치, 그리고 리프(Leaf) 노드를 구분한 기준이 바로 페이지 단위다.
이진(Binary) 트리는 각 노드가 자식 노드를 2개만 가지는데, 만약 DBMS의 B-Tree가 이진 트리라면 인덱스 검색이 상당히 비효율일 것이다. 그래서 B-Tree의 "B"가 이진(Binary) 트리의 약자는 아니라고 강조했던 것이다. 일반적으로 DBMS의 B-Tree는 자식 노드의 개수가 가변적인 구조다. 그러면 MySQL의 B-Tree는 자식 노드를 몇 개까지 가질지가 궁금할 것이다. 그것은 바로 인덱스의 페이지 크기와 키 값의 크기에 따라 결정된다. InnoDB의 모든 페이지 크기는 16KB로 고정돼 있다(이를 변경하려면 소스 컴파일이 필요함). 만약 인덱스의 키가 16바이트라고 가정하면 다음 그림과 같이 인덱스 페이지가 구성될 것이다. 아래 그림에서 자식 노드 주소라는 것은 여러 가지 복합적인 정보가 담긴 영역이며, 페이지의 종류별로 대략 6바이트에서 12바이트까지 다양한 크기의 값을 가질 수 있다. 여기서는 편의성 자식 노드 주소 영역이 평균적으로 12바이트로 구성된다고 가정하자.
위의 그림의 경우, 하나의 인덱스 페이지(16KB)에 몇 개의 키를 저장할 수 있을까? 계산해 보면 16*1024/(16+12) = 585개 저장할 수 있다. 그러면 인덱스 키 값이 커지면 어떤 현상이 발생할까? 위의 경우에서 키값의 크기가 두 배인 32바이트로 늘어났다고 가정하면 한 페이지에 인덱스 키를 16Ѐ1024/(32+12) = 372개 저장할 수 있다. 만약 여러분의 SELECT 쿼리가 레코드 500개를 읽어야 한다면 전자는 인덱스 페이지 한 번으로 해결될 수도 있지만, 후자는 최소한 2번 이상 디스크로부터 읽어야 한다. 결국 인덱스를 구성하는 키값의 크기가 커지면 디스크로부터 읽어야 하는 횟수가 늘어나고, 그만큼 느려진다는 것을 의미한다.
또한, 인덱스 키 값의 길이가 길어진다는 것은 전체적인 인덱스의 크기가 커진다는 것을 의미한다. 하지만 인덱스를 캐시해 두는 InnodB의 버퍼 풀이나 MyISAM의 키 캐시 영역은 크기가 제한적이기 때문에 하나의 레코드를 위한 인덱스 크기가 커지면 커질수록 메모리(버퍼 풀이나 키 캐시)에 캐시해 둘 수 있는 레코드 수는 줄어든다는 것을 의미한다. 자연히 메모리의 효율이 떨어지게 되는 결과를 가져온다.
B-Tree 깊이
B-Tree 인덱스의 깊이(Depth)는 상당히 중요하지만 직접적으로 제어할 방법이 없다. 여기서는 인덱스 키값이 평균 크기가 늘어나면 어떤 현상이 추가로 더 발생하는지 알아보겠다. 위의 그림의 예제를 다시 살펴보자. 인덱스의 B-Tree의 깊이가 3인 경우 최대 몇 개의 키값을 가질 수 있는지 한번 비교해 보자. 키값이 16바이트인 경우에는 최대 2억(585 * 585 * 585)개 정도의 키값을 담을 수 있지만 키값이 32바이트로 늘어나면 5천만(372 * 372 * 372)개로 줄어든다. B-Tree의 깊이는 MySQL에서 값을 검색할 때 몇 번이나 랜덤하게 디스크를 읽어야 하는지와 직결되는 문제다. 결론적으로 인덱스 키값의 크기가 커지면 커질수록 하나의 인덱스 페이지가 담을 수 있는 인덱스 키값의 개수가 작아지고, 그 때문에 같은 레코드 건수라 하더라도 B-Tree의 깊이(Depth)가 깊어져서 디스크 읽기가 더 많이 필요하게 된다는 것을 의미한다.
여기서 언급한 내용은 사실 인덱스 키값의 크기는 가능하면 작게 만드는 것이 좋다는 것을 강조하기 위함이고, 실제로는 아무리 대용량의 데이터베이스라도 B-Tree의 깊이(Depth)가 4~5 이상까지 깊어지는 경우는 거의 발생하지 않는다.
선택도(기수성)
인덱스에서 선택도(Selectivity) 또는 기수성(Cardinality)은 거의 같은 의미로 사용되며, 모든 인덱스 키값 가운데 유니크한 값의 수를 의미한다. 전체 인덱스 키값은 100개인데, 그중에서 유니크한 값의 수는 10개라면 기수성은 10이다. 인덱스 키값 가운데 중복된 값이 많아지면 많아질수록 기수성은 낮아지고 동시에 선택도 또한 떨어진다. 인덱스는 선택도가 높을수록 검색 대상이 줄어들기 때문에 그만큼 빠르게 처리된다.
선택도가 좋지 않다고 하더라도 정렬이나 그룹핑과 같은 작업을 위해 인덱스를 만드는 것이 훨씬 나은 경우도 많다. 인덱스가 항상 검색에만 사용되는 것은 아니므로 여러 가지 용도를 고려해 적절히 인덱스를 설계할 필요가 있다.
country라는 칼럼과 city라는 칼럼이 포함된 tb_test 테이블을 예로 들겠다. tb_test 테이블의 전체 레코드는 1만 건이며, country 칼럼으로만 인덱스가 생성된 상태에서 아래의 두 케이스를 살펴보자.
SELECT * FROM tb_test WHERE country='KOREA' AND city='SEOUL';
MySQL에서는 인덱스의 통계 정보(유니크한 값의 개수)가 관리되기 때문에 city 칼럼의 기수성은 작업 범위에 아무런 영향을 미치지 못한다. 위의 쿼리를 실행하면 A 케이스의 경우에는 평균 1,000건, B 케이스의 경우에는 평균적으로 10건이 조회될 수 있다는 것을 인덱스의 통계 정보(유니크한 값의 개수)로 예측할 수 있다. 만약 A 케이스와 B 케이스 모두 실제 모든 조건을 만족하는 레코드는 단 1건만 있었다면 A 케이스의 인덱스는 적합하지 않은 것이라고 볼 수 있다. A 케이스는 1건의 레코드를 위해 쓸모없는 999건의 레코드를 더 읽은 것이지만, B 케이스는 9건만 더 읽은 것이다. 그래서 A 케이스의 경우 country 칼럼에 생성된 인덱스는 비효율적이다. 물론 필요한 만큼의 레코드를 정확하게 읽을 수 있다면 최상이겠지만, 현실적으로 모든 조건을 만족하도록 인덱스를 생성한다는 것은 불가능하므로 이 정도의 낭비는 무시할 수 있다.
각 국가의 도시를 저장하는 tb_city라는 테이블을 예로 들어보겠다. tb_city 테이블은 1만 건의 레코드를 가지고 있는데, country 칼럼에만 인덱스가 준비돼 있다. tb_city 테이블에는 국가와 도시가 중복해서 저장돼 있지 않다고 가정하자.
CREATE TABLE tb_city(
country VARCHAR(10),
city VARCHAR(10),
INDEX ix_country (country)
);
tb_city 테이블에 아래와 같은 쿼리를 한번 실행해 보자. 이때 tb_city 테이블의 데이터 특성을 두 가지로 나눠서 내부적인 쿼리나 인덱스의 효율성을 살펴보겠다.
SELECT * FROM tb_test WHERE country='KOREA' AND city='SEOUL';
country 칼럼의 유니크 값이 10개일 때
country 칼럼의 유니크 값이 10개이므로 tb_city 테이블에는 10개 국가(country)의 도시(city) 정보가 저장돼 있는 것이다. MySQL 서버는 인덱스된 칼럼(country)에 대해서는 전체 레코드가 몇 건이며, 유니크한 값의 개수 등에 대한 통계 정보를 가지고 있다. 여기서 전체 레코드 건수를 유니크한 값의 개수로 나눠보면 하나의 키 값으로 검색했을 때 대략 몇 건의 레코드가 일치할지 예측할 수 있게 된다. 즉, 이 케이스의 tb_city 테이블에서는 "country='KOREA'"라는 조건으로 인덱스를 검색하면 1000건(10,000/10)이 일치하리라는 것을 예상할 수 있다. 그런데 인덱스를 통해 검색된 1000건 가운데 city='SEOUL'인 레코드는 1건이므로 999건은 불필요하게 읽은 것으로 볼 수 있다.
country 칼럼의 유니크 값이 1000개일 때
country 칼럼의 유니크 값이 1000개이므로 tb_city 테이블에는 1000개 국가(country)의 도시(city) 정보가 저장돼 있는 것이다. 이 케이스에서도 전체 레코드 건수를 국각 칼럼의 유니크 값 개수로 나눠 보면 대략 한 국가당 대략 10개 정도의 도시 정보가 저장돼 있으리라는 것을 예측할 수 있다. 그래서 이 케이스에서는 tb_city 테이블에서 "country='KOREA'"라는 조건으로 인덱스를 검색하면 10건(10,000/1,000)이 일치할 것이며, 그 10건 중에서 city='SEOUL'인 레코드는 1건이므로 9건만 불필요하게 읽은 것이다.
위 두 케이스의 테이블에서 똑같은 쿼리를 실행해 똑같은 결과를 받았지만 사실 두 쿼리가 처리되기 위해 MySQL 서버가 수행한 작업 내용은 매우 크다는 것을 알 수 있다. 이처럼 인덱스에서 유니크한 값의 개수는 인덱스나 쿼리의 효율성에 큰 영향을 미치게 된다.
읽어야 하는 레코드의 건수
인덱스를 통해 테이블의 레코드를 읽는 것은 인덱스를 거치지 않고 바로 테이블의 레코드를 읽는 것보다 높은 비용이 드는 작업이다. 테이블에 레코드가 100만 건이 저장돼 있는데, 그중에서 50만 건을 읽어야 하는 쿼리가 있다고 가정해 보자. 이 작업은 전체 테이블을 모두 읽어서 필요 없는 50만 건을 버리는 것이 효율적일지, 인덱스를 통해 필요한 50만 건만 읽어오는 것이 효율적일지 판단해야 한다. 인덱스를 이용한 읽기의 손익 분기점이 얼마인지 판단할 필요가 있는데, 일반적인 DBMS의 옵티마이저에서는 인덱스를 통해 레코드 1건을 읽는 것이 테이블에서 직접 레코드 1건을 읽는 것보다 4~5배 정도 더 비용이 많이 드는 작업인 것으로 예측한다. 즉, 인덱스를 통해 읽어야 할 레코드의 건수(물론 옵티마이저가 판단한 예상 건수)가 전체 테이블 레코드이 20~25%를 넘어서면 인덱스를 이용하지 않고 직업 테이블을 모두 읽어서 필요한 레코드만 가려내는(필터링) 방식으로 처리하는 것이 효율적이다.
전체 100만 건의 레코드 가운데 50만 건을 읽어야 하는 작업은 인덱스이 손익 분기점인 20~25%보다 훨씬 크기 때문에 MySQL 옵티마이저는 인덱스를 이용하지 않고 직접 테이블을 처음부터 끝까지 읽어서 처리할 것이다. 이렇게 많은 레코드(전체 레코드 20~25% 이상)를 읽을 때는 강제로 인덱스를 사용하도록 힌트를 추가해도 성능상 얻을 수 있는 이점이 없다. 물론 이러한 작업은 MySQL의 옵티마이저가 기본적으로 힌트를 무시하고 테이블을 직접 읽는 방식으로 처리하겠지만 기본적으로는 알고 있어야 할 사항이다.
참고