////
Search

8장 - 인덱스

Created
2022/09/03 11:32
Tags
각 인덱스의 특성과 차이는 상당히 중요
물리 수준의 모델링을 할 때도 중요한 요소가 됨
아무리 서버의 옵티마이저가 발전하고 성능이 개선되어도 여전히 관리자의 역할은 매우 중요
인덱스에 대한 기본 지식은 지금도 앞으로도 개발자나 관리자에게 매우 중요한 부분
쿼리 튜닝의 기본이 될것

8.1 디스크 읽기 방식

데이터베이스의 성능 튜닝은 어떻게 디스크 I/O를 줄이느냐가 관건일 때가 상당히 많음

8.1.1 HDD와 SSD

컴퓨터에서 CPU나 메모리 같은 주요 장치는 대부분 전자식 장치지만 HDD는 기계식 장치
때문에 DB에서 디스크는 늘 병목이 됨
HDD를 대체하기 위해 전자식 저장장치인 SSD가 많이 출시되고 있음
주요 장치의 초당 처리 횟수
SSD와 HDD의 성능 벤치마크
초당 처리 가능한 연산의 횟수를 의미하므로 이 값이 클수록 처리 속도가 빠르다는 것을 의미
메모리와 디스크의 처리 속도는 10만 배 이상의 차이를 보임
디스크의 헤더를 움직이지 않고 한 번에 많은 데이터를 읽는 순차 I/O
SSD가 하드 디스크 드라이브보다 조금 빠르거나 거의 비슷한 성능을 보이기도 함
하지만 SSD의 장점은 기존 HDD보다 랜덤 I/O가 훨씬 빠르다는 것
DB에선 순차 I/O 작업의 비중이 크지 않고 랜덤 I/O를 통해 작은 데이터를 읽고 쓰는 작업이 대부분
일반적인 웹 서비스 환경의 데이터베이스에서는 SSD가 HDD 보다 훨씬 빠르다.

8.1.2 랜덤 I/O와 순차 I/O

순차 I/O(왼쪽)와 랜덤 I/O(오른쪽) 비교
랜덤 I/O는 HDD의 플래터(원판)를 돌려서 데이터가 저장된 위치로 디스크 헤더를 이동시킨 다음 데이터를 읽는 것을 의미
3개의 페이지를 기록하기 위한 시스템콜 요청 횟수 차이
순차 I/O는 1번 시스템 콜을 요청
1번의 헤더를 이동한 것을 의미
랜덤 I/O는 3번 시스템 콜을 요청
3번의 헤더를 이동한 것을 의미
디스크의 성능은 헤더의 위치 이동 없이 얼마나 많은 데이터를 한 번에 기록하느냐에 의해 결정 된다고 볼 수 있음
때문에 헤더를 많이 이동하는 랜덤 I/O 작업이 작업 부하가 훨씬 더 큼
SSD는 랜덤 I/O와 순차 I/O의 차이가 없을 것으로 예측하지만, 실제로는 그렇지 않다.
SSD도 랜덤 I/O는 순차 I/O 보다 전체 스루풋이 떨어진다.
사실 쿼리를 튜닝해서 랜덤 I/O를 순차 I/O로 바꿔서 실행할 방법은 그다지 많지 않음
일반적으로 쿼리를 튜닝하는 것은 랜덤 I/O 자체를 줄여주는 것이 목적!

8.2 인덱스란?

많은 사람이 인덱스를 언급할 때는 항상 책의 맨 끝에 있는 찾아보기(A.K.A. 색인)로 설명함
찾아보기가 인덱스라면 내용은 데이터 파일임
DBMS도 테이블의 모든 데이터를 검색해서 원하는 결과를 가져오려면 시간이 오래 걸림
그래서 칼럼의 값과 해당 레코드가 저장된 주소를 키와 값으로 삼아 인덱스를 만들어 두는 것
찾아보기와 인덱스의 공통점 가운데 중요한 것이 바로 정렬
인덱스는 칼럼의 값을 주어진 순서로 미리 정렬 후 보관함
DBMS에서 인덱스는 데이터의 저장(INSERT, UPDATE, DELETE) 성능을 희생하고 그 대신 데이터의 읽기 속도를 높이는 기능
데이터의 저장 속도를 어디까지 희생할 수 있는지, 읽기 속도를 얼마나 더 빠르게 만들어야 하느냐에 따라 결정해야 한다.
검색 쿼리 문장의 조건절에 사용되는 칼럼이라고 해서 전부 인덱스로 생성하면 데이터 저장 성능이 떨어지고 인덱스의 크기가 비대해져 오히려 역효과만 불러올 수 있다.
인덱스는 데이터를 관리하는 방식(알고리즘)과 중복 값의 허용 여부 등에 따라 여러 가지로 나눠볼 수 있다.
인덱스를 역할별로 구분해 본다면 프라이머리 키(Primary key)와 보조 키(세컨더리 인덱스, Secondary key)로 구분할 수 있다.
프라이머리 키는 이미 잘 아는 것처럼 그 레코드를 대표하는 칼럼의 값으로 만들어진 인덱스를 의미
프라이머리 키를 제외한 나머지 모든 인덱스는 세컨더리 인덱스(Secondary Index)로 분류한다.
해당 레코드를 식별할 수 있는 기준값이 됨 우리는 이를 식별자라고도 부른다.
프라이머리 키는 NULL 값을 허용하지 않으며 중복을 허용하지 않는 것이 특징
인덱스는 대표적으로 B-Tree 인덱스와 Hash 인덱스로 구분할 수 있다
최근에는 Fractal-Tree 인덱스나 로그 기반의 Merge-Tree 인덱스와 같은 알고리즘을 사용하는 DBMS도 개발되고 있다
B-Tree 알고리즘은 가장 일반적으로 사용되는 인덱스 알고리즘
오래전 도입되어 상당히 성숙한 상태
칼럼의 값을 변형하지 않고 원래의 값을 이용해 인덱싱하는 알고리즘이다.
Hash 인덱스 알고리즘은 칼럼의 값으로 해시값을 계산해서 인덱싱하는 알고리즘
범위 검색시 인덱싱 사용 불가
메모리 기반 DB에서 많이 사용 됨
인덱스는 유니크/논유니크 인덱스로 구분할 수 있음
단순히 데이터가 1개만 존재하는지 아닌지를 의미하지만 옵티마이저에겐 상당히 중요

8.3 B-Tree 인덱스

B-Tree는 데이터베이스의 인덱싱 알고리즘 가운데 가장 일반적으로 사용되고, 가장 먼저 도입된 알 고리즘이다. 하지만 아직도 가장 범용적인 목적으로 사용되는 인덱스 알고리즘이다.
일반적으로 DBMS에서는 주로 B+-Tree 또는 B*-Tree 가 사용된다.
B-Tree의 B는 Binary가 아닌 Balanced다!
칼럼의 원래 값을 변형시키지 않고 인덱스 구조체 내에서는 항상 정렬된 상태로 유지
전문 검색이 아닌 경우 대다수의 인덱스는 B-Tree를 사용

8.3.1 구조 및 특성

