1. MySQL Index
- 이번 글에서는 MySQL에서 인덱스를 직접 생성, 조회, 삭제하는 방법과 인덱스가 실제로 사용되는지 확인하는 방법을 알아보겠습니다.
인덱스에 대한 기본적인 내용은 Index 문서를 참고해주세요.
2. CREATE INDEX - 인덱스 생성하기
- 가장 기본이 되는 인덱스 생성 명령어는
CREATE INDEX입니다. - 인덱스를 생성하면 데이터베이스는 지정한 컬럼의 값들을 읽어서 정렬한 뒤, 인덱스 구조를 디스크에 저장합니다.
2.1 기본 문법
CREATE INDEX 인덱스이름 ON 테이블이름 (컬럼1, 컬럼2, ...);
- 인덱스 이름: 생성할 인덱스에 고유한 이름을 붙여줍니다. 보통
idx_테이블명_컬럼명과 같은 규칙으로 지으면 관리하기 편합니다. - 테이블 이름: 인덱스를 생성할 대상 테이블입니다.
- 컬럼: 인덱스를 구성할 컬럼입 니다. 하나 이상의 컬럼을 지정할 수 있습니다.
2.2 실습 예제
items테이블에서 특정item_name으로 상품을 검색하는 경우가 많다고 가정하고,item_name컬럼에 대한 인덱스를 생성해 보겠습니다.
CREATE INDEX idx_items_item_name ON items (item_name);
이 쿼리를 실행하면 데이터베이스는 items 테이블의 모든 item_name 값을 읽어 정렬한 뒤, idx_items_item_name 인덱스를 디스크에 생성합니다. 테이블의 데이터가 많을수록 이 작업은 시간이 오래 걸릴 수 있습니다.
3. SHOW INDEX - 테이블의 인덱스 정보 확인하기
- 테이블에 어떤 인덱스들이 생성되어 있는지 확인하려면
SHOW INDEX명령어를 사용합니다. - 이 명령어를 통해 테이블에 걸려있는 모든 인덱스의 정보를 한눈에 볼 수 있습니다.
3.1 items 테이블 인덱스 확인
SHOW INDEX FROM items;
실행 결과
| Table | Non_unique | Key_name | Column_name | Cardinality |
|---|---|---|---|---|
| items | 0 | PRIMARY | item_id | 25 |
| items | 1 | fk_items_sellers | seller_id | 10 |
| items | 1 | idx_items_item_name | item_name | 25 |
- 결과를 자세히 살펴보겠습니다.
- Key_name: 인덱스의 이름입니다. 우리가 방금 만든
idx_items_item_name이 보입니다. - Column_name: 해당 인덱스가 어떤 컬럼을 기반으로 만들어졌는지 보여줍니다.
- PRIMARY와 fk_items_sellers: 우리는
item_name컬럼에 대한 인덱스만 만들었는데PRIMARY와fk_items_sellers라는 인덱스가 이미 존재합니다. - Non_unique:
1이면 중복 값을 허용하는 인덱스,0이면 중복을 허용하지 않는 고유 인덱스(UNIQUE또는PRIMARY KEY)를 의미합니다. - Cardinality: 인덱스에 저장된 유니크한 값의 개수에 대한 추정치입니다. 이 값이 높을수록 중복도가 낮다는 의미이며, 인덱스의 성능이 좋다고 판단할 수 있습니다.
MySQL에서는 PRIMARY KEY(기본 키)나 FOREIGN KEY(외래 키) 제약조건을 설정하면, 해당 컬럼에 대해 자동으로 인덱스를 생성합니다. item_id는 기본 키이므로 PRIMARY 인덱스가, seller_id는 외래 키이므로 fk_items_sellers 인덱스가 이미 존재했던 것입니다. (UNIQUE 제약조건도 마찬가지로 인덱스가 생성됩니다.)
3.2 sellers 테이블 인덱스 확인
SHOW INDEX FROM sellers;
실행 결과
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Cardinality |
|---|---|---|---|---|---|
| sellers | 0 | PRIMARY | 1 | seller_id | 10 |
| sellers | 0 | seller_name | 1 | seller_name | 10 |
PRIMARY:seller_id기본 키로 인덱스가 자동 생성되었습니다.seller_name:seller_name에UNIQUE제약조건으로 인덱스가 자동 생성되었습니다.
유니크 제약조건은 컬럼 내 데이터의 유일성을 보장해야 합니다. 따라서 새로운 데이터를 삽입(INSERT)하거나 기존 데이터를 수정(UPDATE)할 때마다, 입력하려는 값이 테이블에 이미 존재하는지 빠르게 확인해야 합니다. 만약 인덱스가 없다면 이 중복 검사를 위해 매번 풀 테이블 스캔이 발생하여 쓰기 성능이 크게 저하될 것입니다.
MySQL은 PRIMARY KEY(기본 키), FOREIGN KEY(외래 키), UNIQUE 제약조건에 대해서 인덱스를 자동 생성합니다.
4. DROP INDEX - 인덱스 삭제하기
- 시간이 지나 더 이상 사용하지 않거나, 오히려 쓰기 성능에 방해가 되는 인덱스는 삭제해야 합니다.
DROP INDEX명령어로 간단하게 삭제할 수 있습니다.
4.1 기본 문법
DROP INDEX 인덱스이름 ON 테이블이름;
4.2 실습 예제
- 방금 만들었던
idx_items_item_name인덱스를 삭제해 보겠습니다.
DROP INDEX idx_items_item_name ON items;
이 쿼리를 실행하면 idx_items_item_name 인덱스를 구성하던 데이터 구조가 디스크에서 완전히 사라집니다. 이제 item_name을 조건으로 검색하면 다시 풀 테이블 스캔이 발생할 것입니다. 물론, 원본 items 테이블의 데이터에는 아무런 영향이 없습니다.
5. EXPLAIN - 인덱스가 정말 사용되는지 확인하는 법
- 인덱스를 만들었다고 해서, 데이터베이스가 모든
SELECT문에 항상 그 인덱스를 사용하는 것은 아닙니다. - 데이터의 분포나 쿼리의 형태에 따라, 데이터베이스 옵티마이저는 인덱스를 사용하는 것보다 풀 테이블 스캔이 더 빠르겠다고 판단할 수도 있습니다.
- 우리가 만든 인덱스가 실제로 쿼리에 사용되는지 확인하려면
EXPLAIN이라는 명령어를 쿼리문 앞에 붙여보면 됩니다.
데이터베이스에는 쿼리를 어떤 방식으로 최적화해서 실행할지 계획하는 기능이 있는데, 이것을 쿼리 옵티마이저(최적화기)라 합니다. 사용할 수 있는 인덱스가 여러 개 있다면 어떤 인덱스를 사용할지도 선택합니다.
5.1 인덱스가 없을 때
idx_items_item_name인덱스가 없을 때 실행 결과를 확인해보겠습니다.
EXPLAIN SELECT * FROM items WHERE item_name = '게이밍 노트북';
실행 결과
| id | type | key | rows | filtered | Extra |
|---|---|---|---|---|---|
| 1 | ALL | NULL | 25 | 10.00 | Using where |
- 실행 결과를 통해 데이터베이스가 우리의 쿼리를 어떻게 실행할지 계획했는지, 즉 '실행 계획'을 엿볼 수 있습니다.
- 각 항목이 무엇을 의미하는지 자세히 살펴보겠습니다.
type: ALL
- 가장 중요하게 봐야 할 부분입니다.
type은 데이터베이스가 테이블에 어떻게 접근할지를 나타냅니다.ALL은 **풀 테이블 스캔(Full Table Scan)**을 의미합니다.- 즉,
items테이블의 처음부터 끝까지 모든 데이터를 하나씩 다 읽어서 조건에 맞는 데이터를 찾는다는 뜻입니다.
지금은 데이터가 25개뿐이라 문제가 없지만, 실무에서처럼 데이터가 수백만 건에 달한다면 심각한 성능 저하를 일으키는 주범이 됩니다.
ALL이라고 표시되면 풀 테이블 스캔을 의미합니다. 즉, 테이블의 모든 행을 처음부터 끝까지 다 읽었다는 뜻입니다.
인덱스를 제대로 사용했다면 ref, range 등 다른 값이 표시됩니다. ref는 = 조건이나 JOIN에서 인덱스를 사용했다는 의미입니다.
range는 범위 검색(BETWEEN, >, <, >=, <= 등)에서 인덱스를 사용했다는 의미입니다.
key: NULL
key는 쿼리를 실행할 때 사용한 인덱스를 보여줍니다. 이 값이NULL이라는 것은 어떤 인덱스도 사용하지 못했다는 것을 명확하게 알려줍니다.item_name컬럼으로 데이터를 찾고 있지만, 해당 컬럼에 인덱스가 없기 때문에 당연한 결과입니다.
rows: 25
rows는 옵티마이저가 쿼리를 처리하기 위해 탐색할 것으로 예측하는 행의 수입니다.- 현재
items테이블의 전체 데이터가 25개이므로, 결국 테이블 전체를 다 훑어보겠다고 말하는 것과 같습니다. - 이 값이 작을수록 효율적인 쿼리라 할 수 있습니다.
실제 실행하는 게 아니라 데이터베이스 나름의 통계 데이터를 기반으로 하는 예측 정보입니다. 따라서 정확하게 맞는 것은 아닙니다.
filtered: 10.00
filtered는 테이블에서 읽어온 행들 중에서WHERE조건으로 필터링되고 난 후, 최종적으로 남을 것으로 예측되는 행의 비율입니다.- 여기서는 25 개의 행을 모두 읽은 후, 그중 10%인 2.5개 정도의 행이
item_name = '게이밍 노트북'조건을 만족할 것이라고 예측하고 있습니다.
Extra: Using where
- 데이터를 가져온 후에
WHERE절의 조건(item_name = '게이밍 노트북')을 사용해 필터링 작업을 수행했다는 의미입니다. - 만약 인덱스를 효율적으로 사용했다면, 처음부터 조건에 맞는 데이터만 골라서 가져왔을 것입니다.
- 하지만 인덱스가 없으니 일단 모든 데이터를 다 가져와서, 그 후에 조건에 맞는지 일일이 비교하는 비효율적인 방식으로 일하고 있음을 보여줍니다.
6. 인덱스와 동등 비교
- 데이터베이스에서 인덱스는 다음 세 가지 상황에 사용됩니다.
- 동등 비교(
=) - 범위 검색(
BETWEEN,>,<,>=,<=,LIKE등) ORDER BY를 통한 정렬 작업
- 동등 비교(
type: ref는 동등 비교(=) 조건이나JOIN에서 인덱스를 사용했다는 의미입니다.EXPLAIN을 사용해서 동등 비교를 확인해보고, 인덱스가 있을 때와 없을 때의 차이를 살펴보겠습니다.
6.1 인덱스가 있을 때
- 먼저
items테이블의item_name컬럼에 인덱스를 생성합니다.
CREATE INDEX idx_items_item_name ON items (item_name);
- 이제
EXPLAIN으로 쿼리 실행 계획을 확인해 보겠습니다.
EXPLAIN SELECT * FROM items WHERE item_name = '게이밍 노트북';
실행 결과
| id | type | possible_keys | key | rows | filtered | Extra |
|---|---|---|---|---|---|---|
| 1 | ref | idx_items_item_name | idx_items_item_name | 1 | 100.00 | NULL |
- 인덱스를 생성한 후의 실행 계획은 이전과 확연히 달라진 것을 볼 수 있습니다. 하나씩 분석해 보겠습니다.
type: ref
- 이전의
ALL과 비교했을 때 가장 극적인 변화입니다. type이ref라는 것은, 인덱스를 사용해 동등 비교(=) 조건으로 데이터를 찾았다는 의미입니다.ref는 'reference(참조)'의 약자로, 인덱스를 통해 조건에 맞는 데이터를 매우 효율적으로 참조해서 가져왔다는 뜻입니다.- 풀 테이블 스캔(
ALL)과는 비교할 수 없을 정도로 빠른 접근 방식입니다.
possible_keys
- 현재 쿼리에서 사용 가능한 인덱스의 후보입니다.
- 지금은
idx_items_item_name하나만 있지만 현재 쿼리에서 사용가능한 인덱스를 모두 보여줍니다. - 이 후보들 중에 선택되어 사용될 인덱스가 다음
key항목에 나타납니다.
key: idx_items_item_name
- 이전에는
NULL이었던 이 값에 우리가 방금 생성한 인덱스의 이름(idx_items_item_name)이 명확하게 표시됩니다. - 이것은 옵티마이저가 이 쿼리를 실행하는 데
idx_items_item_name인덱스를 사용했음을 보여주는 직접적인 증거입니다.
rows: 1
- 이 또한 엄청난 변화입니다. 인덱스가 없을 때는 테이블 전체 행의 수인
25를 스캔할 것으로 예측했지만, 이제는 단1개의 행만 읽으면 된다고 예측합니다. - 마치 책의 맨 뒤에 있는 찾아보기를 통해 '게이밍 노트북'이라는 단어가 있는 페이지를 바로 찾아가는 것과 같습니다.
EXPLAIN은 실제 SQL 쿼리를 실행하는 것이 아닙니다. 여기서 rows는 예측값입니다. 따라서 환경에 따라 1이 아닌 다른 값이 나올 수도 있습니다.
filtered: 100.00
- 인덱스를 통해서 찾은 1개의 행을 100% 선택한다는 뜻입니다.
Extra: NULL
- 이전에 표시되었던
Using where가 사라졌습니다. - 이는 인덱스 단계에서 이미 모든 검색 조건이 충족되었기 때문에, 데이터를 가져온 후 별도의 필터링 작업이 필요 없었다는 것을 의미합니다.
- 그만큼 작업이 더 단순하고 효율적으로 처리된 것입니다.
item_name 컬럼에 인덱스를 생성하자 데이터베이스 옵티마이저는 풀 테이블 스캔이라는 비효율적인 방법을 버리고, 인덱스를 사용해 단 하나의 행만 읽어오는 매우 효율적인 실행 계획을 세웠습니다. 이것이 바로 우리가 인덱스를 사용하는 핵심적인 이유입니다.