PostgreSQL 배열, 편리함 뒤에 숨겨진 성능 함정!
PostgreSQL 배열은 간편한 사용성을 제공하지만, 내부적으로는 독자적인 메모리 관리 및 인덱싱 전략을 사용함
배열은 관계형 데이터베이스(Relational Database)의 정규화(Normalization) 원칙에서 벗어나 데이터 격리 아키텍처(Data Isolation Architecture)를 구현하므로, 성능 저하를 유발할 수 있음
GIN 인덱스(Generalized Inverted Index)는 배열 검색에 필수적이지만, 쓰기 연산(Write Operations) 시 성능 저하를 야기할 수 있음
대용량 배열 수정 시 TOAST 메커니즘(TOAST Mechanism)으로 인해 성능 병목 현상이 발생하며, LZ4 압축을 통해 완화 가능함
배열과 관계형 데이터베이스 설계의 트레이드오프
PostgreSQL 배열은 데이터 격리 아키텍처(Data Isolation Architecture)를 통해 관련 데이터를 한 곳에 저장하여 읽기 성능을 향상시킬 수 있다. 하지만, 관계형 데이터베이스의 핵심 원칙인 참조 무결성(Referential Integrity)을 보장하지 못한다. 즉, 배열 내 요소에 대한 외래 키(Foreign Key) 제약 조건이나 CASCADE와 같은 삭제 연산이 지원되지 않아, 데이터 불일치(Data Inconsistency)가 발생할 수 있다. 따라서, 관계 간의 복잡한 관계를 관리해야 한다면, 배열보다는 별도의 연결 테이블을 사용하는 것이 더 적합하다.
배열 인덱싱의 이해: GIN vs B-tree
PostgreSQL에서 배열을 인덱싱할 때, 전체 배열의 일치 여부나 정렬을 위한 경우가 아니라면 B-tree 인덱스(B-tree Index)는 거의 쓸모가 없다. 대신, 배열 내 요소의 존재 여부를 빠르게 검색하기 위해 GIN 인덱스(Generalized Inverted Index)를 사용해야 한다. GIN 인덱스는 셋 연산(Set Operations)에 특화되어 있으며, 배열 내 특정 요소의 포함 여부를 확인하는 데 효과적이다. 하지만, GIN 인덱스는 쓰기 연산 시 성능 저하(Performance Degradation)를 유발할 수 있으며, 대량의 데이터 변경 시 VACUUM 작업을 통해 관리해야 한다.
TOAST 메커니즘과 대용량 배열의 성능 문제
PostgreSQL에서 배열의 크기가 2KB를 초과하면, TOAST(The Oversized-Attribute Storage Technique) 메커니즘을 통해 별도의 저장 공간으로 이동된다. 이로 인해 배열 수정 시, 전체 배열을 다시 읽고, 변경 사항을 적용한 후 다시 저장하는 과정이 필요하므로, 성능 병목 현상이 발생할 수 있다. 특히, TOAST된 배열을 수정하는 것은 CPU 및 I/O 집약적인 작업이 되어, 전체 데이터셋(Dataset)을 다시 쓰는 것과 유사한 결과를 초래한다. 이러한 문제를 완화하기 위해, LZ4 압축 알고리즘을 사용하여 압축 및 압축 해제 속도를 향상시킬 수 있다.
intarray 확장 기능과 특수 배열의 활용
PostgreSQL의 intarray 확장은 4바이트 정수(int4/integer) 배열에 특화된 함수와 인덱스 연산자를 제공하여, 일반 배열보다 훨씬 빠른 성능을 제공한다. intarray를 사용하면, 복잡한 논리 연산을 단일 쿼리 문자열로 표현할 수 있으며, 배열 정렬 및 중복 제거와 같은 작업을 간편하게 처리할 수 있다. 하지만, intarray는 32비트 정수로 제한되므로, 20억 이상의 값을 처리해야 하는 경우에는 사용할 수 없다. pgvector와 같은 확장 기능을 통해, 배열을 유사성 검색(Similarity Search)에 활용할 수 있으며, 데이터의 구조적 특성을 고려하여 성능을 최적화할 수 있다.