B-Tree는 트리 구조의 최상위에 하나의 Root Node가 존재하고 그 하위에 자식노드가 붙어 있는 형태
가장 하위에 있는 노드를 Leaf Node라 한다.
루트도 리프도 아닌 노드를 Branch Node라고 한다.
인덱스의 리프 노드는 항상 실제 데이터 레코드를 찾아가기 위한 주솟값을 가지고 있다.
B-Tree 인덱스의 구조
인덱스는 정렬되어 있지만, 데이터 파일은 그렇지 않다.
파일의 레코드는 INSERT된 순서대로 저장되는 것으로 생각하지만 그렇지 않다.
만약 테이블의 레코드를 전혀 삭제하거나 변경하지 않고 INSERT만 수행한다면 맞을 수도 있다
B-Tree의 리프 노드와 테이블 데이터 레코드(MyISAM)
레코드 주소는 MyISAM 테이블의 생성 옵션에 따라 레코드가 테이블에 INSERT된 순번이거나 데이터 파일 내의 위치
B-Tree의 리프 노드와 테이블 데이터 레코드(InnoDB)
InnoDB 스토리지 엔진을 사용하는 테이블에서는 프라이머리 키가 ROWID의 역할을 함
두 스토리지 엔진의 인덱스에서 가장 큰 차이점은 세컨더리 인덱스를 통해 데이터 파일의 레코드를 찾아가는 방법
MyISAM 테이블은 세컨더리 인덱스가 물리적인 주소를 가짐
InnoDB 테이블은 프 라이머리 키를 주소처럼 사용하기 때문에 논리적인 주소를 가짐
InnoDB는 인덱스를 통해 파일을 바로 찾아기는게 아닌 PK를 이용해 리프 페이지를 검색해 레코드를 읽어야 한다.
InnoDB 스토리지 엔진에서는 모든 세컨더리 인덱스 검색에서 데이터 레코드를 읽기 위해 서는 반드시 프라이머리 키를 저장하고 있는 B-Tree를 다시 한번 검색해야 함

8.3.2 B-Tree 인덱스 키 추가 및 삭제

8.3.2.1 인덱스 키 추가

새로운 키 값이 B-Tree에 저장될 때 테이블의 스토리지 엔진에 따라 새로운 키 값이 즉시 인덱스에 저장될 수도 있고 그렇지 않을 수도 있음
저장될 위치가 결정되면 레코드의 키 값과 대상 레코드의 주소 정보를 B-Tree의 리프 노드에 저장 함
리프 노드가 꽉 차서 더는 저장할 수 없을 때는 리프 노드가 분리해야 함
이는 상위 브랜치 노드까지 처리의 범위가 넓어 짐
이러한 작업 탓에 B-Tree는 상대적으로 쓰기 작업에 비용이 많이 드는 것으로 알려졌다.
일반적으로 테이블에 인덱스가 3개가 있다면
이때 테이블에 인덱스가 하나도 없는 경우는 작업 비용이 1이고, 3개인 경우에는 5.5 정도의 비용(1.5 * 3 + 1) 정도로 예측
중요한건 대다수 디스크로 인덱스 페이지를 읽고 쓰는 비용이 오래걸림
MyISAM/MEMORY 스토리지 엔진을 사용하는 테이블에서는 INSERT 문장이 실행되면 즉시 새로 운 키 값을 B-Tree 인덱스에 변경한다.
InnoDB 스토리지 엔진은 이 작업을 조금 더 지능적으로 처리함
필요하다면 인덱스 키 추가 작업을 지연시켜 나중에 처리할 수 있다.
하지만 프라이 머리 키나 유니크 인덱스의 경우 중복 체크가 필요하기 때문에 즉시 B-Tree에 추가하거나 삭제한다.

8.3.2.2 인덱스 키 삭제

B-Tree의 키 값이 삭제되는 경우는 상당히 간단
해당 키 값이 저장된 B-Tree의 리프 노드를 찾아서 그냥 삭제 마크만 하면 작업이 완료된다.
인덱스 키 삭제로 인한 마킹 작업 또한 디스크 쓰기가 필요하므로 이 작업 역 시 디스크 I/O가 필요한 작업이다.
InnoDB에선 이 작업 또한 버퍼링 되어 지연 처리될 수 있음
MyISAM/MEMORY 스토리지 엔진의 테이블에서는 체인지 버퍼와 같은 기능이 없으므로 인덱스 키 삭제가 완료된 후 쿼리 실행이 완료됨

8.3.2.3 인덱스 키 변경

인덱스의 키 값은 그 값에 따라 저장될 리프 노드의 위치가 결정됨
B-Tree의 키 값이 변경되는 경우에는 단순히 인덱스상의 키 값만 변경하는 것은 불가능
B-Tree의 키 값 변경 작업은 먼 저 키 값을 삭제 후, 다시 새로운 키 값을 추가하는 형태로 처리 됨
키 값 변경시 앞서 말한 삭제/추가 작업의 절차대로 처리 됨
InnoDB에선 이 작업을 모두 체인지 버퍼를 활용해 지연처리 가능

8.3.2.4 인덱스 키 검색

앞서 여러 작업을 실행한 이유는 빠른 검색을 위해서 임
인덱스를 검색시 B-Tree의 루트 노드부터 시작해 브랜치 노드를 거쳐 최종 리프 노드까지 이동하면서 비교 작업을 수행
이 작업을 트리 탐색이라 함
트리 탐색은 SELECT뿐만 아니라 UPDATE 나 DELETE를 처리하기 위해 항상 해당 레코드를 먼저 검색해야 할 경우에도 사용됨
B-Tree 인덱스를 이용한 검색은 100% 일치 또는 값의 앞부분만 일치하는 경우에 사용할 수 있다
부등호비교 조건에서도 인덱스를 활용할 수 있음
인덱스를 구성하는 키 값의 뒷부분만 검색하는 용도로는 인덱스를 사용할 수 없다
인덱스의 키 값에 변형이 가해진 후 비교 되는 경우에는 절대 B-Tree의 빠른 검색 기능을 사용할 수 없음
이미 변형된 값은 B-Tree 인덱스에 존재하는 값이 아니다.
따라서 함수나 연산을 수행한 결과로 정렬한다거나 검색하는 작업은 B-Tree의 장점을 이용할 수 없으므로 주의
InnoDB에서 지원하는 레코드 잠금이나 넥스트 키락(갭락)이 검색을 수행한 인덱스를 잠근 후 테이블의 레코드를 잠그는 방식으로 구현돼 있다.

8.3.3 B-Tree 인덱스 사용에 영향을 미치는 요소

B-Tree 인덱스는 인덱스를 구성하는 칼럼의 크기와 레코드의 건수, 그리고 유니크한 인덱스 키 값의 개수 등에 의해 검색이나 변경 작업의 성능이 영향을 받는다.

8.3.3.1 인덱스 키 값의 크기

InnoDB 스토리지 엔진은 디스크에 데이터를 저장하는 가장 기본 단위를 Page or Block이라고 하며, 디스크의 모든 읽기 및 쓰기 작업의 최소 작업 단위가 된다.
Page는 InnoDB의 버퍼 풀에서 데이터를 버퍼링하는 기본 단위기도 함
이진트리는 각 노드가 자식 노드를 2개만 가짐
DBMS의 B-Tree가 이진 트리라면 인덱스 검색이 상당히 비효율적일 것
일반적으로 DBMS의 B-Tree는 자식 노드의 개수가 가변적인 구조
B-Tree의 자식의 개수는 인덱스 페이지 크기와 키 값의 크기에 따라 결정 됨
InnoDB의 기본 페이지 값은 16KB
인덱스 페이지의 구성
자식 노드의 주소 영역이 12바이트로 구성된다고 가정
계산해 보면 16*1024/(16+12) = 585개 저장할 수 있다.
키 값의 크기가 두 배인 32바이트로 늘어났다고 가정하면 한 페이지에 인덱스 키를 16*1024/(32+12) = 372
검색쿼리가 500개의 데이터를 읽어야 한다고 가정한다면 앞에선 한번의 스캔으로 끝나지만, 후자는 두번의 스캔이 필요함 = 그만큼 느려짐
인덱스 키 값의 길이가 길어진다는 것은 전체적인 인덱스의 크기가 커진다는 것을 의미
인덱스를 캐시해 두는 InnoDB의 버퍼 풀이나 MyISAM의 키 캐시 영역은 크기가 제한적
인덱스 크기가 커지면 커질수록 메모리에 캐시해 둘 수 있는 레코드 수는 줄어듬
메모리의 효율이 떨어지는 결과를 가져온다.

