PostgreSQL에서 테이블 칼럼 순서 지정
GitLab v19.4요약
GitLab에서는 새 테이블의 칼럼을 공간을 가장 적게 쓰는 순서로 배치하도록 요구합니다. C 구조체와 마찬가지로 테이블이 차지하는 공간은 칼럼 순서에 영향을 받습니다. 첫 칼럼은 4바이트 정수입니다. 행과 행 사이의 공간도 정렬 패딩의 대상입니다.
GitLab에서는 새 테이블의 칼럼을 공간을 가장 적게 쓰는 순서로
배치하도록 요구합니다. 간단한 방법은 타입 크기를 기준으로 내림차순으로
정렬하고 가변 크기 타입(text, varchar, 배열, json, jsonb 등)을
맨 뒤에 두는 것입니다.
C 구조체와 마찬가지로 테이블이 차지하는 공간은 칼럼 순서에 영향을 받습니다. 뒤따르는 칼럼의 타입에 따라 칼럼 크기가 정렬되기 때문입니다. 다음 예를 살펴봅니다.
id(integer, 4바이트)name(text, 가변)user_id(integer, 4바이트)
첫 칼럼은 4바이트 정수입니다. 다음은 가변 길이 텍스트입니다. text 데이터
타입은 1워드 정렬을 요구하며, 64비트 플랫폼에서 1워드는 8바이트입니다.
정렬 요구 사항을 충족하기 위해 첫 칼럼 바로 뒤에 0 네 개가 추가되므로,
id가 4바이트를 차지하고 이어서 4바이트의 정렬 패딩이 들어간 다음에야
name 이 저장됩니다. 따라서 이 경우 4바이트 정수를 저장하는 데
8바이트를 쓰게 됩니다.
행과 행 사이의 공간도 정렬 패딩의 대상입니다. user_id 칼럼은 4바이트만
차지하며, 64비트 플랫폼에서는 다음 행을 "깨끗한" 워드 경계에서 시작할 수
있도록 정렬 패딩으로 0 네 개가 추가됩니다.
그 결과 각 칼럼의 실제 크기는 가변 길이 데이터와 24바이트 튜플 헤더를 제외하면 8바이트, 가변, 8바이트가 됩니다. 즉 4바이트 정수 두 개를 위해 각 행마다 최소 16바이트가 필요합니다. 테이블의 행이 몇 개 되지 않는다면 문제가 되지 않습니다. 그러나 수백만 행을 저장하기 시작하면 순서를 달리해 공간을 절약할 수 있습니다. 위 예에서 가장 좋은 칼럼 순서는 다음과 같습니다.
id(integer, 4바이트)user_id(integer, 4바이트)name(text, 가변)
또는 다음과 같습니다.
name(text, 가변)id(integer, 4바이트)user_id(integer, 4바이트)
이 예에서는 id와 user_id 칼럼이 함께 묶이므로 두 값을 저장하는 데
8바이트만 있으면 됩니다. 결과적으로 각 행이 8바이트씩 적은 공간을
사용합니다.
Ruby on Rails 5.1부터 ID의 기본 데이터 타입은 8바이트를 사용하는 bigint 입니다.
여기서는 재배치 시나리오를 더 현실적으로 보여 주기 위해 integer를 사용합니다.
타입 크기#
PostgreSQL 문서에 많은 정보가 있지만, 여기서는 찾아보기 쉽도록 자주 쓰는 타입의 크기를 정리합니다. 여기서 "워드"는 워드 크기를 뜻하며, 32비트 플랫폼에서는 4바이트, 64비트 플랫폼에서는 8바이트입니다.
| 타입 | 크기 | 필요한 정렬 |
|---|---|---|
smallint |
2바이트 | 1워드 |
integer |
4바이트 | 1워드 |
bigint |
8바이트 | 8바이트 |
real |
4바이트 | 1워드 |
double precision |
8바이트 | 8바이트 |
boolean |
1바이트 | 불필요 |
text / string |
가변, 1바이트 + 데이터 | 1워드 |
bytea |
가변, 1 또는 4바이트 + 데이터 | 1워드 |
timestamp |
8바이트 | 8바이트 |
timestamptz |
8바이트 | 8바이트 |
date |
4바이트 | 1워드 |
"가변" 크기는 실제 크기가 저장되는 값에 따라 달라진다는 뜻입니다. PostgreSQL 이 값을 행에 직접 포함할 수 있다고 판단하면 그렇게 하지만, 값이 매우 크면 데이터를 외부에 저장하고 칼럼에는 1워드 크기의 포인터를 저장합니다. 이 때문에 가변 크기 칼럼은 항상 테이블 맨 뒤에 두어야 합니다.
실제 예시#
events 테이블을 예로 듭니다. 현재 이 테이블의 레이아웃은 다음과
같습니다.
| 칼럼 | 타입 | 크기 |
|---|---|---|
id |
integer | 4바이트 |
target_type |
character varying | 가변 |
target_id |
integer | 4바이트 |
title |
character varying | 가변 |
data |
text | 가변 |
project_id |
integer | 4바이트 |
created_at |
timestamp without time zone | 8바이트 |
updated_at |
timestamp without time zone | 8바이트 |
action |
integer | 4바이트 |
author_id |
integer | 4바이트 |
칼럼을 정렬하기 위해 패딩을 추가하면 칼럼이 다음과 같이 고정 크기 청크로 나뉩니다.
| 청크 크기 | 칼럼 |
|---|---|
| 8바이트 | id |
| 가변 | target_type |
| 8바이트 | target_id |
| 가변 | title |
| 가변 | data |
| 8바이트 | project_id |
| 8바이트 | created_at |
| 8바이트 | updated_at |
| 8바이트 | action, author_id |
즉 가변 크기 데이터와 튜플 헤더를 제외하면 행마다 최소 8 * 6 = 48바이트가 필요합니다.
대신 다음 칼럼 순서를 사용해 이를 최적화할 수 있습니다.
| 칼럼 | 타입 | 크기 |
|---|---|---|
created_at |
timestamp without time zone | 8바이트 |
updated_at |
timestamp without time zone | 8바이트 |
id |
integer | 4바이트 |
target_id |
integer | 4바이트 |
project_id |
integer | 4바이트 |
action |
integer | 4바이트 |
author_id |
integer | 4바이트 |
target_type |
character varying | 가변 |
title |
character varying | 가변 |
data |
text | 가변 |
이렇게 하면 다음과 같은 청크가 만들어집니다.
| 청크 크기 | 칼럼 |
|---|---|
| 8바이트 | created_at |
| 8바이트 | updated_at |
| 8바이트 | id, target_id |
| 8바이트 | project_id, action |
| 8바이트 | author_id |
| 가변 | target_type |
| 가변 | title |
| 가변 | data |
이 경우 가변 크기 데이터와 24바이트 튜플 헤더를 제외하면 행마다
40바이트만 필요합니다. 8바이트 절약이 크지 않아 보일 수 있지만,
events 테이블처럼 큰 테이블에서는 의미가 생깁니다. 예를 들어
8000만 행을 저장할 때 칼럼 몇 개의 순서를 바꾸는 것만으로
최소 610MB의 공간을 절약합니다.