데이터베이스 인덱스가 쿼리 시간을 어떻게 단축하는지, 언제 도움이 되고(또는 해가 되는지), 실제 애플리케이션에서 인덱스를 설계·테스트·유지하는 실전 방법을 알아봅니다.

데이터베이스 인덱스는 데이터베이스가 행을 더 빠르게 찾도록 돕는 별도의 조회 구조입니다. 테이블의 두 번째 복사본이 아닙니다. 책의 색인 페이지를 생각해 보세요: 색인을 사용해 대략적인 위치로 바로 이동한 다음, 필요한 정확한 페이지(행)를 읽습니다.
인덱스가 없으면 데이터베이스는 종종 유일한 안전한 옵션으로 많은 행을 읽어 쿼리와 일치하는지 확인해야 합니다. 테이블이 수천 행일 때는 괜찮을 수 있습니다. 테이블이 수백만 행으로 커지면 “더 많은 행을 확인”하는 것은 더 많은 디스크 읽기, 더 큰 메모리 압박, 더 많은 CPU 작업으로 이어집니다—그래서 예전엔 즉각적이었던 쿼리가 느려지기 시작합니다.
인덱스는 데이터베이스가 "ID가 123인 주문을 찾기" 또는 "이 이메일을 가진 사용자를 가져오기" 같은 질문에 답할 때 검사해야 하는 데이터 양을 줄입니다. 모든 것을 스캔하는 대신 데이터베이스는 검색을 빠르게 좁히는 압축된 구조를 먼저 따라갑니다.
하지만 인덱싱이 만능 해결책은 아닙니다. 일부 쿼리는 여전히 많은 행을 처리해야 합니다(광범위한 리포트, 선택도가 낮은 필터, 무거운 집계 등). 그리고 인덱스에는 실제 비용이 있습니다: 추가 저장 공간과 느려진 쓰기 성능 — 인서트와 업데이트도 인덱스를 갱신해야 하기 때문입니다.
당신은 다음을 보게 될 것입니다:
데이터베이스가 쿼리를 실행할 때 크게 두 가지 선택지가 있습니다: 테이블을 행 단위로 모두 스캔하거나, 일치하는 행으로 바로 점프하는 것. 대부분의 인덱스 이득은 불필요한 읽기를 피하는 데서 옵니다.
**전체 테이블 스캔(Full table scan)**은 문자 그대로 테이블의 모든 행을 읽고 WHERE 조건에 맞는지 확인한 뒤 결과를 반환하는 방식입니다. 작은 테이블에는 괜찮지만 테이블이 커질수록 느려집니다—행이 많을수록 작업이 많아집니다.
인덱스를 사용하면 데이터베이스는 대부분의 행을 읽지 않아도 됩니다. 대신 먼저 검색용으로 구축된 압축된 구조인 인덱스를 확인해 일치하는 행이 어디에 있는지 찾고, 그 특정 행들만 읽습니다.
책을 생각해보세요. “광합성”이 언급된 모든 페이지를 찾으려면 책 전체를 처음부터 끝까지 읽을 수도 있습니다(전체 스캔). 또는 책의 색인을 사용해 해당 페이지들로 바로 이동해 그 부분만 읽을 수 있습니다(인덱스 조회). 두 번째 방법이 훨씬 빠릅니다. 거의 모든 페이지를 건너뛰기 때문입니다.
데이터베이스는 특히 데이터가 메모리에 없을 때 읽기를 기다리는 시간이 많이 소요됩니다. 건드려야 하는 행(및 페이지) 수를 줄이면 일반적으로 다음이 감소합니다:
인덱싱은 데이터가 큰 경우와 쿼리 패턴이 선택적인 경우(예: 1,000만 건 중 20건만 가져오는 경우)에 가장 큰 도움을 줍니다. 쿼리가 어쨌든 대부분의 행을 반환하거나, 테이블이 메모리에 편안하게 들어갈 정도로 작다면 전체 스캔이 같은 속도이거나 더 빠를 수 있습니다.
인덱스는 값을 정리하여 데이터베이스가 원하는 항목 근처로 바로 이동할 수 있게 합니다. 모든 행을 확인할 필요가 없게 만드는 것이 핵심입니다.
SQL 데이터베이스에서 가장 흔한 인덱스 구조는 B-트리(또는 B+트리)입니다. 개념적으로는:
정렬되어 있기 때문에 B-트리는 동등 검색(WHERE email = ...)과 범위 쿼리(WHERE created_at >= ... AND created_at < ...) 모두에 적합합니다. 데이터베이스는 올바른 근처로 이동한 뒤 순차적으로 앞으로 스캔할 수 있습니다.
사람들이 B-트리 조회를 “로그계(로그형)”라고 말할 때 실무적으로 의미하는 바는 이렇습니다: 테이블이 수천에서 수백만 행으로 늘어나도 값을 찾는 단계 수는 천천히 늘어납니다. 데이터가 두 배라고 작업량이 두 배로 늘어나지 않습니다. 대신 트리의 소수 레벨을 따라 포인터를 따라가므로 데이터가 훨씬 많아져도 단계 수는 조금만 늘어납니다.
일부 엔진은 해시 인덱스를 제공합니다. 값의 해시를 사용해 항목을 직접 찾기 때문에 동등 비교에 매우 빠를 수 있습니다.
단점: 해시 인덱스는 일반적으로 범위나 정렬된 스캔에는 도움이 되지 않으며, 엔진별로 가용성과 동작이 다릅니다.
PostgreSQL, MySQL/InnoDB, SQL Server 등은 인덱스를 저장하고 사용하는 방식(페이지 크기, 클러스터링, 포함 컬럼, 가시성 검사)이 다릅니다. 그러나 핵심 개념은 동일합니다: 인덱스는 데이터베이스가 전체 테이블을 스캔하는 대신 일치하는 행을 훨씬 적은 작업으로 찾게 해주는 탐색 가능한 압축 구조를 만듭니다.
인덱스는 "SQL 전체"를 가속화하지 않습니다—특정 접근 패턴을 가속화합니다. 쿼리의 필터, 조인, 정렬 방식과 인덱스가 맞아떨어질 때 데이터베이스는 관련 행으로 바로 점프할 수 있습니다.
1) WHERE 필터(특히 선택도가 높은 컬럼)
쿼리가 큰 테이블을 적은 행으로 좁히는 경우 인덱스가 보통 첫 번째 고려 대상입니다. 예: 식별자로 사용자를 조회할 때.
users.email에 인덱스가 없으면 데이터베이스는 모든 행을 스캔해야 할 수 있습니다:
SELECT * FROM users WHERE email = '[email protected]';
email에 인덱스가 있으면 일치하는 행을 빠르게 찾아 멈출 수 있습니다.
2) JOIN 키(외래 키와 참조 키)
조인은 작은 비효율이 큰 비용으로 증폭되는 곳입니다. orders.user_id를 users.id에 조인할 때 조인 컬럼에 인덱스가 있으면 반복적인 스캔 없이 행을 매칭하는 데 도움이 됩니다.
3) ORDER BY(결과를 이미 정렬된 상태로 원할 때)
많은 행을 모아서 정렬하는 것은 비용이 큽니다. 예를 들어:
SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC;
user_id와 정렬 컬럼이 정렬 순서에 맞는 인덱스가 있으면 엔진이 필요한 순서로 행을 읽을 수 있어 큰 중간 정렬을 피할 수 있습니다.
4) GROUP BY(그룹화가 인덱스와 정렬 일치할 때)
그룹화는 데이터베이스가 그룹화된 순서로 데이터를 읽을 수 있을 때 이득이 날 수 있습니다. 보장은 없지만 필터와 함께 사용되거나 인덱스에 자연스럽게 클러스터되어 있으면 작업량이 줄어듭니다.
B-트리 인덱스는 범위 조건(날짜, 가격, BETWEEN 등)에 특히 강합니다:
SELECT * FROM orders
WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01';
대시보드, 리포트, "최근 활동" 화면에서 이 패턴은 매우 흔하고, 범위 컬럼에 인덱스를 추가하면 즉각적인 개선이 발생하는 경우가 많습니다.
요지는 간단합니다: 쿼리의 검색 및 정렬 방식과 인덱스가 일치할 때 인덱스가 가장 큰 도움을 줍니다. 쿼리가 이러한 접근 패턴과 맞으면 데이터베이스는 광범위한 스캔 대신 표적 읽기를 수행합니다.
인덱스는 데이터베이스가 건드려야 하는 행 수를 크게 줄일 때 가장 효과적입니다. 이 속성이 바로 **선택도(selectivity)**입니다.
선택도는 간단히 말해: 특정 값이 얼마나 많은 행과 일치하는가? 선택도가 높은 컬럼은 서로 다른 값이 많아 각 조회가 적은 행과 일치합니다.
email, user_id, order_number (종종 유니크 또는 거의 유니크)is_active, is_deleted, 몇 개 값만 가진 status선택도가 높으면 인덱스는 소수의 행으로 바로 이동할 수 있습니다. 선택도가 낮으면 인덱스가 테이블의 큰 부분을 가리키게 되어 데이터베이스는 여전히 많은 행을 읽고 필터링해야 합니다.
예를 들어 1,000만 행이 있고 is_deleted 컬럼의 98%가 false라면,
SELECT * FROM orders WHERE is_deleted = false;
이 쿼리에서 is_deleted 인덱스는 큰 도움이 되지 않습니다. 일치 집합이 거의 전체 테이블이기 때문에 인덱스를 사용하는 것이 엔진이 인덱스 항목과 테이블 페이지를 오가는 추가 작업을 하기 때문에 순차 스캔보다 느릴 수도 있습니다.
쿼리 플래너는 비용을 추정합니다. 인덱스가 작업량을 충분히 줄이지 못하면(너무 많은 행이 일치하거나 쿼리가 대부분의 컬럼을 요구하는 경우) 전체 스캔을 선택할 수 있습니다.
데이터 분포는 고정되어 있지 않습니다. status 컬럼이 처음에는 고르게 분포되었다가 시간이 지나 특정 값이 우세해질 수 있습니다. 통계가 갱신되지 않으면 플래너는 잘못된 결정을 내려 더 이상 이득을 주지 않는 인덱스를 계속 사용할 수도 있습니다.
단일 컬럼 인덱스는 좋은 출발점이지만, 많은 실제 쿼리는 한 컬럼으로 필터링하고 다른 컬럼으로 정렬하거나 추가 필터를 적용합니다. 이런 경우 복합(다중 컬럼) 인덱스가 유용합니다: 하나의 인덱스로 쿼리의 여러 요구를 충족시킬 수 있습니다.
대부분의 데이터베이스(특히 B-트리 기반)는 복합 인덱스를 왼쪽에서부터 효율적으로만 사용할 수 있습니다. 인덱스를 컬럼 A로 먼저 정렬하고, 그 안에서 컬럼 B로 정렬한다고 생각하세요.
따라서:
account_id로 필터한 뒤 created_at으로 정렬하거나 필터할 때 훌륭합니다created_at만으로 필터하는 쿼리에는 보통 도움이 되지 않습니다(왼쪽most가 아니기 때문)일반적인 작업은 “이 계정의 최신 이벤트를 보여줘”입니다. 이 쿼리 패턴:
SELECT id, created_at, type
FROM events
WHERE account_id = ?
ORDER BY created_at DESC
LIMIT 50;
다음 인덱스에서 큰 이득을 보는 경우가 많습니다:
CREATE INDEX events_account_created_at
ON events (account_id, created_at);
데이터베이스는 인덱스에서 해당 계정 부분으로 바로 이동해 시간 순으로 행을 읽을 수 있으므로 대량 스캔과 정렬을 피할 수 있습니다.
커버링 인덱스는 쿼리에 필요한 모든 컬럼을 포함하므로 데이터베이스가 테이블 행을 조회하지 않고 인덱스만으로 결과를 반환할 수 있습니다(읽기 횟수 감소, 랜덤 I/O 감소).
주의: 컬럼을 더 포함하면 인덱스가 커지고 비용이 늘어납니다.
넓은 복합 인덱스는 쓰기 지연과 저장 공간 증가를 초래합니다. 특정 고가치 쿼리에만 추가하고, EXPLAIN 플랜과 실제 측정으로 전후를 검증한 후에 도입하세요.
인덱스는 종종 “공짜 속도”로 묘사되지만, 실상은 아닙니다. 기본 테이블이 변경될 때마다 인덱스 구조도 유지되어야 하고 실제 리소스를 소비합니다.
새 행을 INSERT하면 데이터베이스는 행을 한 번 쓰는 것에 그치지 않고 그 테이블의 모든 인덱스에 대응하는 항목도 삽입합니다. DELETE와 많은 UPDATE도 마찬가지입니다.
이 때문에 인덱스가 많으면 쓰기 집약 워크로드에서 눈에 띄게 성능이 느려질 수 있습니다. 특히 인덱싱된 컬럼을 건드리는 UPDATE는 요즘 일부 엔진에서 오래된 인덱스 항목을 제거하고 새 항목을 추가해야 해 추가적인 페이지 분할이나 내부 재균형을 초래할 수 있습니다. 주문 이벤트, 센서 데이터, 감사 로그 같이 쓰기가 많은 앱이라면 모든 것을 인덱스하면 읽기는 빨라도 데이터베이스가 느려질 수 있습니다.
각 인덱스는 디스크 공간을 차지합니다. 큰 테이블에서 인덱스는 테이블 크기와 비슷하거나 더 클 수 있습니다, 특히 겹치는 인덱스가 여러 개일 때.
캐싱 측면에서도 영향이 있습니다. 데이터베이스는 캐시를 많이 사용하므로 작업 집합에 큰 인덱스가 여러 개 포함되면 캐시가 더 많은 페이지를 담아야 합니다. 그렇지 않으면 디스크 I/O가 늘고 성능이 예측 불가능해집니다.
인덱싱은 무엇을 가속화할지 선택하는 문제입니다. 읽기 중심 워크로드면 인덱스를 더 많이 두는 것이 가치가 있을 수 있습니다. 쓰기 중심이면 가장 중요한 쿼리를 지원하는 인덱스에 우선순위를 두고 중복 인덱스는 피하세요. 실용적인 규칙: 어떤 인덱스를 추가할 때는 그 인덱스가 돕는 쿼리를 명확히 말할 수 있어야 하며, 읽기 성능 향상이 쓰기·유지 비용을 상쇄하는지 검증하세요.
인덱스를 추가하는 것은 도움이 될 것 같지만 실제로 검증해야 합니다. 두 가지 도구는 쿼리 플랜(EXPLAIN)과 실제 전/후 측정입니다.
관심 있는 정확한 쿼리에 대해 EXPLAIN(또는 EXPLAIN ANALYZE)을 실행하세요.
EXPLAIN ANALYZE): 플랜이 100행을 예상했는데 실제로 100,000행을 건드렸다면 옵티마이저가 잘못 추정한 것입니다—대개 통계가 오래되었거나 필터가 예상보다 덜 선택적일 때 발생합니다.ORDER BY에 맞는 인덱스를 추가하면 이 정렬이 사라져 큰 이득을 줄 수 있습니다.같은 파라미터로, 대표적인 데이터 크기에서 쿼리를 벤치마크하고 지연시간(p50/p95), 스캔된 행 수, CPU/IO 영향을 기록하세요. 플랜이 바뀌었는지(인덱스 사용, 읽은 행 수 감소)도 확인하세요.
캐싱에 주의하세요: 첫 실행은 캐시에 데이터가 없어 느릴 수 있고, 반복 실행하면 캐시로 인해 인덱스 없이도 빠르게 보일 수 있습니다. 여러 번 실행해 비교하고 플랜 변화가 있는지에 집중하세요.
EXPLAIN ANALYZE에서 더 적은 행을 건드리고 비용이 큰 단계(정렬 등)가 줄었다면 인덱스가 실제로 도움이 된 것입니다.
올바른 인덱스를 추가했는데도 속도 향상이 없는 경우 쿼리 작성 방식이 인덱스 사용을 막는 경우가 있습니다. 쿼리는 여전히 올바른 결과를 반환하지만 느린 플랜을 강요받을 수 있습니다.
1) 앞쪽 와일드카드(Leading wildcards)
WHERE name LIKE '%term'
처럼 쓰면 일반 B-트리 인덱스는 사용할 수 없습니다. 정렬된 순서에서 '%term'이 어디서 시작할지 모르기 때문입니다. 대신 많은 행을 스캔하게 됩니다.
대안:
WHERE name LIKE 'term%'.2) 인덱스된 컬럼에 함수 적용
다음은 겉보기엔 무해해 보입니다:
WHERE LOWER(email) = '[email protected]'
하지만 LOWER(email)처럼 함수를 적용하면 기존 email 인덱스를 직접 사용할 수 없습니다.
대안:
lowercase 등) WHERE email = ...로 비교하세요.LOWER(email) 전용 표현식/함수 인덱스를 생성하세요(데이터베이스 종속).암시적 형변환: 다른 데이터 타입을 비교하면 한쪽을 캐스트해야 하고 이로 인해 인덱스 사용이 비활성화될 수 있습니다(예: 정수 컬럼을 문자열 리터럴과 비교).
문자열 정렬 규칙/인코딩 불일치: 비교에 사용되는 콜레이션이 인덱스가 생성된 콜레이션과 다르면 옵티마이저가 인덱스를 회피할 수 있습니다.
LIKE '%x')?LOWER(col), DATE(col), CAST(col))?EXPLAIN으로 실제 플랜을 확인했는가?인덱스는 "설정하고 잊어버릴" 수단이 아닙니다. 시간이 지나면서 데이터가 변하고 쿼리 패턴이 바뀌며 테이블과 인덱스의 물리적 형태가 흐트러집니다. 잘 선택된 인덱스도 유지하지 않으면 점차 비효율적이거나 해로워질 수 있습니다.
대부분의 데이터베이스는 쿼리 플래너(옵티마이저)를 사용해 쿼리를 어떻게 실행할지 결정합니다: 어떤 인덱스를 사용할지, 어떤 조인 순서를 택할지, 인덱스 조회가 가치가 있는지 등. 이런 결정을 위해 플래너는 값 분포, 행 수, 데이터 편향에 대한 통계를 사용합니다.
통계가 오래되면 플래너의 행 추정이 크게 빗나갈 수 있습니다. 이는 잘못된 플랜 선택으로 이어집니다: 예상보다 훨씬 많은 행을 반환하는 인덱스를 선택하거나, 인덱스를 사용했어야 할 상황에서 스캔을 선택하는 등.
일상적인 해결책: 정기적으로 통계 업데이트(보통 ANALYZE 등)를 스케줄하세요. 대량 로드, 대규모 삭제, 큰 변동이 있을 때는 더 빨리 갱신하세요.
행이 삽입·갱신·삭제되면서 인덱스에 블로트(쓸모없는 여유 페이지)와 단편화가 쌓일 수 있습니다. 결과는 더 큰 인덱스, 더 많은 읽기, 특히 범위 쿼리에서 느려지는 현상입니다.
해결책: 과도하게 커지거나 성능이 저하된 인덱스는 주기적으로 재구성(rebuild) 또는 재조직(reorganize) 하세요. 구체적인 도구와 영향은 데이터베이스마다 다르므로 무분별한 적용은 피하고 측정 기반으로 진행하세요.
다음 항목을 모니터링하세요:
이 피드백 루프는 언제 유지관리가 필요한지, 언제 인덱스를 조정하거나 제거해야 하는지 알려줍니다. 개선 검증에 관한 추가 자료는 /blog/how-to-prove-an-index-helps-explain-and-measurements를 참고하세요.
인덱스를 추가하는 것은 추측이 아니라 의도적인 변경이어야 합니다. 가벼운 워크플로를 따르면 측정 가능한 이득에 집중하고 "인덱스 스프롤"을 방지할 수 있습니다.
증거에서 시작하세요: 슬로우 쿼리 로그, APM 트레이스, 사용자 보고 등. 느리고 빈번한 쿼리 하나를 골라 시작하세요 — 드문 10초짜리 리포트보다 흔한 200ms 조회가 더 중요합니다.
정확한 SQL과 파라미터 패턴을 캡처하세요(예: WHERE user_id = ? AND status = ? ORDER BY created_at DESC LIMIT 50). 작은 차이가 어떤 인덱스가 도움이 되는지 바꿉니다.
현재 지연(p50/p95), 스캔된 행 수, CPU/IO 영향을 기록하세요. 비교를 위해 현재 플랜(EXPLAIN / EXPLAIN ANALYZE)을 저장하세요.
쿼리가 필터하고 정렬하는 방식을 반영하는 컬럼을 고르세요. 플랜이 큰 범위를 스캔하지 않도록 하는 최소한의 인덱스를 선호하세요.
프로덕션과 유사한 데이터 크기에서 스테이징으로 테스트하세요. 작은 데이터셋에서는 인덱스가 좋아 보이다가 규모가 크면 실망할 수 있습니다.
큰 테이블에서는 온라인 옵션을 사용하세요(예: PostgreSQL의 CREATE INDEX CONCURRENTLY). 데이터베이스가 쓰기를 잠글 수 있다면 낮은 트래픽 시간에 스케줄하세요.
같은 쿼리를 다시 실행하고 비교하세요:
인덱스가 쓰기 비용을 증가시키거나 메모리를 과도하게 차지하면 깔끔하게 제거할 수 있어야 합니다(가능한 경우 DROP INDEX CONCURRENTLY). 마이그레이션은 되돌릴 수 있도록 하세요.
마이그레이션 또는 스키마 노트에 인덱스가 지원하는 쿼리와 향상된 지표를 기록하세요. 미래의 당신(또는 동료)이 왜 존재하는지, 언제 삭제해도 되는지 알 수 있습니다.
새 서비스를 구축하면서 초기에 "인덱스 스프롤"을 피하고 싶다면, Koder.ai는 전체 루프를 빠르게 반복하는 데 도움이 됩니다: 채팅으로 React + Go + PostgreSQL 앱을 생성하고 스키마/인덱스 마이그레이션을 요구사항에 맞춰 조정한 뒤 소스 코드를 내보낼 수 있습니다. 실무에서는 "이 엔드포인트가 느리다"에서 "EXPLAIN 플랜, 최소 인덱스, 되돌릴 수 있는 마이그레이션"으로 빠르게 도달하는 것을 쉽게 합니다.
인덱스는 강력한 수단이지만 모든 문제를 해결하지는 않습니다. 때로는 요청의 느린 부분이 데이터베이스가 올바른 행을 찾은 이후에 발생하거나, 쿼리 패턴 자체가 인덱싱으로는 최선의 해결책이 아닐 수 있습니다.
이미 좋은 인덱스를 사용하는 쿼리가 여전히 느리다면 다음을 살펴보세요:
OFFSET 999000처럼 페이지 1000을 요청하면 인덱스가 있어도 느릴 수 있습니다. 키셋 페이징(마지막 본 id/타임스탬프 이후의 행) 사용을 권장합니다.SELECT *로 넓은 행을 반환하거나 수만 건을 반환하면 네트워크, JSON 직렬화, 애플리케이션 처리에서 병목이 발생할 수 있습니다.LIMIT을 걸며 의도적으로 페이지 처리추가 병목 진단 방법은 /blog/how-to-prove-an-index-helps와 병행하면 좋습니다.
추측하지 마세요. 시간 소비 위치(데이터베이스 실행 vs 반환된 행 vs 애플리케이션 코드)를 측정하세요. 데이터베이스가 빠른데도 API가 느리다면 인덱스를 더 만들어도 도움이 되지 않습니다.
데이터베이스 인덱스는 선택한 컬럼 값을 검색 가능한 정렬된 형태로 저장하고 테이블 행을 가리키는 포인터를 함께 가진 별도의 자료구조입니다(종종 B-트리). 데이터베이스는 이를 사용해 대부분의 테이블을 읽지 않고 선택적인 쿼리를 빠르게 답할 수 있습니다.
테이블의 두 번째 전체 복사본은 아니지만, 일부 컬럼 데이터와 메타데이터를 중복 저장하므로 추가 저장 공간을 소비합니다.
인덱스가 없으면 데이터베이스는 전체 테이블을 스캔해서 WHERE 절을 만족하는 행을 하나하나 검사해야 할 수 있습니다.
인덱스가 있으면 데이터베이스는 종종 일치하는 행 위치로 직접 이동해 그 행들만 읽을 수 있어서 디스크 I/O, 필터 평가에 쓰이는 CPU 작업, 캐시 압박을 줄일 수 있습니다.
B-트리 인덱스는 값들을 정렬된 상태로 유지하고 페이지 단위로 다른 페이지를 가리키는 구조를 가집니다. 데이터베이스는 빠르게 적절한 “근처”로 이동할 수 있습니다.
그래서 B-트리는 아래 두 경우 모두에 유리합니다:
WHERE email = ...)WHERE created_at >= ... AND created_at < ...)해시 인덱스는 값의 해시를 이용해 버킷으로 바로 이동하므로 동등 비교(=)에서 매우 빠를 수 있습니다.
단점:
많은 실제 워크로드에서 B-트리는 더 다양한 쿼리 패턴을 지원하기 때문에 기본 선택입니다.
인덱스는 일반적으로 다음 패턴에서 가장 큰 효과를 냅니다:
WHERE 필터(매치되는 행이 적음)JOIN 키(외래 키 등)ORDER BY(대형 정렬을 피할 수 있음)GROUP BY)에서 인덱스 정렬 순서를 이용할 수 있는 경우쿼리가 테이블의 큰 비율을 반환하면 인덱스의 이득은 작습니다.
선택도(Selectivity)는 특정 값에 대해 얼마나 많은 행이 일치하는지를 뜻합니다. 인덱스는 결과 집합을 크게 좁힐 때 효과를 발휘합니다.
저선택도 컬럼(예: is_deleted, is_active, 값이 몇 개뿐인 status)은 테이블의 큰 부분과 매치되므로 인덱스가 별로 도움이 되지 않습니다. 오히려 인덱스를 사용하면 인덱스 엔트리와 테이블 페이지를 오가는 추가 작업 때문에 순차 스캔보다 느려질 수 있습니다.
옵티마이저는 인덱스를 사용해도 작업량이 충분히 줄지 않을 것으로 판단하면 인덱스를 무시합니다.
흔한 이유:
대부분의 B-트리 구현에서 인덱스는 첫 번째 컬럼을 기준으로 정렬되고, 그 다음 컬럼으로 세분화됩니다. 그래서 데이터베이스는 왼쪽에서부터 시작하는 컬럼들로만 인덱스를 효율적으로 사용할 수 있습니다.
예시:
(account_id, created_at) 인덱스는 WHERE account_id = ?와 시간 기반 필터/정렬에 아주 유리합니다.created_at만으로 필터하는 쿼리에는 보통 도움이 되지 않습니다(왼쪽most가 아니기 때문).커버링 인덱스는 쿼리에 필요한 모든 컬럼을 포함하므로 데이터베이스가 테이블 행을 조회하지 않고도 인덱스만으로 결과를 반환할 수 있습니다.
장점:
단점:
특정 고가치 쿼리에 대해서만 필요 최소한으로 사용하는 것이 좋습니다.
확인 방법은 두 가지입니다:
EXPLAIN / EXPLAIN ANALYZE로 플랜이 바뀌었는지 확인하세요(예: Seq Scan → Index Scan/Seek, 읽은 행 수 감소, 정렬 단계 제거).또한 새 인덱스가 // 성능에 미치는 영향도 관찰해야 합니다.
INSERTUPDATEDELETE