PostgreSQL 배열 타입과 검색 최적화
PostgreSQL 배열 타입의 기본 개념과 쿼리 성능 개선을 위한 인덱스·설계·튜닝 전략을 실무 관점에서 정리한 기술자료
목차
소개
PostgreSQL은 배열(array) 타입을 지원한다. 여러 값을 한 칼럼에 담을 수 있어 설계가 단순해진다. 다만 배열을 남용하면 검색 성능 저하가 발생한다. 본문은 배열 타입의 기본 사용법부터 검색을 빠르게 만드는 인덱스와 쿼리 작성법, 설계 대안까지 실무에서 바로 적용 가능한 내용으로 구성한다.
배열 타입 기본과 사용 방식
기본 개념
배열은 동일 타입 값을 순차적으로 저장한다. 간단한 예로 정수 목록이나 태그 목록을 저장할 때 활용된다. 배열 칼럼은 선언이 쉽다.
CREATE TABLE articles (
id serial PRIMARY KEY,
title text,
tags text[]
);
INSERT INTO articles (title, tags) VALUES
('첫 글', ARRAY['postgres','array','search']),
('두번째 글', ARRAY['sql','performance']);
조회 연산자
배열 관련 주요 연산자와 함수는 다음과 같다.
- @> : 포함(containment). 오른쪽 배열을 왼쪽이 포함하는지 검사
- && : 겹침(overlap). 두 배열이 공통 값을 가지는지 검사
- ANY / = ANY : 배열 내 임의 값 비교
- unnest(), array_length(), cardinality() 등 유틸 함수
-- 특정 태그를 포함하는 행
SELECT * FROM articles WHERE tags @> ARRAY['postgres'];
-- 겹치는 태그가 있는 행
SELECT * FROM articles WHERE tags && ARRAY['performance','search'];
-- 배열 안의 원소 중 하나와 비교
SELECT * FROM articles WHERE 'sql' = ANY(tags);
검색 성능 문제와 원인
빈번한 성능 병목 패턴
배열을 그대로 쓰면 다음 문제로 성능 저하가 발생한다.
- 전체 스캔 증가: 배열 연산자가 적절한 인덱스를 사용하지 못할 때
- 불필요한 널·중복 검사 비용 증가
- 복잡한 조건에서 함수 호출(unnest 등)로 인한 CPU 부하
따라서 배열 사용 시 검색 패턴을 먼저 분석하는 것이 중요하다.
검색 최적화 전략
1. 적절한 인덱스 선택
배열 검색에서 가장 많이 쓰이는 인덱스는 GIN이다. GIN은 다중 키 구조(multi-key)를 효율적으로 처리해 @>나 && 연산에 적합하다. 단, 쓰기 비용이 증가하므로 업데이트/삽입 빈도를 고려해야 한다.
-- GIN 인덱스 생성 예시
CREATE INDEX idx_articles_tags_gin ON articles USING GIN (tags);
-- 배열 포함 검색은 이 인덱스를 사용함
EXPLAIN ANALYZE SELECT * FROM articles WHERE tags @> ARRAY['postgres'];
2. 표현식 인덱스와 부분 인덱스
특정 검색 패턴이 자주 사용되면 표현식 인덱스나 부분 인덱스로 범위를 좁힐 수 있다. 예를 들어 최근 데이터만 조회하거나 특정 조건에서 자주 쓰이는 원소에 대해 인덱스를 만드는 방식이다.
-- 특정 조건에만 적용되는 부분 인덱스
CREATE INDEX idx_articles_tags_recent ON articles USING GIN (tags) WHERE created_at > now() - interval '30 days';
-- 배열의 특정 위치를 기준으로 표현식 인덱스 생성(예: 첫번째 태그)
CREATE INDEX idx_articles_tag0 ON articles (((tags[1])));
3. 정규화와 하이브리드 설계
검색 복잡도가 높거나 집계가 자주 요구되는 경우 정규화가 더 유리하다. 배열을 유지하되 검색용으로 별도 매핑 테이블을 둔 하이브리드 방식도 고려된다. 이렇게 하면 쓰기와 읽기 성능을 균형 있게 맞출 수 있다.
-- 매핑 테이블 예시
CREATE TABLE article_tags (
article_id int REFERENCES articles(id),
tag text,
PRIMARY KEY (article_id, tag)
);
-- 태그 검색 시 매핑 테이블을 조인하여 인덱스 활용
CREATE INDEX idx_article_tags_tag ON article_tags (tag);
4. 쿼리 작성 방식의 영향
같은 결과라도 연산자 선택에 따라 인덱스 사용 여부가 달라진다. 가능한 경우 @> 또는 && 같은 배열 연산자를 사용하는 것이 좋다. unnest()로 값들을 풀어 처리하면 인덱스 이점을 잃는 경우가 많다.
-- 인덱스 활용 가능성이 높은 쿼리
SELECT * FROM articles WHERE tags && ARRAY['search'];
-- unnest 사용 예시는 풀 스캔을 유발할 수 있음
SELECT a.* FROM articles a JOIN LATERAL unnest(a.tags) AS t(tag) ON t.tag = 'search';
성능 점검과 튜닝 체크리스트
- 실제 쿼리 빈도와 패턴 분석을 우선 수행
- 읽기 중심이면 GIN 인덱스 적용 고려
- 쓰기 비용이 크면 부분 인덱스나 정규화 검토
- EXPLAIN ANALYZE로 인덱스 사용 여부 확인
- 대량 업데이트 시 인덱스 유지 비용과 VACUUM 영향 고려
마무리
배열 타입은 설계를 단순화하지만 검색 성능을 해칠 수 있다. 먼저 검색 패턴을 확인한 뒤 GIN 인덱스, 부분 인덱스, 표현식 인덱스나 정규화 중 적절한 전략을 선택하면 성능 개선이 가능하다. 쿼리별로 EXPLAIN을 통해 실제 플랜을 확인하는 과정이 최종 판단에 필수적이다.