[DB] 카디널리티(Cardinality)란 무엇이며 왜 알아야 할까?
인덱스를 걸었는데 왜 여전히 Full Table Scan이 발생할까요? 인덱스 설계의 핵심 지표인 카디널리티(Cardinality)의 개념과 옵티마이저가 인덱스를 외면하게 되는 원리에 대해 알아봅시다.
데이터가 쌓일수록 왜 조회 성능이 느려지지?
수백만건 데이터 속 원하는 정보를 찾기 위해 하염없이 기다려본 경험이 있으신가요? 쿼리 성능 저하의 원인은 다양하지만, 대부분 비효율적인 데이터 조회방식에 있습니다.
범인은 Full Table Scan : 그래서 우리는 인덱스를 쓴다
DB가 특정 데이터를 찾기위해 테이블의 모든 행을 하나씩 전부 확인하는 방식입니다. 수백만 권 책이 있는 도서관에서 원하는 책 한 권을 찾기 위해 모든 책의 제목을 일일이 확인하는 것과 같습니다. 이 방식의 문제는 데이터가 많아질수록 성능은 기하급수적으로 저하된다는 것입니다.
이런 비효율을 해결하기 위해 우리는 인덱스를 사용합니다. 인덱스는 DB의 목차같은 역할을 합니다. 인덱스를 사용하면 전체를 훑지않고 데이터가 있는 위치로 바로 점프할 수 있어 검색 속도를 획기적으로 높일 수 있습니다.
그런데 왜 옵티마이저는 여전히 Full Table Scan을 선택하지?
모든 데이터를 하나씩 확인 (순차 검색)
목차를 통해 해당 위치로 바로 점프 (이진 탐색 등)
ID: 111
Name: Choi
하지만 인덱스가 만능은 아닙니다. 분명 인덱스를 걸었음에도 DBMS 옵티마이저가 이를 무시하고, 여전히 Full Table Scan을 선택하는 경우가 있습니다.
왜 이런 일이 발생할까요? 옵티마이저는 무조건 인덱스를 타는 게 아니라, 매번 비용(Cost)을 계산해서 가장 효율적인 방법을 선택하기 때문입니다.
이때 옵티마이저가 “인덱스 쓰는게 오히려 더 느리겠는데? 그냥 다 읽자!”라고 판단하게 만드는 결정적인 기준이 바로 카디널리티(Cardinality)입니다.
카디널리티 (Cardinality) : 값의 고유한 정도
카디널리티란 특정 칼럼에 포함된 값들의 고유한 정도(Uniqueness)를 의미합니다.
→ 데이터를 거의 유일하게 식별 가능한 컬럼
→ 몇 가지 정해진 값만 반복해서 나타나는 컬럼들
- 카디널리티가 높다 : 전체 행 개수 대비 중복되지 않는 값의 개수가 많다.
- → 데이터를 거의 유일하게 식별할 수 있는 컬럼들
- 카디널리티가 낮다 : 전체 행 개수 대비 중복되는 값의 개수가 많다.
- → 몇 가지 정해진 값만 반복해서 나타나는 컬럼들
카디널리티가 낮다 = 옵티마이저가 인덱스 선택 안 할 확률이 높다
조회 시 검색 범위가 즉시 줄어들어
최소한의 작업으로 데이터 발견
인덱스를 타도 남는 데이터가 너무 많음
결국 대량의 행을 추가로 확인해야 함
인덱스의 궁극적 목적은 검색 범위를 좁혀서 Random I/O 비용을 줄이는 것입니다.
따라서 카디널리티가 높은 컬럼의 경우(중복이 없으면), 인덱스를 타는 순간 검색 범위가 아주 좁게 좁혀집니다. 몇 번만 점프(Random I/O)만 하면 되기 때문에 옵티마이저는 인덱스를 선택합니다.
반면, 카디널리티가 낮은 컬럼의 경우(중복이 많으면), 인덱스를 타더라도 여전히 수십만 건이 남습니다. 수십만 번 점프해야하기 때문에, 옵티마이저는 인덱스 대신 Full Table Scan을(Sequential I/O) 선택합니다. 보통 데이터베이스 엔진은 인덱스를 통해 읽어야 할 데이터가 전체 데이터의 약 10%~25%를 넘어가면, 인덱스가 있어도 사용하지 않고 Full Table Scan을 선택합니다.
[심화] 카디널리티가 낮아도 인덱스 쓰게 만드는 법 : 복합인덱스 (Composite Index)
그렇다면 카디널리티가 낮은 컬럼은 무조건 인덱스에서 제외할까요?
그렇지 않습니다. 복합인덱스를 활용하면 이야기가 달라집니다.
(Low Cardinality)
'완료' 상태만 걸러내도 수만 건이 남아
결국 대량의 행을 추가 확인해야 함
(Low Cardinality)
(High Cardinality)
'완료' 상태 중에서 '특정 일시'만
골라내어 성능이 비약적 향상
카디널리티가 낮은 칼럼이라도, 다른 칼럼과 결합하면 강력한 필터가 될 수 있습니다.
ex. (결제상태, 결제일시)
결제상태라는 낮은 카디널리티 컬럼만으로는 비효율적이지만,결제일시라는 높은 카디널리티 칼럼과 결합하면, “완료상태 + 어제날짜” 같은 특정범위의 데이터를 매우 빠르게 찾을 수 있습니다.
⚠️ [예외] 카디널리티가 높은데도 인덱스 안 쓰는 경우 : 비용(Cost)의 역설
그러나 카디널리티가 인덱스 성능의 유일한 척도는 아닙니다.
옵티마이저는 카디널리티가 높아도 전체 비용(Cost) 관점에서 종합적으로 계산하여 최적의 경로를 선택하기 때문입니다. 따라서 다음과 같은 특정 환경에서는 높은 카디널리티의 이점이 상쇄될 수 있습니다.
데이터가 적으면 인덱스를 거치는 오버헤드가
실제 데이터를 읽는 시간보다 큼
인덱스 한 페이지에 담기는 정보량이 줄어,
디스크 접근 횟수 증가 및 메모리 낭비
1) 테이블의 전체 데이터 양이 너무 적은 경우
전체 데이터 수가 너무 적으면 인덱스를 사용하는 것 자체가 오히려 시간 낭비가 될 수 있습니다. 전체 데이터 수가 5개인 테이블에서 특정 단어를 찾는다고 가정해 봅시다. 인덱스 페이지를 열어 위치를 확인하고 다시 테이블을 보는 것보다, 그냥 처음부터 끝까지 훑어보는 게 훨씬 빠르겠죠?
인덱스를 읽고 주소를 찾아가는 준비 단계 비용이, 테이블을 그냥 다 읽어버리는 비용보다 크기 때문에 옵티마이저는 인덱스를 무시하게 됩니다.
2) 데이터의 길이가 너무 긴 경우
VARCHAR(2000)처럼 매우 긴 텍스트 컬럼에 인덱스를 걸면, 인덱스 자체의 크기가 비대해집니다. DB는 인덱스를 페이지라는 일정한 크기 단위로 나누어 관리하는데, 데이터가 길어지면 다음과 같은 문제가 발생합니다.
- I/O 비용 증가 : 한 페이지에 담을 수 있는 인덱스 정보가 몇 개 안되다보니, 평소라면 1개만 읽어도 될 것을 5개, 10개씩 읽어야 합니다. 결국 디스크를 읽는 횟수(I/O)가 늘어나 속도가 느려집니다.
- 메모리 효율 저하 : 이렇게 무거워진 인덱스들이 메모리 버퍼 공간을 많이 차지하게 됩니다. 따라서 정작 자주 쓰이는 다른 데이터들이 메모리에 올라오지 못하고 밀려나게 되면서 전체적인 DB 성능이 떨어지는 원인이 됩니다.
카디널리티를 고려하며 인덱스를 쓰자
데이터가 쌓일수록 인덱스는 성능을 높여주는 좋은 도구입니다.
하지만 무조건 인덱스를 사용하는 것이 높은 성능을 보장하는 것은 아닙니다. 카디널리티가 낮은 칼럼에 무작정 걸어버리먼 오히려 성능에 악영향을 줍니다. 인덱스 주소를 확인하고 실제 데이터를 하나하나 찾아가는 Random I/O 비용이 테이블 전체를 훑는 비용보다 크기 때문입니다.
결국 중요한 것은 단순히 인덱스를 ‘만드는 것’이 아니라, 옵티마이저가 기꺼이 선택할 수밖에 없는 인덱스를 ‘설계하는 것’입니다. 카디널리티로 데이터 분포도를 파악하면 전략적인 인덱스를 설계할 수 있습니다. 오늘 배운 카디널리티의 원리와 I/O 비용의 관계를 바탕으로, 여러분의 쿼리가 가장 효율적인 길을 찾아갈 수 있도록 전략적인 인덱스를 설계해 보시길 바랍니다.
댓글남기기