MySQL 단일 인덱스가 유효한 조건: 선택도·범위 검색·정렬

반응형

MySQL 단일 인덱스는 컬럼 하나를 기준으로 검색 범위를 빠르게 줄일 때 유효하다. WHERE에 등장한 컬럼이라고 모두 인덱스를 만들 필요는 없다. 조건을 적용한 뒤 남는 행의 수, 실제로 반환할 열, 정렬 방식과 쓰기 비용을 함께 봐야 한다.

아래 설명은 MySQL 8.4와 InnoDB의 일반 B-tree 보조 인덱스를 기준으로 한다.

단일 인덱스는 어떤 일을 하는가

사용자를 이메일로 자주 찾는다면 다음 인덱스를 생각할 수 있다.

CREATE INDEX idx_users_email ON users (email);

이 인덱스는 email 값을 정렬된 검색 키로 저장한다. InnoDB의 보조 인덱스 엔트리에는 행을 찾기 위한 기본 키도 포함된다. 다음 쿼리는 보조 인덱스에서 이메일을 찾은 뒤 기본 키로 전체 행을 읽을 수 있다.

SELECT *
FROM users
WHERE email = 'chaaany@example.com';

이메일이 업무 규칙상 유일해야 한다면 성능과 별개로 UNIQUE 제약을 검토해야 한다.

CREATE UNIQUE INDEX uk_users_email ON users (email);

인덱스는 데이터 무결성 제약을 대신 결정해 주지 않는다. 중복 허용 여부는 도메인 규칙으로 먼저 정한다.

동등 검색과 범위 검색

B-tree 인덱스는 =, IN, 비교 연산과 BETWEEN 같은 조건에서 특정 키나 키 범위를 찾는 데 사용할 수 있다.

CREATE INDEX idx_users_created_at ON users (created_at);

SELECT id, created_at
FROM users
WHERE created_at >= '2026-04-01'
  AND created_at <  '2026-05-01';

날짜 구간은 끝을 포함하는 BETWEEN보다 반열린 구간으로 적으면 시간 정밀도가 바뀌어도 경계를 다루기 쉽다. 인덱스는 4월 1일의 시작 위치를 찾고 5월 1일 전까지 연속된 엔트리를 읽을 수 있다.

하지만 한 달치가 테이블의 1%인지 70%인지에 따라 비용이 달라진다. 반환 범위가 넓고 SELECT *로 모든 열을 읽는다면 보조 인덱스와 클러스터드 인덱스를 오가는 비용이 커질 수 있다.

카디널리티보다 선택도를 본다

카디널리티는 서로 다른 값의 개수다. 이메일처럼 값 종류가 많은 컬럼은 일반적으로 한 조건이 적은 행을 가리키기 쉽다. 상태값처럼 종류가 적은 컬럼은 그렇지 않을 가능성이 크다.

다만 “카디널리티가 낮으면 인덱스가 쓸모없다”는 규칙은 정확하지 않다.

ACTIVE   999,000행
DELETED    1,000행

값 종류는 두 개뿐이지만 status = 'DELETED'는 전체의 0.1%만 고른다. 이 조건만 자주 조회한다면 인덱스가 후보가 될 수 있다. 반대로 status = 'ACTIVE'가 거의 전부를 반환하면 같은 인덱스의 이득이 작을 수 있다.

실무에서는 다음 질문이 더 직접적이다.

  • 이 조건으로 전체 행의 몇 퍼센트가 남는가
  • 값별 분포가 한쪽으로 치우쳐 있는가
  • 결과에서 몇 개의 열을 읽는가
  • 이 쿼리는 얼마나 자주 실행되는가

옵티마이저는 테이블과 인덱스 통계를 바탕으로 행 수를 추정한다. 실제 분포와 추정치가 크게 다르면 통계 갱신과 데이터 편향을 함께 확인한다.

ORDER BY는 인덱스만 있으면 사라지는가

created_at 인덱스는 정렬된 순서를 제공하므로 다음 쿼리의 별도 정렬을 피할 가능성이 있다.

SELECT id, created_at
FROM users
ORDER BY created_at DESC
LIMIT 50;

그러나 인덱스가 있다고 항상 filesort가 사라지는 것은 아니다. 옵티마이저는 인덱스 순서대로 읽은 뒤 테이블 행을 찾는 비용과, 테이블을 읽어 별도로 정렬하는 비용을 비교한다. WHERE, ORDER BY, 선택한 열, LIMIT과 정렬 방향이 함께 계획을 결정한다.

실행 계획에서 다음을 본다.

EXPLAIN FORMAT=TREE
SELECT id, created_at
FROM users
ORDER BY created_at DESC
LIMIT 50;
  • 어떤 인덱스를 읽는가
  • Sort 단계가 따로 있는가
  • 몇 행을 읽은 뒤 50행을 반환할 것으로 보는가

인덱스가 있어도 사용하지 않는 이유

다음은 오류라기보다 비용 기반 선택일 수 있다.

  1. 조건이 전체의 큰 비율을 반환한다.
  2. 테이블이 매우 작다.
  3. 함수나 암시적 형 변환 때문에 인덱스 검색 조건을 만들지 못한다.
  4. 비교하는 문자열 컬럼의 타입·길이·문자 집합이 맞지 않는다.
  5. 통계가 실제 데이터 분포를 충분히 반영하지 못한다.
  6. 다른 인덱스나 풀 스캔이 더 저렴하다고 추정한다.

예를 들어 컬럼에 함수를 적용한 다음 조건은 일반 B-tree 인덱스를 그대로 활용하기 어려울 수 있다.

WHERE DATE(created_at) = '2026-04-01'

가능하다면 동일한 의도를 범위 조건으로 표현한다.

WHERE created_at >= '2026-04-01'
  AND created_at <  '2026-04-02'

함수 기반 검색이 핵심이라면 생성 컬럼과 그 인덱스 같은 MySQL 기능을 별도로 검토해야 한다.

읽기 이득과 쓰기 비용을 같이 측정한다

보조 인덱스가 하나 늘면 INSERT, DELETE와 인덱스 키를 바꾸는 UPDATE에서도 해당 구조를 유지해야 한다. 디스크 공간과 버퍼 풀도 사용한다. 그러므로 “조회가 빨라질 것 같다”만으로 추가하지 않는다.

안전한 확인 순서는 다음과 같다.

  1. 느린 쿼리와 호출 빈도를 확인한다.
  2. 현재 EXPLAINEXPLAIN ANALYZE를 남긴다.
  3. 대표 데이터 분포에서 후보 인덱스를 비교한다.
  4. 읽은 행 수와 지연뿐 아니라 쓰기 지연·인덱스 크기도 본다.
  5. 운영 적용 뒤 실제 쿼리 지연 분포를 다시 확인한다.

이 흐름을 거치면 “카디널리티가 높으니 생성” 같은 단일 규칙보다 데이터에 맞는 결정을 할 수 있다. 인덱스 탐색 구조는 InnoDB B+Tree와 풀 스캔, 여러 조건을 함께 쓰는 경우는 MySQL 복합 인덱스 설계에서 이어진다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글