본문 바로가기

DB

Real MySQL 8장 인덱스 #1 (~8.5)

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단계
    1. 인덱스에서 조건을 만족하는 값이 저장된 위치를 찾는다. (index seek)
    2. 1번에서 탐색된 위치부터 필요한 만큼 인덱스를 차례대로 쭉 읽는다. (index scan)
    3. 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 서버에 적용하는 방법은 어렵지 않지만 한글에 맞게 완성도를 갖추는 작업은 많은 시간과 노력이 필요하다.

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로 설정하면 된다.
  • 사용자 정의 불용어 사용
    • 불용어 목록을 파일로 저장하고 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