8장 인덱스
- 인덱스에 대한 기본 지식은 쿼리 튜닝의 기본이 되므로 매우 중요하다.
8.1 디스크 읽기 방식
- CPU나 메모리처럼 전기적 특성을 띤 장치의 성능은 짧은 시간동안 매우 빠른 속도로 발전했다.
- 하지만 디스크 같은 기계식 장치의 성능은 제한적으로 발전했다.
- 비록 최근에 자기 디스크 원판을 사용하는 하드디스크보다 SSD 드라이브가 많이 활용되고 있지만 그래도 여전히 데이터 저장 매체가 컴퓨터에서 가장 느린 부분이라는 사실은 변함이 없다.
- 이러한 이유에서 데이터베이스 성능 튜닝은 어떻게 디스크 I/O를 줄이느냐가 관건일 떄가 상당히 많다.
8.1.1 하드 디스크 드라이브(HDD)와 솔리드 스테이트 드라이브(SSD)
- CPU나 메모리 장치는 대부분 전자식 장치인데 하드 디스크 드라이브는 기계식 장치이다.
- 그래서 데이터베이스 서버에서 항상 디스크 장치가 병목이 된다.
- 이러한 기계식 하드 디스크 드라이브를 대체하기 위해 전자식 저장 매체인 SSD(Solid State Drive)가 많이 출시되고 있다.
- SSD는 기존의 하드디스크 드라이브에서 데이터 저장용 플래터(원판)을 제거하고 그 대신 플래시 메모리를 장착한다.
- 디스크 원판을 기계적으로 회전시킬 필요가 없으므로 아주 빨리 데이터를 읽고 쓸 수 있다.
- 플래시 메모리는 전원이 공급되지 않아도 데이터가 삭제되지 않는다.
- 그리고 컴퓨터의 D-Ram 보다는 느리지만 하드 디스크 드라이브 보다는 훨씬 빠르다.
- 요즘 DBMS용으로 사용할 서버에서는 대부분 SSD를 채택하고 있다.
- 한 번에 많은 데이터를 읽는 순차 I/O에서는 SSD가 하드 디스크 드라이브(헤더를 움직이지 않을 떄)보다 조금 빠르거나 거의 비슷한 성능을 보이기도 한다.
- 하지만 SSD의 장점은 기존 하드 디스크 드라이브 보다 랜덤 I/O가 훨씬 빠르다는 것이다.
- 데이터 베이스 서버에서 순차 I/O 작업은 그다지 비중이 크지 않고 랜덤 I/O를 통해 작은 데이터를 읽고 쓰는 작업이 대부분이므로 SSD의 장점은 DBMS 스토리지에 최적이라고 볼 수 있다.
8.1.2 랜덤 I/O와 순차 I/O
- 랜덤 I/O : 하드 디스크 드라이브의 플래터(원판)을 돌려서 읽어야 할 데이터의 저장된 위치로 디스크 헤더를 이동시킨 다음 데이터를 읽는 것을 반복!
- 순차 I/O : 위의 과정을 한 번 수행하고 순차적인 데이터를 읽으면 된다.
- 예를 들어 3개의 페이지를 디스크에 기록하는데 순차 I/O는 디스크 헤더를 한 번 옮기고 쓰면 되는 반면 랜덤 I/O는 3번 움직인다.
- 거의 대부분의 시간은 헤더를 움직이는 데에서 결정되기 때문에 랜덤 I/O가 순차 I/O 보다 세 배의 시간이 걸린다고 보면 된다.
- 데이터베이스 대부분의 작업은 작은 데이터를 빈번히 읽고 쓰기 때문에 MySQL 서버에는 그룹 커밋이나 바이너리 로그 버퍼 또는 InnoDB 로그 버퍼 등의 기능이 내장돼 있다.
8.2 인덱스란?
- 인덱스를 실세계에 비유할 때 가장 많이 사용되는 모델이 책의 맨 뒤에 있는 찾아보기(색인)이다.
- 찾아보기가 인덱스라면 책의 내용은 데이터 파일이라고 볼 수 있다.
- 찾아보기의 페이지 번호는 데이터의 주소에 비유될 수 있다.
- DBMS도 모든 데이터를 검색해서 원하는 결과를 가져오려면 시간이 오래걸리는데 이러한 이유로 컬럼 - 주소 쌍을 인덱스로 만들어 두는 것이다.
- 책의 찾아보기와 인덱스의 공통점은 정렬이다.
- 찾아보기는 최대한 빨리 찾을 수 있게 ㄱ,ㄴ,ㄷ,ㄹ... 순으로 정렬되어있다. 인덱스도 마찬가지로 컬럼을 정렬해서 보관한다.
- 인덱스와 비슷한 자료구조로는 SortedList가 있다. SortedList는 값이 추가되면 항상 정렬된 상태로 유지한다.
- 이는 INSERT, UPDATE, DELETE의 성능은 희생되고 대신 읽기 속도를 높이는 역할을 한다.
- 테이블의 인덱스를 하나 더 추가할지 말지는 데이터의 저장 속도를 어디까지 희생할 수 있는지, 읽기 속도를 얼마나 빠르게 만들어야 하느냐에 따라 결정된다.
인덱스의 종류
- 역할별로 구분
- 프라이머리 키 : 말 그대로 프라이머리 키로 만들어진 인덱스, null을 허용하지 않고 중복을 허용하지 않는다.
- 보조 키 : 프라이머리 키를 제외한 나머지 모든 인덱스
- 데이터 저장 방식(알고리즘)으로 구분
- B-Tree 인덱스 : 가장 일반적인 인덱스 알고리즘, 컬럼의 값을 변형하지 않고 원래 값을 이용해 인덱싱
- Hash 인덱스 : 해시값을 계산해서 인덱싱한다. 매우 빠른 검색을 지원한다.
- 데이터 중복 허용 여부
- Unique
- Non-Unique
- 인덱스의 기능별로 구분
- 전문 검색용 인덱스
- 공간 검색용 인덱스
8.3 B-Tree 인덱스
- B-Tree 의 B는 Balanced를 의미한다.
- B-Tree는 컬럼의 원래 값을 변형시키지 않고 인덱스 구조체 내에서는 정렬된 상태로 유지한다.
- 대부분 인덱스는 B-Tree를 사용할 정도로 일반적인 용도에 적합하다.
8.3.1 구조 및 특성
- 루트 노드
- 브랜치 노드
- 리프 노드 : 인덱스의 리프노드는 항상 실제 데이터 레코드를 찾아가기 위한 주솟값을 가지고 있다.
- 인덱스의 키 값은 모두 정렬되어 있지만 데이터 파일의 레코드는 정렬돼 있지 않다.
8.3.2 B-Tree 인덱스 키 추가 및 삭제
- 레코드를 저장하거나 변경할 때 인덱스 키 추가나 삭제 작업이 발생한다.
8.3.2.1 인덱스 키 추가
- 스토리지 엔진에 따라 즉시 인덱스에 저장될 수도 있고 그렇지 않을 수도 있다.
- B-Tree상에 위치를 검색하고 위치가 결정되면 레코드의 키 값과 주소 정보를 리프 노드에 저장한다.
- 만약 리프노드가 꽉 찼다면 split 하여 저장한다. => 저장 비용이 많이 발생
8.3.2.2 인덱스 키 삭제
- 해당 키 값이 저장된 B-Tree 리프 노드를 찾아서 삭제 마크를 한다.
- 나중에 재활용 되거나 그대로 방치 될 수 있다.
8.3.2.3 인덱스 키 변경
- B-Tree의 키 값 변경은 먼저 키를 삭제하고 다시 새로운 키를 추가한다.
8.3.2.4 인덱스 키 검색
- insert, update, delete에서 추가비용이 들면서까지 인덱스를 관리하는 이유는 빠른 검색을 위해서이다.
- 인덱스를 검색하는 작업을 트리 탐색이라고 하는데 SELECT 뿐아니라 UPDATE, DELETE에서도 트리 탐색이 사용된다.
- InooDB 스토리지 엔진에서 인덱스는 보다 특별한 의미가 있는데 레코드 잠금 시 인덱스를 잠근 후 테이블의 레코드를 잠근다.
- 따라서 적절한 인덱스를 설정하지 않으면 불필요하게 많은 레코드를 잠그게 될 수 있어서 주의해야 한다.
8.3.3 B-Tree 인덱스 사용에 영향을 미치는 요소
- B-Tree 인덱스는 인덱스를 구성하는 칼럼의 크기와 레코드의 건수, 그리고 유니크한 인덱스 키 값의 개수 등에 의해 검색이나 변경 작업의 성능이 영향을 받는다.
8.3.3.1 인덱스 키 값의 크기
- 디스크에 데이터를 저장하는 가장 기본 단위를 페이지 또는 블록이라고 한다.
- 디스크의 모든 읽기 및 쓰기 작업의 최소 작업 단위가 된다.
- 또한, 페이지는 InnoDB 스토리지 엔진의 버퍼 풀에서 데이터를 버퍼링하는 기본 단위이다.
- 인덱스도 결국 페이지 단위로 관리된다.
- 일반적으로 DBMS의 B-Tree는 자식 노드의 개수가 가변적인 구조다.
- 인덱스 키 값의 크기에 따라 하나의 인덱스 페이지에 저장할 수 있는 키의 개수가 정해질 수 있다.
- 예를들어 페이지 기본 크기인 16KB를 기준으로 인덱스의 키가 16 바이트, 자식 노드 주소영역이 12바이트로 구성했을때?
- 16 * 1024 / (16 + 12) = 585
- 하나의 노드에 자식 노드 585개를 가질 수 있다.
- 인덱스 키 값이 32바이트로 늘어났다고 가정하면 16 * 1024/(32 + 12) = 372, 즉, 372개의 자식노드를 가질 수 있다.
8.3.3.2 B-Tree 깊이
- 인덱스의 깊이(Depth)는 상당히 중요하지만 직접적으로 제어하긴 어렵다.
- 그렇지만, 깊이에 따른 최대 가질 수 있는 키의 개수는 구할 수 있다.
- 깊이가 3인 B-Tree를 생각해보자.
- 키 값의 크기가 16바이트 일 때 최대 585 * 585 * 585 (약 2억) 개 정도의 키 값을 담을 수 있다.
- 키 값의 크기가 32 바이트로 늘어난다면 372 * 372 * 372 (약 5천만) 개로 줄어든다.
- 키 값의 크기가 커지면 담을 수 있는 키의 개수가 적어지고 깊이가 늘어날 수 있으므로 최대한 작게 만드는게 좋다.
8.3.3.3 선택도(기수성) Selectivity ( Cardinality )
- 모든 인덱스 값 가운데 유니크한 값의 수
- 인덱스는 선택도가 높을수록 검색 대상이 줄어들기 때문에 그만큼 빠르게 처리된다.
- 인덱스에서 유니크한 값의 개수는 인덱스나 쿼리의 효율성에 큰 영향을 미친다.
8.3.3.4 읽어야 하는 레코드의 건수
- 인덱스를 통해 테이블의 레코드를 읽는게 과연 인덱스를 거치지 않고 읽는 것보다 이득일까?
- 예를들어 100만건의 레코드가 들어있는 테이블이 있다고 가정하자. 여기서 50만건의 데이터를 읽으려고 하는데 전체 데이터를 스캔하는 것과 인덱스를 통해 필요한 50만건의 데이터를 조회하는 것 중에 무엇이 더 빠를까?
- 일반적으로 DBMS의 옵티마이저는 인덱스를 통해 레코드를 읽는것이 테이블에서 직접 레코드를 읽는 것 보다 4~5배 정도의 비용이 더 많이 드는 작업으로 예측한다.
- 즉, 인덱스를 통해 읽어야 할 레코드의 건수가 전체 테이블 레코드의 20~25%를 넘어가면 인덱스를 이용하지 않고 테이블을 모두 직접 읽어서 필요한 레코드만 가려내는 방식으로 처리하는 것이 효율적이다.
- 전체 100만건의 레코드 가운데 50만건을 읽어야 하는 작업은 인덱스의 손익 분기점인 20~25%보다 훨씬 크기 때문에 MySQL 옵티마이저는 인덱스를 이용하지 않고 직접 테이블을 스캔하여 처리할 것이다.
8.3.4 B-Tree 인덱스를 통한 데이터 읽기
8.3.4.1 인덱스 레인지 스캔
- 한 건만 읽는 경우, 한 건 이상 읽는 경우! 둘 다 인덱스 레인지 스캔으로 설명한다. (10장 실행 계획 파트에서 차이점을 자세히 다룬다.)
select * from employees where first_name between 'Ebbe' and 'Gad';
- 인덱스 레인지 스캔의 3단계
- 인덱스에서 조건을 만족하는 값이 저장된 위치를 찾는다. (index seek)
- 1번에서 탐색된 위치부터 필요한 만큼 인덱스를 차례대로 쭉 읽는다. (index scan)
- 2번에서 읽어 들인 인덱스 키와 레코드 주소를 이용해 레코드가 저장된 페이지를 가져오고, 최종 레코드를 읽어온다.
- 쿼리가 필요로 하는 데이터에 따라 3번 과정은 필요하지 않을 수도 있는데, 이를 커버링 인덱스라고 한다.
- 커버링 인덱스로 처리되는 쿼리는 디스크의 레코드를 읽지 않아도 되기 때문에 랜덤 읽기가 사앙히 줄어들고 성능은 빨라진다.
8.3.4.2 인덱스 풀 스캔
- 인덱스 레인지 스캔과 달리 인덱스의 처음부터 끝까지 모두 읽는 방식
- 쿼리의 조건절에 사용된 칼럼이 인덱스의 첫 번째 칼럼이 아닌 경우 인덱스 풀 스캔 방식이 사용된다.
- 예를들어 인덱스는 (A,B,C)의 순서로 만들어져 있지만 쿼리의 조건절은 B 칼럼이나 C 칼럼으로 검색하는 경우다.
- 일반적으로 인덱스의 크기가 테이블의 크기보다 작으므로 직접 테이블을 처음부터 끝까지 읽는 것보다 인덱스만 읽는 것이 효율적이다. 하지만 인덱스뿐만 아니라 데이터 레코드까지 모두 읽어야 한다면 이 방식으로 처리되지 않는다.
8.3.4.3 루스(Loose) 인덱스 스캔
- 오라클에서는 이를 인덱스 스킵 스캔이라고도 부른다.
- 앞에 소개한 두 가지 스캔(인덱스 레인지, 풀 스캔)의 경우 상반된 의미인 Tight 인덱스 스캔이라고 부른다.
- 루스 인덱스 스캔은 말 그대로 느슨하게, 듬성듬성하게 인덱스를 읽는 것을 의미한다.
- 일반적으로 Group By 또는 Max, Min 함수에 대해 최적화 하는 경우에 사용된다.
select dept_no, MIN(emp_no)
from dept_emp
where dept_no between 'd001' and 'd004'
group by dept_no;
8.3.4.4 인덱스 스킵 스캔
- 인덱스의 핵심은 값이 정렬돼 있다는 것이며, 이로 인해 인덱스를 구성하는 칼럼의 순서가 매우 중요하다.
- 다음과 같은 인덱스를 생성했다고 해보자.
ALTER TABLE employees ADD INDEX ix_gender_birthdate (gender, birth_date);
- 이 인덱스를 사용하려면 인덱스의 첫번째 칼럼인 gender가 조건절에 들어가야 한다.
-- 인덱스를 사용하지 못하는 쿼리
select * from employees where birth_date >= '1992-01-12';
-- 인덱스를 사용할 수 있는 쿼리
select * from employees where gender = 'M' birth_date >= '1992-01-12';
- 그치만 MySQL 8.0 버전부터는 옵티마이저가 gender 칼럼을 건너뛰어서 birth_date 칼럼만으로도 인덱스 검색이 가능하게 해주는 인덱스 스킵 스캔 최적화 기능이 도입되었다.
- MySQL 옵티마이저는 우선 gender 칼럼에서 유니크한 값을 모두 조회해서 주어진 쿼리에 gender 칼럼의 조건을 추가해서 쿼리를 다시 실행하는 형태로 처리한다.
- 단점
- where 절에 없는 인덱스의 선행 칼럼의 유니크한 값의 개수가 적어야한다.
- 쿼리가 인덱스에 존재하는 칼럼만으로 처리 가능해야 한다.(커버링 인덱스)ㅓ
8.3.5 다중 칼럼 인덱스
- 두 개 이상의 칼럼으로 구성된 인덱스를 다중 칼럼 인덱스라고 한다.
- 복합 칼럼 인덱스, 또는 2개 이상의 칼럼이 연결되었다고 해서 Concatenated Index 라고도 한다.
- 인덱스의 두 번째 칼럼은 첫 번째 칼럼에 의존해서 정렬돼 있다.
8.3.6 B-Tree 인덱스의 정렬 및 스캔 방향
- 인덱스를 생성할 때 설정한 정렬 규칙에 따라서 인덱스의 키 값은 항상 오름차순이거나 내림차순으로 정렬되어 저장된다.
- 하지만 인덱스가 오름차순으로 생성됐다고해서 그 인덱스를 오름차순으로만 읽을 수 있는 것은 아니다.
- 그 인덱스를 거꾸로 읽으면 내림차순으로 정렬된 인덱스로도 사용할 수 있다.
- 인덱스를 어느 방향으로 읽을지는 옵티마이저가 실시간으로 만들어내는 실행 계획에 따라 결정된다.
8.3.6.1 인덱스의 정렬
Create index ix_teamname_userscore on employees (team_name asc, user_score desc);
- 위와 같이 MySQL 8.0부터는 정렬 순서를 혼합한 인덱스를 생성할 수 있게 됐다.
8.3.6.1.1 인덱스 스캔 방향
SELECT *
FROM employees
ORDER BY first_name DESC
LIMIT 1;
- 위의 쿼리는 인덱스를 처음부터 오름차순으로 끝까지 읽어서 first_name이 가장 큰 값 하나를 가져오는 것일까?
- 그렇지 않다. 인덱스는 오름차순으로 정렬되어 있어도 최솟값브터 읽으면 오름차순으로 가져올 수 있고, 최댓값부터 거꾸로 읽으면 내림차순으로 가져올 수 있다.
- MySQL 옵티마이저는 이미 알고 있다.
- 쿼리의 ORDER BY 처리나 MIN, MAX 함수의 최적화가 필요한 경우에도 MySQL 옵티마이저는 인덱스의 읽기 방향을 전환해서 사용하도록 실행 계획을 만들어 낸다.
8.3.6.1.2 내림차순 인덱스
- first_name 칼럼의 역순 정렬을 위주로 사용하는 테이블에 다음과 같이 인덱스를 추가한다고 해보자.
create index ix_firstname_asc ON employees (first_name ASC);
create index ix_firstname_desc ON employees (first_name DESC);
- 인덱스를 거꾸로 읽는 역순 스캔은 정순 스캔에 비해 느릴 수 밖에 없다.
- 페이지 잠금이 인덱스 정순 스캔에 적합한 구조이다.
- 페이지 내에서 인덱스 레코드가 단방향으로만 연결된 구조
8.3.7 B-Tree 인덱스의 가용성과 효율성
- 인덱스로 설정되어 있다고 해서 해당 칼럼으로 조건문을 걸었을 때 모두 인덱스를 사용할 수 있는 것은 아니다.
- 쿼리의 Where 조건이나 Group by, Order by 절이 어떤 경우에 인덱스를 사용할 수 있고 어떤 방식으로 사용할 수 있는지 식별할 수 있어야 한다.
- 그래야만 쿼리의 조건을 최적화하거나, 역으로 쿼리에 맞는 인덱스를 최적으로 생성할 수 있다.
8.3.7.1 비교 조건의 종류와 효율성
select * from dept_emp
where dept_no = 'd002' and emp_no >= 10114;
- 위의 쿼리를 실행할 때 다음 각 인덱스에서 어떻게 차이가 있는지 살펴보자.
- 케이스 A: INDEX(dept_no, emp_no)
- 케이스 B: INDEX(emp_no, dept_no)
- 케이스 A 인덱스는 dept_no = 'd002' and emp_no >= 10114 인 레코드를 찾고 그 이후에는 dept_no 가 'd002'가 아닐 때까지 쭉 읽으면 된다.
- 케이스 B 인덱스의 경우에는 dept_no = 'd002' and emp_no >= 10114 인 레코드를 찾고 그 이후 모든 레코드에 대해 dept_no가 'd002'인지 비교하는 과정을 거쳐야 한다.
- 이처럼 인덱스 A에서의 두 조건과 같이 작업의 범위를 결정하는 조건을 '작업 범위 결정 조건'이라 하고
- 케이스 B에서 dept_no='d002'와 같이 작업 범위를 좁히지 못하고 거르는 역할을 하는 조건을 '필터링 조건' 또는 '체크 조건'이라 한다.
8.3.7.2 인덱스의 가용성
- B-Tree 인덱스의 특징은 왼쪽 값에 기준해서(Left-most) 오른쪽 값이 정렬돼 있다는 것이다.
- 여기서 왼쪽이란 하나의 칼럼 내에서뿐만 아니라 다중 칼럼 인덱스의 칼럼에 대해서도 함께 적용된다.
select * from employees where first_name like '%mer';
- 위의 쿼리는 인덱스 레인지 스캔 방식으로 인덱스를 이용할 수 없다.
- 정렬 우선순위가 낮은 뒷부분의 값만으로는 왼쪽 기준(Left-most) 정렬 기반의 인덱스인 B-tree에서는 인덱스의 효과를 얻을 수 없다.
8.3.7.3 가용성과 효율성 판단
- B-tree 인덱스의 특성상 다음 조건에서는 작업 범위 결정 조건으로 사용할 수 없다.
- Not-Equal로 비교된 경우( "<>", "Not in", "not between", "is not null")
- Like '%??' : 앞부분이 아닌 뒷부분 일치 형태로 문자열 패턴이 비교된 경우
- 스토어드 함수나 다른 연산자로 인덱스 칼럼이 변형된 후 비교된 경우
- where substring(column, 1,1) = 'x'
- Not-deterministic속성의 스토어드 함수가 비교 조건에 사용된 경우
- 데이터 타입이 서로 다른 비교
- where char_column = 10 (15장 데이터 타입 참조)
- 문자열 데이터 타입의 콜레이션이 다른 경우 (15장 데이터 타입 참조)
- MySQL에서는 Null 값도 인덱스에 저장된다.
8. 4 R-Tree 인덱스
- 공간 인덱스(Spatial Index)는 R-Tree 인덱스 알고리즘을 이용해 2차원의 데이터를 인덱싱하고 검색하는 목적의 인덱스다.
- 기본적인 내부 매커니즘은 B-Tree와 비슷하지만 인덱스를 구성하는 칼럼의 값이 1차원 스칼라 값인지 2차원 공간 개념 값인지가 다르다.
- 최근 GPS나 지도 서비스와 같이 위치 기반 서비스를 구현하는 방법 중에 하나로 MySQL의 공간 확장(Spatial Extension)을 이용하면 간단하게 이러한 기능을 구현할 수 있다.
- 공간 확장의 세 가지 기능 (더 자세한 내용은 12.2절 공간 검색 참조)
- 공간 데이터를 저장할 수 있는 데이터 타입
- 공간 데이터의 검색을 위한 공간 인덱스(R-Tree 알고리즘)
- 공간 데이터의 연산 함수(거리 또는 포함 관계의 처리)
8.4.1 구조 및 특성
- 데이터 타입
- POINT
- LINE
- POLYGON
- GEOMETRY : GEOMETRY 타입은 나머지 세개 타입의 슈퍼 타입으로, 다른 타입의 객체를 모두 저장할 수 있다.
- MBR이란 Minimum Bounding Rectangle의 약자로 해당 도형을 감싸는 최소 크기의 사각형을 의미한다. 이 사각형들의 포함 관계를 B-Tree 형태로 구현한 인덱스가 R-Tree 인덱스다.
- 다음의 그림은 MBR을 3개의 레벨로 나눠서 그려본 것이다.
- 이를 R-Tree 인덱스로 다음과 같이 표현할 수 있다.
8.4.2 R-Tree 인덱스의 용도
- R-Tree는 앞에서 언급한 MBR 정보를 이용해 B-Tree 형태로 인덱스를 구축하므로 Rectangle의 'R'과 B-Tree의 'Tree'를 섞어서 R-Tree라는 이름이 붙여졌으며, 공간(Spatial) 인덱스라고도 한다.
- 일반적으로 위도, 경도 좌표 저장에 주로 사용된다.
8.5 전문 검색 인덱스
- 지금까지 살펴본 인덱스 알고리즘은 일반적으로 크지 않은 데이터에 대한 인덱싱 알고리즘이었다.
- 대표적으로 MySQL의 B-Tree 인덱스는 실제 컬럼이 1MB이더라도 전체 값을 인덱스 키로 사용하는 것이 아니라, 1000 바이트 또는 3072바이트 까지만 잘라서 인덱스 키로 사용한다. 또한 B-Tree 인덱스의 특성에서도 알아봤듯이 전체 일치 또는 좌측 일부 일치와 같은 검색만 가능하다.
- 문서 내용 전체를 인덱스화해서 특정 키워드가 포함된 문서를 검색하는 전문(Full Text) 검색에는 일반적인 B-Tree 인덱스를 사용할 수 없다.
- 문서 전체에 대한 분석과 검색을 위한 인덱싱 알고리즘을 전문 검색 인덱스라고 한다.
8.5.1 인덱스 알고리즘
- 전문 검색에서는 문서 본문의 내용에서 사용자가 검색할 키워드를 분석해 내고, 이를 빠른 검색용으로 사용할 수 있게 인덱스를 구축한다.
8.5.1.1 어근 분석 알고리즘
- MySQL의 전문 검색 알고리즘은 다음 두 가지 중요한 과정을 거쳐서 인덱싱 작업을 한다.
- 불용어 처리(Stop word)
- 검색에서 별 가치가 없는 단어를 모두 필터링해서 제거하는 작업
- 모두 상수로 정의해서 사용하는 경우가 많고, 데이터베이스화해서 사용자가 추가하거나 삭제할 수 있게 구현하는 경우도 있다.
- 어근 분석(Stemming)
- 검색어로 선정된 단어의 뿌리인 원형을 찾는 작업이다.
- MySQL에서는 형태소 분석 라이브러리인 MeCab을 플러그인 형태로 사용할 수 있게 지원한다.
- MeCab을 MySQL 서버에 적용하는 방법은 어렵지 않지만 한글에 맞게 완성도를 갖추는 작업은 많은 시간과 노력이 필요하다.
- 불용어 처리(Stop word)
8.5.1.2 n-gram 알고리즘
- MeCab을 위한 형태소 분석은 매우 전문적인 전문 검색 알고리즘이어서 만족할 만한 결과를 내려면 많은 시간과 노력이 필요하다.
- n-gram은 단순히 키워드를 검색해내기 위한 인덱싱 알고리즘이라서 보다 범용적으로 사용할 수 있다.
- n-gram이란 본문을 무조건 몇 글자씩 잘라서 인덱싱하는 방법이다.
- n-gram에서 n은 인덱싱할 키워드의 최소 글자 수를 의미하는데, 일반적으로 2글자 단위로 키워드를 쪼개서 인덱싱하는 2-gram 방식이 많이 사용된다.
- 2-gram 알고리즘으로 다음 문장의 토큰을 분리하는 방법을 살펴보자.
- To be or not to be. That is the question
- MySQL 서버는 이렇게 생성된 토큰들에 대해 불용어를 걸러내는 작업을 수행한다.
- MySQL 서버에 내장된 불용어는 information_schema.innodb_ft_default_stopword테이블을 통해 확인할 수 있다.
- MySQL 서버는 이렇게 구분된 토큰을 단순한 B-Tree 인덱스에 저장한다.
8.5.1.3 불용어 변경 및 삭제
- 전문 검색 인덱스의 불용어 처리 무시
- 스토리지 엔진에 관계없이 MySQL 서버의 모든 전문 검색 인덱스에 대해 불용어를 완전히 제거하는 방법
- my.cnf파일의 fn_stopword_file 시스템 변수에 빈 문자열을 설정하면 된다.
- innoDB 테이블의 전문 검색 인덱스의 불용어 처리를 무시하려면 innodb_ft_enable_stopword 시스템 변수를 OFF로 설정하면 된다.
- 스토리지 엔진에 관계없이 MySQL 서버의 모든 전문 검색 인덱스에 대해 불용어를 완전히 제거하는 방법
- 사용자 정의 불용어 사용
- 불용어 목록을 파일로 저장하고 my.cnf파일의 ft_stopword_file 설정에 등록한다.
- 불용어의 목록을 테이블로 저장하는 방법
- 불용어 테이블을 생성하고 innodb_ft_server_stopword_table 시스템 변수에 불용어 테이블을 설정한다. 이때 불용어 목록을 변경한 이후 전문 검색 인덱스가 생성돼야만 변경된 불용어가 적용된다. (순서 중요)
8.5.2 전문 검색 인덱스의 가용성
- 전문 검색 인덱스의 사용
- 쿼리 문장이 전문 검색을 위한 문법(MATCH ~ AGAINST ~)을 사용해야 한다.
- 테이블이 전문 검색 대상 컬럼에 대해서 전문 인덱스를 보유해야 한다.
CREATE TABLE tb_test (
doc_id INT,
doc_body TEXT,
PRIMARY KEY (doc_id),
FULLTEXT KEY fx_docbody (doc_body) WITH PARSER ngram
) ENGINE=InnoDB;
- 위와 같이 테이블에 doc_body 칼럼에 전문 검색 인덱스를 생성했다고 해보자.
select * from tb_test where doc_body like '%애플%' ;
- 위와 같은 검색 쿼리는 어떻게 작동할까?
- 전문 검색 인덱스를 이용해 효율적으로 쿼리가 작동했을까?
- 아니다. 테이블을 처음부터 끝까지 읽는 풀 테이블 스캔으로 쿼리를 처리한다.
- 전문 검색 인덱스를 사용하려면 반드시 다음과 같이 MATCH ~ AGAINST ~ 구문으로 검색 쿼리를 작성해야 한다.
select * FROM tb_test where match(doc_body) against('애플' in boolean mode);
'DB' 카테고리의 다른 글
커넥션풀과 데이터소스 이해 (0) | 2022.06.05 |
---|---|
JDBC란? (SQL Mapper, ORM) (0) | 2022.06.03 |
Real MySQL 6장 데이터압축, 7장 데이터 암호화 (0) | 2022.05.26 |
Real MySQL 5장 트랜잭션과 잠금 (0) | 2022.05.25 |
Real MySQL 4장 아키텍처 (0) | 2022.05.24 |