[DB] PK (Primary Key) ID 길이가 성능에 영향을 미칠까?
Primary Key ?
- PK (Primary Key)는 데이터베이스에서 각 행(row)을 고유하게 식별하기 위한 열(column)이다.
- PK는 효율적인 데이터 검색, 외래 키 참조, 데이터 무결성 보장을 위해 사용된다.
PK 길이가 길어질 때의 문제점은 무엇일까?
- 저장 공간 증가: 긴 PK는 테이블의 저장 공간을 늘리고, 외래 키로 참조되는 테이블에도 영향을 미친다.
- 인덱스 크기 증가로 인한 검색 성능 저하: PK는 기본적으로 인덱스로 관리되며, 길이가 길수록 인덱스 크기가 증가하고 검색 속도가 느려진다.
- 외래 키 테이블의 추가 비용: 외래 키가 긴 PK를 참조할 때, 외래 키 테이블도 더 많은 공간을 차지하게 된다.
PK 길이가 길어질 때 인덱스 크기 증가로 인한 검색 성능 저하에 대해서 더 자세히 알아보겠다.
- PK는 기본적으로 클러스터드 인덱스(Clustered Index)로 관리된다.
- MySQL은 PK를 기준으로 유사한 값들이 함께 조회되는 경우가 많다는 점에서 착안해, PK가 유사한 레코드들끼리 묶어서 저장한다. 유사한 것들을 묶는 것을 클러스터링이라고 하는데, 그래서 일반적으로 PK는 클러스터 인덱스(Clustered Index)라고도 불리며, 그 외의 일반적인 인덱스는 논클러스터 인덱스로 불린다. 그리고 이러한 클러스터링 특성 때문에 레코드의 저장이나 PK의 변경은 처리 속도가 느리다.(참고)
- 그래서 클러스터드 인덱스는 테이블의 데이터가 PK 값 순서대로 정렬되며, 인덱스 노드와 데이터 페이지가 하나의 구조로 관리된다.
- PK를 기반으로 자동 생성된 인덱스는 테이블의 데이터 정렬 및 검색에 사용되며, 모든 조회, 삽입, 삭제, 갱신 작업에 영향을 미친다.
Index란?
DB에서 index 는 단어 그대로 색인자의 역할을 한다. 보통 한 개의 테이블에는 수십 개의 column이 존재하며, MySQL은 데이터 조회 시 가장 첫 번째 column부터 차례대로 검색하기 시작한다. 만약 index 가 존재하지 않는다면 데이터를 조회할 때 언제나 테이블 전체를 Full scan 해야 하는데 이는 비용과 성능 면에서 매우 비효율적이다.
하지만 데이터가 Create, Update, Delete 되는 경우 Index도 함께 수정돼야 하기 때문에 index가 없는 경우보단 효율이 떨어진다. 즉 Read 성능을 살리고 CUD를 성능을 희생한다고 볼 수 있다. 따라서 잦은 수정이 필요한 테이블의 경우는 오히려 index를 활용하지 않는게 더 이로울 수 있다.(참고)
Page란?
Page(페이지)는 데이터베이스에서 데이터를 저장하는 기본 단위이다. MySQL에서는 innodb_page_size 시스템 변수로 4KB ~ 64 KB 사이 값 선택 가능하고, 기본 값은 16KB이다. B+Tree의 각 노드는 페이지로 구성되며, 각 페이지는 인덱스 키와 자식 노드 주소를 포함합니다. (B+Tree는 아래에서 더 자세하게 살펴보겠다.)
인덱스 페이지 내 키 값 구성 예시를 보면,
- 인덱스 페이지 크기: 16KB (16 * 1024 = 16,384)
- 인덱스 키 값: 28 Byte
- 인덱스 키 영역: 16 Byte
- 자식 노드 주소 영역: 12 Byte
- 한 키 값의 전체 크기 = 16 + 12 = 28 Byte
- 한 페이지에 저장 가능한 키 값 수 : 16,384/28 = 585개
- 한 페이지에 약 585개의 키 값을 저장할 수 있다.
키 값 크기와 페이지 효율 비교하면 키 값이 작을수록 페이지에 저장 가능한 키 수가 증가하는 것을 볼 수 있다.
키 값 크기 | 16KB 페이지 내 저장 가능 키 수 |
20 bytes | 16,384/20 = |
28 bytes | 16,384/28 = |
36 bytes | 16,384/36 = 455 |
64 bytes | 16,384/64 = 256 |
그렇기에 페이지 크기와 키 값 크기의 관계를 보면,
- 키 값 크기가 커질수록 한 페이지에 저장할 수 있는 키 값 수가 줄어든다.
- 페이지 크기가 클수록 한 페이지에 더 많은 키 값을 저장할 수 있어 트리의 높이가 낮아진다.
그러면 페이지 크기가 크면 무조건 좋은가에 대한 생각이 들 수 있지만, 페이지 크기가 클수록 트리의 높이가 줄어들지만, 큰 페이지는 메모리 사용량과 디스크 I/O 비용이 증가할 수 있기 때문에 주의하여야 한다.
- 디스크 I/O 비용 증가 : 데이터베이스는 필요한 데이터를 페이지 단위로 읽어온다. 그래서 페이지 크기가 크면 요청한 데이터 외에 불필요한 데이터도 함께 로드하게 된다.
- 예로, 4KB 페이지 크기에서 100바이트의 데이터를 읽으면 약 4KB를 로드하지만, 64KB 페이지 크기에서는 동일한 100바이트의 데이터를 읽기 위해 64KB를 로드하게 된다. 페이지 크기를 너무 크게 잡을 시, 디스크에서 더 많은 데이터를 읽으므로 I/O 비용이 증가한다.
- 또한, 페이지가 클수록 데이터를 수정할 때 변경된 작은 데이터(예: 100바이트)를 반영하기 위해 전체 페이지를 다시 저장해야 한다.
- 큰 페이지 크기는 특정 데이터를 찾기 위해 더 많은 데이터를 탐색해야 하므로 랜덤 I/O 비용을 증가시킨다.
Page Split은 인덱스에 새로운 데이터가 삽입될 때 기존 페이지에 빈 공간이 부족하면 발생한다.
새로운 페이지를 생성하여 기존 데이터를 분리하고, 삽입 데이터를 적절한 위치에 배치한다.
(문제점) Page Split은 추가적인 디스크 I/O를 유발하며 성능 저하를 초래할 수 있다. 특히, 랜덤하게 생성되는 UUID는 Page Split이 빈번하게 발생한다.
MySQL 은 PK 를 설정하면 해당 값을 index 로 잡아 데이터를 B+tree 구조로 저장한다.
InnoDB의 클러스터링 인덱스도 사실상 B-Tree 구조로 관리된다.
하지만 클러스터링 인덱스는 테이블의 데이터 자체를 포함하고 있으며, 다음과 같은 특징이 있다.
데이터와 인덱스의 결합
- 클러스터링 인덱스는 B-Tree 구조로 정렬되어 있지만, 인덱스의 각 노드(리프 노드)는 테이블의 실제 레코드 자체를 포함 .
- 클러스터링 인덱스를 통해 데이터를 찾으면, 바로 해당 레코드의 모든 데이터를 가져올 수 있음.
- 이를 인덱스-오거나이즈드 테이블(Index-Organized Table) 이라고도 부름
B+Tree 구조
- 클러스터링 인덱스도 실제로는 B+Tree 구조에 기반하고 있음
- 기본 키를 기준으로 데이터를 정렬하고, B-Tree 구조의 각 노드가 테이블의 레코드를 가리키는 것이 아니라 레코드를 포함하고 있는 형태임
- 이 때문에 클러스터링 인덱스도 B+Tree 인덱스의 일종으로 간주할 수 있음.
(참고)
B+Tree?
B+Tree는 데이터베이스에서 인덱스를 관리하기 위해 사용되는 트리 자료 구조다. 모든 리프 노드가 동일한 깊이에 있으며, 리프 노드에는 실제 데이터의 위치(포인터)가 저장된다.
- 각 노드는 키 값(PK)을 기준으로 정렬.
- 리프 노드(Leaf Node)는 실제 데이터 행(Row)에 대한 포인터를 포함.
- 비 리프 노드(Non-leaf Node)는 탐색 경로를 제공하며, 리프 노드로 이어짐.
B+Tree의 구조
- 루트 노드: 트리의 최상단 노드. 검색 시작점.
- 중간 노드: 탐색 경로를 제공.
- 리프 노드: 실제 데이터 위치를 가리키는 포인터를 포함.
[50]
/ \
[25] [75]
/ \ / \
[1~24] [26~49] [51~74] [76~100]
- 루트 노드: [50]
- 중간 노드: [25], [75]
- 리프 노드: [1~24], [26~49], [51~74], [76~100]
PK가 인덱스로 설정되면
- 데이터는 PK 값 기준으로 정렬되며, 빠른 검색이 가능하다.
- 키 값이 길어질수록 트리의 높이(Depth)가 증가하여 성능에 영향을 미친다.
그렇기 때문에 PK 가 길어지면, 아래와 같은 현상이 발생한다.
→ 인덱스 키 값 크기 증가
→ 인덱스 페이지에 담을 수 있는 인덱스 키 값 갯수 감소
→ B-Tree 깊이 증가
→ 디스크 읽기 증가
(참고)
예시 1) PK: bigint (Auto-increment)
- PK 값이 짧고 순차적으로 증가하기 때문에 트리의 균형이 유지되고 노드에 더 많은 키 값을 저장할 수 있음.
- 노드에 많은 키 값을 저장할 수 있어 트리의 높이가 낮음
- 트리의 높이가 낮아 탐색 속도가 빠름.
- 디스크 I/O가 최소화되어 검색 성능이 우수.
예시 2) PK: UUID
- PK 값이 길고 랜덤하게 생성되므로 트리의 균형을 유지하기 어렵고, 노드에 저장 가능한 키 값 수가 줄어듦.
- 노드 당 저장할 수 있는 키 값 수가 줄어들어 트리의 높이가 증가
- 트리의 높이가 증가하여 탐색 속도가 느려짐.
- 디스크 I/O가 증가하여 검색 성능이 저하.
[PK 선택 시 고려해야 할 사항]
1. 성능
2. 분산 시스템
-> UUID를 사용해야 하는 경우:
데이터가 분산 환경에서 생성될 때.
각 시스템 간 데이터 충돌을 방지해야 할 때.
- 장점: 전역 고유성 보장.
- 단점: 길이가 길고, 랜덤한 값이기 때문에 인덱스 정렬 효율이 떨어짐.
-> Auto-increment가 적합한 경우:
단일 데이터베이스에서 간단한 고유성이 필요할 때.
- 장점: 짧고 정렬된 값으로 성능이 좋음.
- 단점: 분산 시스템에서 고유성을 보장하기 어려움.