8.3.3.2 B-Tree 깊이

B-Tree 인덱스의 깊이(Depth)는 상당히 중요하지만 직접 제어할 방법은 없다.
B-Tree의 깊이는 MySQL에서 값을 검색할 때 몇 번이나 랜덤하게 디스크를 읽어야 하는지와 직결되는 문제
B-Tree 깊이가 3인 경우
키 값이 16바이트인 경우에는 최대 2억(585 * 585 * 585)개 정도
키 값이 32바이트로 늘어나면 5천만(372 * 372 * 372)개 정도
실제로는 아무리 대용량 데이터베이스라도 B-Tree의 깊이가 5단계 이상까지 깊어 지는 경우는 흔치 않다.

8.3.3.3 선택도 (기수성)

인덱스에서 선택도(Selectivity) 또는 기수성(Cardinality)은 거의 같은 의미로 사용됨
모든 인덱스 키 값 가운데 유니크한 값의 수를 의미
전체 인덱스 100 중 유니크 값이 10이라면 기수성은 10임
인덱스 키 값 가운데 중복된 값이 많아질수록 기수성은 낮아지고 동시에 선택도 또한 떨어짐
선택도가 좋지 않다고 하더라도 정렬이나 그루핑과 같은 작업을 위해 인덱스를 만드는 것이 훨씬 나은 경우도 많다.
인덱스가 항상 검색에만 사용되는 것은 아니므로 여러 가지 용도를 고려해 적절히 인덱스를 설계할 필요가 있다.

8.3.3.4 읽어야 하는 레코드의 건수

인덱스를 통해 테이블의 레코드를 읽는 것은 인덱스를 거치지 않고 바로 테이블의 레코드를 읽는 것보 다 높은 비용이 드는 작업이다.
일반적인 DBMS의 옵티마이저에서는 인덱스를 통해 레코드 1건을 읽는 것이 테이블에서 직접 레코드 1건을 읽는 것보다 4~5배 정도 비용이 더 많이 드는 작업인 것으로 예측한다.
인덱스를 통해 읽어야 할 레코드의 건수가 전체 테이블 레코드의 20~25%를 넘어서면?
인덱스를 이용하지 않고 테이블을 모두 직접 읽어서 필요한 레코드만 필터링하는것이 효율적
이렇게 많은 레코드를 읽을 때는 강제로 인덱스를 사 용하도록 힌트를 추가해도 성능상 얻을 수 있는 이점이 없다.
물론 이러한 작업은 MySQL의 옵티마이 저가 기본적으로 힌트를 무시하고 테이블을 직접 읽는 방식으로 처리하겠지만 기본으로 알고 있어야 할 사항

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

8.3.4.1 인덱스 레인지 스캔

인덱스를 이용한 레인지 스캔
인덱스 레인지 스캔은 인덱스의 접근 방법 가운데 가장 대표적인 접근 방식으로, 뒤에서 설명할 나머지 두 가지 접근 방식보다는 빠른 방법이다.
인덱스 레인지 스캔은 검색해야 할 인덱스의 범위가 결정됐을 때 사용하는 방식이다.
검색하려는 값의 수나 검색 결과 레코드 건수와 관계없이 레인지 스캔이라고 표현
루트 노드에서부터 비교를 시작해 브랜치 노드를 거치고 최종적으로 리프 노드까지 찾아 들어가야만 비로소 필요한 레코드의 시작 지점을 찾을 수 있다.
일단 시작해야 할 위치를 찾으면 그때부 터는 리프 노드의 레코드만 순서대로 읽으면 된다.
최종적으로 스캔을 멈춰야 할 위치에 다다르면 지금까지 읽은 레코드를 사용 자에게 반환하고 쿼리를 끝낸다
인덱스 레인지 스캔을 통한 데이터 레코드 읽기
B-Tree 인덱스에서 루트와 브랜치 노드를 이용해 스캔 시작 위치를 검색
그 지점부터 필요한 방향(오름차순 또는 내림차순)으로 인덱스를 읽어 나감
중요한 것은 어떤 방식으로 스캔하든 관계없이, 해당 인덱스를 구성하는 칼럼의 정순/역순으로 정렬된 상태로 레코드를 가져온다는 것
인덱스의 리프 노드에서 검색 조건에 일치하는 건들은 데이터 파 일에서 레코드를 읽어오는 과정이 필요하다는 것
인덱스 스캔의 3단계
1.
인덱스에서 조건을 만족하는 값이 저장된 위치를 찾는다.
a.
이 과정을 인덱스 탐색(Index seek)이라고 한다.
2.
1번에서 탐색된 위치부터 필요한 만큼 인덱스를 차례대로 쭉 읽는다.
a.
이 과정을 인덱스 스캔(Index scan)이라고 한다.
b.
1번과 2번 합쳐서 인덱스 스캔으로 통칭하기도 한다.
3.
2번에서 읽어 들인 인덱스 키와 레코드 주소를 이용해 레코드가 저장된 페이지를 가져오고, 최종 레코드를 읽어 온다.

8.3.4.2 인덱스 풀 스캔

인덱스 레인지 스캔과 마찬가지로 인덱스를 사용
인덱스 레인지 스캔과는 달리 인덱스의 처음부터 끝까지 모두 읽는 방식
인덱스 풀 스캔이라고 한다.
대표적으로 쿼리의 조건절에 사용된 칼럼이 인덱스의 첫 번째 칼럼이 아닌 경우 인덱스 풀 스캔 방식이 사용 됨
일반적으로 인덱스의 크기는 테이블의 크기보다 작으므로 직접 테이블을 처음부터 끝까지 읽는 것보다 는 인덱스만 읽는 것이 효율적
쿼리가 인덱스에 명시된 칼럼만으로 조건을 처리할 수 있는 경우 주로 이 방식이 사용
레코드까지 모두 읽어야 한다면 이 방식으로 처리되지 않음
인덱스 풀 스캔
인덱스 리프 노드의 제일 앞 또는 제일 뒤로 이동한 후, 인덱스의 리프 노드를 연결하는 링크드 리스트를 따라 처음부터 끝까지 스캔하는 방식을 인덱스 풀 스캔이라고 한다.
레인지 스캔보다는 느리지만 테이블 풀 스캔보다는 효율적

8.3.4.3 루스 인덱스 스캔

루스 인덱스 스캔
많은 사용자에게 루스(Loose) 인덱스 스캔 이라는 단어는 상당히 생소할 것
루스 인덱스 스캔 이란 말 그대로 느슨하게 또는 듬성듬성하게 인덱스를 읽는 것을 의미
앞에서 소개한 두 가지 접근 방법은 루스 인덱스 스캔과는 상반된 의미에서 타이트 인덱스 스캔으로 분류
레인지 스캔과 비슷하지만 중간에 필요하지 않은 인덱스는 무시하며 다음으로 넘어가는 형태로 처리
일반적으로 GROUP BY 또는 집합 함수 가운데 MAX() 또는 MIN() 함수에 대해 최적화를 하는 경우에 사용

8.3.4.4 인덱스 스킵 스캔

데이터베이스 서버에서 인덱스의 핵심은 값이 정렬돼 있다는 것이며, 이로 인해 인덱스를 구성하는 칼 럼의 순서가 매우 중요하다.
이 인덱스를 사용하려면 WHERE 조건절에 칼럼에 대한 비교 조건이 필수
mysql> ALTER TABLE employees ADD INDEX ix_gender_birthdate (gender, birth_date); -- // 인덱스를 사용하지 못하는 쿼리 mysql> SELECT * FROM employees WHERE birth_date>='1965-02-01'; -- // 인덱스를 사용할 수 있는 쿼리 mysql> SELECT * FROM employees WHERE gender='M' AND birth_date>='1965-02-01';
SQL
복사
주로 이런 경우에는 birth_date 칼럼부터 시작하는 인덱스를 새로 생성해야만 했다.
MySQL 8.0부터는 옵티마이저가 gender 칼럼을 건너뛰어서 birth_date 칼럼만으로도 인덱스 검 색이 가능하게 해주는 인덱스 스킵 스캔(Index skip scan) 최적화 기능이 도입
이전 버전에도 인덱스 스킵 스캔과 비슷한 최적화를 수행하는 루스 인덱스 스캔이라는 기능이 있었다.
루스 인덱스 스캔은 GROUP BY 작업을 처리하기 위해 인덱스를 사용하는 경우에 만 적용할 수 있었다.
인덱스 스킵 스캔은 WHERE 조건절의 검색을 위해 사용 가능하도록 용도가 훨씬 넓어진 것
mysql> SET optimizer_switch='skip_scan=off'; mysql> EXPLAIN SELECT gender, birth_date FROM employees WHERE birth_date>='1965-02-01'; +----+-----------+-------+---------------------+--------------------------+ | id | table | type | key | Extra | +----+-----------+-------+---------------------+--------------------------+ | 1 | employees | index | ix_gender_birthdate | Using where; Using index | +----+-----------+-------+---------------------+--------------------------+
SQL
복사
MySQL 8.0 미만
위의 실행 계획에서 type 칼럼이 index라고 표시된 것은 인덱스를 처음부터 끝까지 모두 읽었다.
인덱스를 비효율적으로 사용한 것
이 쿼리는 gender 칼럼과 birth_date 칼럼만 있으면 처리를 완료할 수 있기 때문에 ix_gender_birthdate 인덱스를 풀 스캔한 것
인덱스 스킵 스캔
mysql> SET optimizer_switch='skip_scan=off'; mysql> EXPLAIN SELECT gender, birth_date FROM employees WHERE birth_date>='1965-02-01'; +----+-----------+-------+---------------------+--------------------------+ | id | table | type | key | Extra | +----+-----------+-------+---------------------+--------------------------+ | 1 | employees | range | ix_gender_birthdate | Using where; Using index | +----+-----------+-------+---------------------+--------------------------+
SQL
복사
MySQL 8.0 이후
type 칼럼의 값이 range로 표시됨, 이는 인덱스에서 꼭 필요한 부분만 읽었다는 것을 의미
그리고 실행 계획의 Extra 칼럼에 Using index for skip scan이라는 문구가 표시됨
이는 ix_gender_birthdate 인덱스에 대해 인덱스 스킵 스캔을 활용해 데이터를 조회했다는 것을 의미
인덱스 스킵 스캔은 MySQL 8.0 버전에 새로이 도입된 기능이어서 아직 다음과 같은 단점이 있다.
WHERE 조건절에 조건이 없는 인덱스의 선행 칼럼의 유니크한 값의 개수가 적어야 함
인덱스 스킵 스캔은 인덱스의 선행 칼럼이 가진 유니크한 값의 개수가 소량일 때만 적용 가능한 최적화
쿼리가 인덱스에 존재하는 칼럼만으로 처리 가능해야 함(커버링 인덱스)

8.3.5 다중 칼럼 인덱스

지금까지 살펴본 인덱스들은 모두 1개의 칼럼만 포함된 인덱스
하지만 실제 서비스용 데이터베이스에서는 2개 이상의 칼럼을 포함하는 인덱스가 더 많이 사용
두 개 이상의 칼럼으로 구성된 인덱스를 다중 칼럼 인덱스라고 하며, 또한 2개 이상의 칼럼이 연결됐다고 해서 Concatenated Index라고도 한다.
그림에서는 생략 했으나 루트 노드와 리프 노드는 항상 존재
그림에서 중요한 것은 인덱스의 두 번째 칼럼은 첫 번째 칼럼에 의존해서 정렬돼 있다는 것
두 번 째 칼럼의 정렬은 첫 번째 칼럼이 똑같은 레코드에서만 의미가 있다는 것
예제에서 emp_no 값의 정렬 순서가 빠르다고 하더라도 dept_no 칼럼의 정렬 순서가 늦다면 인덱스의 뒤쪽에 위치
예제에서 emp_no 값이 10003인 레코드가 인덱스 리프 노드의 제일 마지막에 위치하는 것
다중 칼럼 인덱스에서는 인덱스 내에서 각 칼럼의 위치(순서)가 상당히 중요하며, 그것을 아주 신중히 결정해야 함

8.3.6 B-Tree 인덱스의 정렬 및 스캔 방향

인덱스를 생성할 때 설정한 정렬 규칙에 따라 인덱스의 키 값은 항상 오름차순이거나 내림차순으로 정렬되어 저장된다.
But. 인덱스가 오름차순으로 생성되었다고 하더라도 오름차순으로만 읽을 수 있는건 아님
인덱스를 어느 방향으로 읽을지는 쿼리에 따라 옵티마이저가 만드는 실행계획에 의해 결정됨

8.3.6.1 인덱스의 정렬

일반적 사용 DBMS에서는 인덱스를 생성하는 시점에 인덱스를 구성하는 각 칼럼의 정렬을 오름/내림 차순으로 설정할 수 있다.
MySQL 5.7 버전까지는 칼럼 단위로 정렬 순서를 혼합(ASC/DESC)해서 생성할 수 없었음
이런 문제 해결을 위해 숫자 칼럼의 경우 -1을 곱해 저장하는 우회방법을 사용했었음
MySQL 8.0 부터는 다음과 같은 형태의 정렬 순서를 혼합한 인덱스도 생성할 수 있게 되었음
mysql> CREATE INDEX ix_teamname_userscore ON employees (team_name ASC, user_score DESC);
SQL
복사

5.3.6.1.1 인덱스 스캔 방향

first_name 칼럼에 대한 인덱스가 포함된 employees 테이블에 대해 아래 쿼리를 실행할때
mysql> SELECT * FROM employees ORDER BY first_name DESC LIMIT 1;
SQL
복사
인덱스는 항상 오픔차순으로만 정렬돼 있지만 인덱스를 최솟값부터 읽으면 오름차순으로 값을 가져올 수 있음
최대값부터 거꾸로 읽으면 내림차순으로 값을 가져올 수 있다는 것을 MySQL 옵티마이저는 이미 알고있음
좌-오름차순(ASC), 우-내림차순(DESC)
인덱스 생성 시점에 오름차순/내침차순으로 정렬이 결정되지만 쿼리가 그 인덱스를 사용하는 시점에 읽는 방향에 따라 오름/내림차순 정렬 효과를 얻을 수 있음!
쿼리의 ORDER BY 처리나 MIN() 또는 MAX() 함수 등의 최적화가 필요한 경우에도 옵티마이저는 인덱스의 읽기 방향을 전환해 사용하도록 실행계획을 만듦

8.3.6.1.2 내림차순 인덱스

MySQL 서버에서는 인덱스가 내림차순인지 오름차순인지 관계 없이 읽는 순서만 변경해서 해결이 가능하다.
mysql> CREATE INDEX ix_teamname_userscore ON employees (team_name ASC, user_score DESC);
SQL
복사
칼럼이 2개 이상으로 구성된 복합 인덱스는 각각의 칼럼이 내림/오름차순이 홉합된 경우 MySQL8.0의 내림차순 인덱스로만 해결이 가능하다.
first_name 칼럼을 역순으로 정렬하는 요건만 있다면 다음 2개 인덱스 중 어떤 것을 선택하는게 좋을까?

8.3.7 B-Tree 인덱스 가용성과 효율성

쿼리의 WHERE 조건이나 GROUP BY, 또는 ORDER BY 절이 어떤 경우에 인덱스를 사용할 수 있고 어떤 방식으로 사용할 수 있는지 식별할 수 있어야 한다.

8.3.7.1 비교 조건의 종류와 효율성

다중 칼럼 인덱스에서 각 칼럼의 순서와 그 칼럼에 사용된 조건이 동등(=) 비교, 아니면 크다(>) 또는 작다(<) 같은 범위 조건인지에 따라 각 인덱스 칼럼의 활용 형태가 달라지면 그 효율 또한 달라짐
mysql> SELECT * FROM dept_emp WHERE dept_no='d002' AND emp_no >= 10114;
SQL
복사
해당 쿼리를 처리하기 위해 두 가지 케이스의 인덱스를 비교해보자
A : INDEX (dept_no, emp_no)
dept_no=’d002’ AND emp_no10114 인 레코드를 찾는다
이후 dept_no 가 d002가 아닐때 까지 쭉 읽는다.
B : INDEX (emp_no, dept_no)
`emp_no>=10144 AND depth_no='d002' 인 레코드를 찾는다.
이후 모든 레코드에 대해 dept_no가 d002인이 비교하는 과정을 거친다.
인덱스를 통해 읽은 레코드가 나머지 조건에 맞는지 비교하면서 선택하는 작업 = 필터링
A와 B의 인덱스 비교가 다른이유
다중 칼럼 인덱스의 정렬 방식 때문
인덱스의 N번째 키 값은 N-1번째 키 값에 대해서 다시 정렬됨
A인덱스의 경우 2번째 컬럼인 emp_no는 비교작업의 범위를 좁히는 데 도움을 줌
작업 범위 결정 조건
B인덱스의 경우 2번째 칼럼인 dept_no는 비교 작업의 범위를 좁히는 데 아무런 도움 X
범위를 줄이지 못하고 단순히 거름종이 역할만 하는 조건
필터링 조건 또는 체크 조건

8.3.7.2 인덱스의 가용성

B-Tree 인덱스의 특징은 왼쪽 값에 기준해서 오른쪽 값이 정렬돼 있다는 것
왼쪽이란 하나의 칼럼 내에서뿐만 아니라 다중 칼럼 인덱스의 칼럼에 대해서도 함께 적용 됨
인덱스 키 값의 이런 정렬 특성은 빠른 검색의 전제 조건
하나의 칼럼으로 검색해도 값의 왼쪽 부분이 없으면 인덱스 레인지 스캔 방식의 검색이 불가능
A : INDEX(first_name_ B : INDEX(dept_no, emp_no)
SQL
복사
mysql> SELECT * FROM employees WHERE first_name LIKE '%mer';
SQL
복사
해당 쿼리는 인덱스 레인지 스캔 방식으로 이용 불가
first_name 칼럼의 왼쪽부터 비교해야함
해당 쿼리는 왼쪽값이 고정되 있지 않음
정렬 우선순위가 낮은 뒷부분의 값만으로는 왼쪽 기준 정렬 기반의 인덱스인 B-tree에서는 인덱스의 효과를 얻을 수 없다.
mysql> SELECT * FROM dpet_emp WHERE emp_no >= 10144;
SQL
복사
인덱스의 선행 칼럼인 dept_no 조건 없이 emp_no 값으로만 검색하면 인덱스를 효율적으로 사용할 수 없다.
dept_no 칼럼에 대해 먼저 정렬 후 다시 emp_no 칼럼으로 정렬되기 때문
간단히 WHERE 조건절에 대한 내용만 언급했지만 인덱스의 왼쪽 값 기준 규칙은 GROUP BY 절이 나 ORDER BY 절에도 동일 적용

8.3.7.3 가용성과 효율성 판단

기본적으로 B-Tree 인덱스의 특성상 다음 조건에서는 사용 불가
사용할 수 없다는 것은 작업 범위 결정 조건으로 사용할 수 없다는 것을 의미
경우에 따라서는 체크 조건으로 인덱스 사용 가능
NOT-EQUAL로 비교된 경우 (<>, NOT IN, NOT BETWEEN, IS NOT NULL)
...WHERE column <> 'N' ...WHERE column NOT IN (10, 11, 12) ...WHERE column IS NOT NULL
SQL
복사
LIKE ‘%??’ 앞부분이 나닌 뒷부분 일치 형태로 문자열 패턴이 비교된 경우
...WHERE column LIKE '%승환'
SQL
복사
스토어드 함수나 다른 연산자로 인덱스 칼럼이 변형된 후 비교된 경우
...WHERE SUBSTRING(column, 1, 1) = 'X'
SQL
복사
NOT-DEERMINISTIC 속성의 스토어드 함수가 비교 조건으로 사용된 경우
...WHERE column = deterministic_function()
SQL
복사
데이터 타입이 서로 다른 비교 (인덱스 칼럼의 타입을 변환해야 비교가 가능한 경우)
...WHERE char_column = 10
SQL
복사
문자열 데이터 타입의 콜레이션이 다른 경우
...WHERE utf_bin_char_column = euckr_bin_char_column
SQL
복사
다른 일반적 DBMS에서는 NULL 값이 인덱스에 저장되지 않음
MySQL에서는 NULL 값도 인덱스에 저장됨!
INDEX ix_test (column_1, column_2, column_3 ..., column_n)
SQL
복사
작업 범위 결정 조건으로 인덱스를 사용하지 못하는 경우
column_1 칼럼에 대한 조건이 없는 경우
column_1 칼럼의 비교 조건이 위의 인덱스 사용 불가 조건 중 하나일 경우
작업 범위 결정 조건으로 인덱스 사용이 가능한 경우
column_1 ~ column_(i-1) 칼럼까지 동등 비교 형태로 비교
column_1 칼럼에 대해 다음 연산자 중 하나로 비교
동등 비교, 크다 작다 형태, LIKE로 좌측 일치 패턴
위의 두 가지 조건을 모두 만족하는 쿼리는 column_1부터 column_i까지는 작업 범위 결정 조건으로 사용
column_(i+1)부터 column_n까지의 조건은 체크 조건으로 사용

8.4.1 구조 및 특성

공간 정보의 저장 및 검색을 위한 여러 기하학적 도형 정보 관리타입을 제공함
GEOMETRY 타입은 나머지 3개 타입의 슈퍼 타입
POINT와 LINE, POLYGON 객체를 모두 저장 가능
공간 정보의 검색을 위한 R-Tree 알고리즘을 이해하려면 MBR이라는 개념을 알고 있어야 한다.
MBR이란 Minimum Bounding Rectangle의 약자
해당 도형을 감싸는 최소 크기의 사각형을 의미
공간 데이터를 MBR 3개의 레벨로 나눔
최상위 (R1, R2), 차상위 (R3, R4, R5, R6), 최하위 (R7~R14)
최상위 MVR은 R-Tree의 루트노드에 저장되는 정보
차상위는 브랜치 노드
도형 객체는 리프 노드에 저장됨

8.4.2 R-Tree 인덱스의 용도

R-Tree는 MBR 정보를 이용해 B-Tree 형태로 인덱스를 구축하므로 Rectangle의 ‘R’ 과 B-Tree의 ‘Tree’를 섞어서 R-Tree라고 부르며, 공간(Spatial) 인덱스라고도 한다.
일반적으로는 GPS 기준의 위도, 경도 좌표 저장에 주로 사용된다.
하지만 위도, 경도 좌표뿐 아니라 CAD/CAM 소프트웨어 또는 회로 디자인 등과 같이 좌표 시스템에 기반을 둔 정보에 대해서는 모두 적용할 수 있다.
R-Tree는 ST_Contains() 또는 ST_Within() 과 같은 포함 관계 비교함수로 인덱스 활용 가능
ST_Distance()ST_Distance_Sphere() 함수는 공간 인덱스를 효율적으로 사용하지 못함
기준점(P)으로부터 반경 거리 5km 이내의 점(위치)들을 검색하려면?
우선 사각 점선의 상자에 포함되는 점들을 검색 하면 된다.
여기서 ST_Contains()ST_Within() 연산은 사각형 박스와 같은 다각형(Polygon)으로만 연 산할 수 있음
반경 5km를 그리는 원을 포함하는 최소 사각형(MBR)으로 포함 관계 비교를 수행
ST_Contains() 함수와 ST_Within() 함수는 거의 동일한 비교를 수행하지만 두 함수의 파라미터는 반대로 사용해야 함

8.5 전문 검색 인덱스

문서 전체에 대한 분석과 검색을 위한 이러한 인덱싱 알고리즘을 전문 검색(Full Text search) 인덱스 라고 함
전문 검색 인덱스는 일반화된 기능의 명칭이지 전문 검색 알고리즘의 이름을 지칭하는 것은 아님

8.5.1 인덱스 알고리즘

전문 검색에서는 문서 본문의 내용에서 사용자가 검색하게 될 키워드를 분석해 내고, 빠른 검색용으로 사용할 수 있게 키워드로 인덱스를 구축

8.5.1.1 어근 분석 알고리즘

불용어 처리는 검색에서 별 가치가 없는 단어를 모두 필터링해서 제거하는 작업을 의미
불용어의 개수는 많지 않아 알고리즘을 구현한 코드에 모두 상수로 정의해 사용하는 경우가 많움
유연성을 위해 불용어 자체를 데이터베이스화해 사용자가 추가/삭제할 수 있게 구현현한 경우도 있음
어근 분석은 검색어로 선정된 단어의 뿌리인 원형을 찾는 작업
MySQL 서버에서는 오픈소스 형태소 분석 라이브러리인 MeCab을 플러그인 형태로 사용할 수 있게 지원
하지만 MeCab이 제대로 작동하려면 단어 사전과 품사를 식별할 수 있는 구조 인식과정이 필요함
이 과정은 상당한 시간이 필요한 작업
MeCab을 MySQL 서버에 적용하는 방법은 어렵지 않지만 한글에 맞게 완성도를 갖추는 작업은 많은 시간과 노력이 필요하다.

8.5.1.2 n-gram 알고리즘

n-gram이란 본문을 무조건 몇 글자씩 잘라서 인덱싱하는 방법이다.
2-gram 방식으로 문장을 토큰화 한다면?
to be or not to be. That is the question
SQL
복사
전문 검색을 더 빠르게 하기 위해 2단계 인덱싱과 같은 방법도 있지만 MySQL 서버는 이렇게 구분된 토큰을 단순한 B-Tree 인덱스에 저장

8.5.1.3 불용어 변경 및 삭제

전문 검색 인덱스의 불용어 처리 무시
스토리지 엔진에 관계없이 MySQL 서버의 모든 전문 검색 인덱스에 대해 불용어를 완전히 제거
MySQL 서버의 설정 파일(my.cnf)의 ft_stopword_file 시스템 변수에 빈 문자열을 설정
InnoDB 스토리지 엔진을 사용하는 테이블의 전문 검색 인덱스에 대해서만 불용어 처리 무시
innodb_ft_enable_stopword 시스템 변수를 OFF로 설정
사용자 정의 불용어 사전
사용자가 직접 정의한 불용어 사용 가능
불용어 목록을 파일로 저장하고, MySQL 서버 설정 파일에서 파일의 경로를 ft_stopword_file 설정에 등록
InnoDB 스토리지 엔진을 사용하는 테이블의 전문 검색 엔진에서 불용어의 목록을 테이블로 저장
불용어 테이블을 생성하고, innodb_ft_ server_stopword_table 시스템 변수에 불용어 테이블을 설정

8.5.2 전문 검색 인덱스의 가용성

전문 검색 인덱스를 이용하기 위한 두 가지 조건
쿼리 문장이 전문 검색을 위한 문법(MATCH … AGAINST …)를 사용
테이블 전문 검색 대상 칼럼에 대해 전문 인덱스 보유

8.6 함수 기반 인덱스

일반적인 인덱스는 칼럼의 값 일부(칼럼의 값 앞부분) 또는 전체에 대해서만 인덱스 생성이 허용
하지만 때로는 칼럼의 값을 변형해서 만들어진 값에 대해 인덱스를 구축해야 할 때도 있음
이러한 경우 함수 기반의 인덱스를 활용하면 됨
MySQL 서버에서 함수 기반 인덱스를 구현하는 방법
가상 컬럼을 이용한 인덱스
함수를 이용한 인덱스
가상 칼럼이 VIRTUAL이나 STORED 옵션 중 어떤 옵션으로 생성됐든 관계없이 해당 가상 칼럼에 인덱스를 생성 가능
가상 칼럼은 테이블에 새로운 칼럼을 추가하는 것과 같은 효과를 내기 때문에 실제 테이블의 구조가 변경된다는 단점

8.6.2 함수를 이용한 인덱스

가상 칼럼은 MySQL 5.7 버전에서도 사용할 수 있었지만 함수를 직접 인덱스 생성 구문에 사용할 수는 없었다.
함수를 직접 사용하는 인덱스는 테이블의 구조는 변경하지 않고, 계산된 결괏값에 검색을 빠르게 만들어 줌
mysql> CREATE TABLE user ( user_id BIGINT, first_name VARCHAR(10), last_name VARCHAR(10), PRIMARY KEY (user_id), INDEX ix_fullname ((CONCAT(first_name,' ',last_name))));
SQL
복사
함수 생성 시 명시된 표현식과 쿼리의 WHERE 조건절에 사용된 표현식이 다르다면 옵티마이저는 다른 표현식으로 간주해 함수 기반 인덱스 사용 불가
mysql> EXPLAIN SELECT * FROM user WHERE CONCAT(first_name, ' ', last_name) = 'Matt Lee';
SQL
복사
ix_fullname 인덱스를 사용하지 않는것으로 표기된다면 CONCAT함수에 사용된 공백 문자 리터럴때문일 확률이 높음 3가지 시스템 변수를 동일 콜레이션으로 일치 시켜 다시 테스트
collation_connection
collation_database
collation_server

8.7 멀티 밸류 인덱스

전문 검색 인덱스를 제외한 모든 인덱스는 레코드 1건이 1개의 인덱스 키 값을 가진다
인덱스 키와 데이터 레코드는 1:1의 관계를 가진다.
멀티 밸류 인덱스는 하나의 데이터 레 코드가 여러 개의 키 값을 가질 수 있는 형태의 인덱스
일반적인 DBMS를 기준으로는 정규화에 위배되는 형태
최근 RDBMS들이 JSON 데이터 타입을 지원하기 시작 하면서 JSON의 배열 타입의 필드에 저장된 원소들에 대한 인덱스 요건이 발생한 것
mysql> CREATE TABLE user ( user_id BIGINT AUTO_INCREMENT PRIMARY KEY, first_name VARCHAR(10), last_name VARCHAR(10), credit_info JSON, INDEX mx_creditscores ( (CAST(credit_info->'$.credit_scores' AS UNSIGNED ARRAY)))); mysql> INSERT INTO user VALUES (1, 'Matt', 'Lee', '{"credit_scores":[360, 353, 351]}');
SQL
복사
멀티 밸류 인덱스를 사용하기 위해 아래 함수를 이용해 검색해야 함
MEMBER OF()
JSON_CONTAINS()
JSON)OVERLAPS()

8.8 클러스터링 인덱스

클러스터링이란 여러 개를 하나로 묶는다는 의미
DB에서 클러스터링은 테이블의 레코드를 비슷한 것들끼리 묶어서 저장하는 형태로 구현
주로 비슷한 값들을 동시에 조회하는 경우가 많다는 점에 착안한 것
MySQL에서 클러스터링 인덱스는 InnoDB 스토리지 엔진에서만 지원

8.8.1 클러스터링 인덱스

클러스터링 인덱스는 테이블의 프라이머리 키에 대해서만 적용되는 내용
프라이머리 키 값이 비슷한 레코드 끼리 묶어서 저장하는 것을 클러스터링 인덱스라 함
중요한 것은 프라이머리 키 값에 의해 레코드의 저장 위치가 결정된다는 것
프라이머리 키가 변경되면 레코드의 물리적 저장 위치가 바뀌어야 함
프라이머리 키 값으로 클러스터링된 테이블은 프라이머리 키 값 자체에 대한 의존도가 큼
때문에 신중히 프라이머리 키 를 결정해야 함
클러스터링 인덱스는 프라이머리 키 값에 의해 레코드의 저장 위치가 결정 됨
사실 인덱스 알고리즘이라기보다 테이블 레코드의 저장 방식이라고 볼 수 있다.
클러스터링 인덱스 = 클러스터링 테이블 과 동의어로 사용됨
클러스터링의 기준이 되는 프라이머리 키는 클러스터링 키라고도 표현
일반적으로 InnoDB와 같이 항상 클러스터링 인덱스로 저장되는 테이블은 프라이머리 키 기반의 검색이 매우 빠름
대신 레코드의 저장이나 프라이머리 키의 변경이 상대적으로 느림
일반 B-Tree와 비슷하지만 B-Tree 리프 노드와 달리 클러스터링 인덱스의 리프 노드에는 레코드의 모든 칼럼이 같이 저장돼 있음을 알 수 있음
mysql> UPDATE tb_test SET emp_no=100002 WHERE emp_no=100007;
SQL
복사
업데이트가 실행 된 후 데이터 구조
실제로 프라이머 리 키의 값이 변경되는 경우는 거의 없음
MyISAM이나 기타 InnoDB를 제외한 테이블의 데이터 레코드는 프라이머리 키나 인덱스 키 값이 변경 된다 해서 실제 데이터 레코드의 위치가 변경되지는 않음
프라이머리 키가 없는 테이블의 경우 InnoDB 스토리지 엔진이 다음 우선순위대로 프라이머리 키를 대체할 칼럼을 선택 함
1. 프라이머리 키가 있으면 기본적으로 프라이머리 키를 클러스터링 키로 선택 2. NOT NULL 옵션의 유니크 인덱스 중에서 첫 번째 인덱스를 클러스터링 키로 선택 3. 자동으로 유니크한 값을 가지도록 증가되는 칼럼을 내부적으로 추가한 후, 클러스터링 키로 선택
SQL
복사
InnoDB 엔진이 적절한 클러스터링 키 후보를 찾지 못하는 경우엔?
InnoDB 스토리지 엔진이 내부적으로 레코드의 일련번호 칼럼을 생성
자동으로 추가된 프라이머리 키는 사용자에게 노출되지 않으며, 쿼리 문장에 명시적으로 사용할 수 없음
때문에 아무 의미 없는 숫자를 클러스터링 하는 것이며, 우리에게 아무런 혜택을 주지 못함
InnoDB 테이블에서 클러스터링 인덱스는 테이블당 단 하나만 가질 수 있는 엄청난 혜택
가능하다면 프라이머리 키를 명시적으로 생성하자

8.8.2 세컨더리 인덱스에 미치는 영향

MyISAM/MEMORY 테이블 같은 클러스터링되지 않은 테이블은 INSERT될 때 처음 저장된 공간에서 절대 이동하지 않는다.
프라이머리 키나 세컨더리 인덱스의 각 키는 주소(ROWID)를 이용 해 실제 데이터 레코드를 찾아온다.
때문에 MyISAM/MEMORY 테이블에서는 프라이머리 키와 세컨더리 인덱스는 구조적으로 아무런 차이가 없다.
InnoDB에서는 클러스터링 키 값이 변경될 때마다 데이터 레코드의 주소가 변경되고 그때마다 테이블의 모든 인덱스에 저장된 주솟값을 변경해야 한다.
이런 오버헤드를 제거하기 위해 InnoDB 테이블의 클러스터링 테이블은 모든 세컨더리 인덱 스는 해당 레코드가 저장된 주소가 아닌 프라이머리 키 값을 저장하도록 구현돼 있다.
MyISAM: ix_firstname 인덱스를 검색해서 레코드의 주소를 확인한 후, 레코드의 주소를 이용해 최종 레코드를 가져옴 InnoDB: ix_firstname 인덱스를 검색해 레코드의 프라이머리 키 값을 확인한 후, 프라이머리 키 인덱스를 검색해 서 최종 레코드를 가져옴
SQL
복사
InnoDB가 MyISAM보다 조금 더 복잡하게 처리됨
하지만 InnoDB 테이블에서 프라이머리 키는 더 큰 장점을 제공하기 때문에 성능 저하에 대해 너무 걱정하지 않아도 된다.

8.8.3 클러스터링 인덱스의 장점과 단점

장점

프라이머리 키로 검색할 때 처리 성능이 매우 빠름
특히, 프라이머리 키를 범위 검색하는 경우 매우 빠름
테이블의 모든 세컨더리 인덱스가 프라이머리 키를 가지고 있기 때문에 인덱스만으로 처리될 수 있는 경우가 많음
이를 커버링 인덱스라고 함

단점

테이블의 모든 세컨더리 인덱스가 클러스터링 키를 갖기 때문에 클러스터링 키 값의 크기가 클 경우 전체적으로 인덱스의 크기가 커짐
세컨더리 인덱스를 통해 검색할 때 프라이머리 키로 다시 한번 검색해야 하므로 처리 성능이 느림
INSERT할 때 프라이머리 키에 의해 레코드의 저장 위치가 결정되기 때문에 처리 성능이 느림
프라이머리 키를 변경할 때 레코드를 DELETE하고 INSERT하는 작업이 필요하기 때문에 처리 성능이 느림
대부분 클러스터링 인덱스의 장점은 빠른 읽기이며, 단점은 느린 쓰기라는 것을 알 수 있다.
일반적으로 웹 서비스와 같은 온라인 트랜잭션 환경에서는 쓰기와 읽기의 비율이 2:8 또는 1:9 정도이기 때문에 조금 느린 쓰기를 감수하고 읽기를 빠르게 유지하는 것은 매우 중요

8.8.4 클러스터링 테이블 사용 시 주의사항

8.8.4.1 클러스터링 인덱스 키의 크기

클러스터링 테이블의 경우 모든 세컨더리 인덱스가 프라이머리 키 값을 포함
프라이머리 키의 크기가 커지면 세컨더리 인덱스도 자동으로 크기가 커짐
일반적으로 테이블에 세컨더리 인덱스가 4~5개 정도 생성된다는 것을 고려하면 세컨더리 인덱스 크기는 급격히 증가
인덱스가 커질수록 같은 성능을 내기 위해 그만큼의 메모리가 더 필요해지므로 InnoDB 테이블의 프라이머리 키는 신중하게 선택 해야 함

8.8.4.2 프라이머리 키는 AUTO-INCREMENT 보다는 업무적인 칼럼으로 생성

InnoDB의 프라이머리 키는 클러스터링 키로 사용되며, 이 값에 의해 레코드의 위치가 결정
프라이머리 키로 검색하는 경우 클러스터링되지 않은 테이블에 비해 매우 빠르게 처리될 수 있음을 의미
특히 범위로 많은 레코드를 검색하는 경우
설령 그 칼럼의 크기가 크더라도 업무적으로 해당 레코드를 대표할 수 있다면 그 칼럼을 프라이머리 키로 설정하는 것이 좋다.

8.8.4.3 프라이머리 키는 반드시 명시할 것

가끔 프라이머리 키가 없는 테이블을 자주 보게 되는데, 가능하면 AUTO_INCREMENT 칼럼을 이용해서라도 프라이머리 키는 생성하는 것을 권장
InnoDB 테이블에 프라이머리 키를 정의하지 않는 경우와 AUTO_INCREMENT 칼럼을 생성하고 프라이머리 키로 설정하는 것이 결국 똑같다. (InnoDB 내부적 생성)
그렇다면 사용자가 사용할 수 있는 값을 프라이머리 키로 설 정하는 것이 좋을 것
ROW 기반의 복제나 InnoDB Cluster에서는 모든 테이블이 프라이머리 키를 가져야만 하는 정상적인 복제 성능을 보장하기도 하므로 프라이머리 키는 꼭 생성하자!

8.8.4.4 AUTO-INCREMENT 칼럼을 인조 식별자로 사용할 경우

여러 개의 칼럼이 복합으로 프라이머리 키가 만들어지는 경우 프라이머리 키의 크기가 길어질 때가 있음
하지만 프라이머리 키의 크기가 길어도 세컨더리 인덱스가 필요치 않다면 그대로 프라이머리 키를 사용하는 것이 좋다.
세컨더리 인덱스도 필요하고 프라이머리 키의 크기도 길다면 AUTO-INCREMENT를 추가하고, 이를 프라이머리 키로 설정하면 됨
프라이머리 키를 대체하기 위해 인위적으로 추가된 프라이머리 키를 인조 식별자(Surrogate key)라고 함
로그 테이블과 같이 조회보다는 INSERT 위주의 테이블들은 AUTO_INCREMENT를 이용한 인조 식별자를 프라이머리 키로 설정하는 것이 성능 향상에 도움이 됨

8.9 유니크 인덱스

유니크는 사실 인덱스라기보다는 제약 조건에 가깝다고 볼 수 있다.
말 그대로 테이블이나 인덱스에 같 은 값이 2개 이상 저장될 수 없음을 의미
MySQL에서는 인덱스 없이 유니크 제약만 설정할 방법이 없다.
질문) 그럼 다른 DB는 있는건가? ⇒ 가능하더라도 굳이?

8.9.1 유니크 인덱스와 세컨더리 인덱스의 비교

8.9.1.1 인덱스 읽기

많은 사람이 유니크 인덱스가 빠르다고 생각한다. 하지만 이것은 사실이 아니다.
유니크하지 않은 세컨더리 인덱스에서 한 번 더 해야 하는 작업은 디스크 읽기가 아니라 CPU에서 칼럼값을 비교하는 작업이기 때문에 이는 성능상 영향이 거의 없다고 볼 수 있다.
유니크하지 않은 세컨더리 인덱스는 중복된 값이 허용되므로 읽어야 할 레코드가 많아서 느린 것이지, 인덱스 자체의 특성 때문에 느린 것이 아니라는 것이다.
하나의 값을 검색하는 경우, 유니크 인덱스와 일반 세컨더리 인덱스는 사용되는 실행 계획이 다르다.
하지만 이는 인덱스의 성격이 유니크한지 아닌지에 따른 차이일 뿐 큰 차이는 없다.
한 개의 레코드를 읽느냐 2개 이상의 레코드를 읽느냐의 차이만 있다는 것을 의미할 뿐, 읽어야 할 레코드 건수가 같다면 성능상 차이는 미미

8.9.1.2 인덱스 쓰기

새로운 레코드가 INSERT되거나 인덱스 칼럼의 값이 변경되는 경우에는 인덱스 쓰기 작업이 필요
유니크 인덱스의 키 값을 쓸 때는 중복된 값을 체크하는 과정이 한 단계 더 필요
그래서 유니크하지 않은 세컨더리 인덱스의 쓰기보다 느리다.
중복된 값을 체크할 때는 읽기 잠금을 사용하고, 쓰기를 할 때는 쓰기 잠금을 사용하는데 이 과정에서 데드락이 아주 빈번히 발생한다.
유니크 인덱스는 반드시 중복 체크를 해야 하므로 작업 자체를 버퍼링하지 못한다. 이 때문에 유니크 인덱스는 일반 세컨더리 인덱스보다 변경 작업이 더 느리게 작동한다.

8.9.2 유니크 인덱스 사용 시 주의사항

꼭 필요한 경우라면 유니크 인덱스를 생성하는 것은 당연하다.
하지만 더 성능이 좋아질 것으로 생각하고 불필요하게 유니크 인덱스를 생성하지는 않는 것이 좋다.
유니크 인덱스도 일반 세컨더리 인덱스와 같은 역할을 동일하게 수행할 수 있으므로 세컨더리 인덱스를 중복으로 만들어 줄 필요는 없다.
가끔 똑같은 칼럼에 대해 프라이머리 키와 유니크 인덱스를 동일하게 생성한 경우도 있는데, 이 또한 불필요한 중복이므로 주의하자.
결론적으로 유일성이 꼭 보장돼야 하는 칼럼에 대해서는 유니크 인덱스를 생성하되, 꼭 필요하지 않다면 유니크 인덱스보다는 유니크하지 않은 세컨더리 인덱스를 생성하는 방법도 한 번씩 고려

8.10 외래키

MySQL에서 외래키는 InnoDB 스토리지 엔진에서만 생성할 수 있으며, 외래키 제약이 설정되면 자동 으로 연관되는 테이블의 칼럼에 인덱스까지 생성
외래키가 제거되지 않은 상태에서는 자동으로 생성된 인덱스를 삭제할 수 없다.
InnoDB의 왜래키 관리에는 중요한 두 가지 특징이 있음
테이블의 변경(쓰기 잠금)이 발생하는 경우에만 잠금 경합(잠금 대기)이 발생
외래키와 연관되지 않은 칼럼의 변경은 최대한 잠금 경합(잠금 대기)을 발생시키지 않음
데이터베이스에서 외래 키를 물리적으로 생성하려면 이러한 현상으로 인한 잠금 경합까지 고려해 모델 링을 진행하는 것이 좋다.
mysql> CREATE TABLE tb_parent ( id INT NOT NULL, fd VARCHAR(100) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB; mysql> CREATE TABLE tb_child ( id INT NOT NULL, pid INT DEFAULT NULL, fd VARCHAR(100) DEFAULT NULL, PRIMARY KEY (id), KEY ix_parentid (pid), CONSTRAINT child_ibfk_1 FOREIGN KEY (pid) REFERENCES tb_parent (id) ON DELETE CASCADE ) ENGINE=InnoDB; mysql> INSERT INTO tb_parent VALUES (1, 'parent-1'), (2, 'parent-2'); mysql> INSERT INTO tb_child VALUES (100, 1, 'child-100');
SQL
복사

8.10.1 자식 테이블의 변경이 대기하는 경우

자식 테이블의 외래 키 칼럼의 변경은 부모 테이블의 확인이 필요
이 상태에서 부모 테이블의 해당 레코드가 쓰기 잠금이 걸려 있으면 해당 쓰기 잠금이 해제될 때까지 기다리게 되는 것
자식 테이블의 외래키(pid)가 아닌 칼럼의 변경은 외래키로 인한 잠금 확장이 발생하지 않는다.

8.10.2 부모 테이블의 변경 작업이 대기하는 경우

첫 번째 커넥션에서 부모 키 1을 참조하는 자식 테이블의 레코드를 변경하면 tb_child 테이블의 레코드에 대해 쓰기 잠금을 획득
이 상태에서 2번 커넥션이 tb_parent 테이블에서 id가 1인 레코드를 삭제하는 경우
이 쿼리(작업번호 4번) 는 tb_child 테이블의 레코드에 대한 쓰기 잠금이 해제될 때까지 기다려야 함
이는 자식 테이블이 생성될 때 정의된 외래키 특성 때문에 부모 레코드가 삭제되면 자식 레코드도 동식에 삭제되는 식으로 작동하기 때문