PostgreSQL 범위 타입(range) 활용과 인덱싱
PostgreSQL range 타입의 기본 개념과 int4range·tsrange·daterange 예제, 주요 연산자와 GiST·SP-GiST·BRIN 인덱스 구성 및 성능 고려사항을 정리한 내용
목차
개요: range 타입이란
PostgreSQL의 range 타입은 값의 연속 구간을 하나의 데이터형으로 표현한다. 숫자 범위(int4range, numrange), 날짜/시간 범위(daterange, tsrange, tstzrange) 등을 지원한다. 범위를 기본 단위로 저장하면 기간 검색, 충돌 확인(overlap), 포함 관계 검사 등을 간결하고 효율적으로 처리할 수 있다.
범위 타입의 기본 사용법
데이터 정의와 예제
테이블에 범위 타입을 사용해 예약, 재고 기간, 가격 밴드 등을 모델링할 수 있다. 아래는 예약 테이블 샘플이다.
CREATE TABLE room_reservation (
id serial PRIMARY KEY,
room_id int NOT NULL,
period daterange NOT NULL
);
INSERT INTO room_reservation (room_id, period) VALUES
(1, '[2026-04-01,2026-04-05)'),
(1, '[2026-04-05,2026-04-10)'),
(2, '[2026-04-03,2026-04-06)');
범위 리터럴은 대괄호와 소괄호로 포함/비포함을 명시한다. '[a,b)'는 a 포함, b 불포함을 뜻한다.
주요 연산자와 함수
범위 타입에는 전용 연산자와 함수가 있다. 대표적으로:
&&: 두 범위의 겹침(overlap) 검사@>: 왼쪽 범위가 오른쪽 범위를 포함(contains)<@: 왼쪽 범위가 오른쪽에 포함됨~: 비어 있는 범위(is empty) 확인(함수isempty()권장)lower(),upper(): 범위의 하한과 상한 반환
연산자 예제
예약 충돌 확인, 특정 날짜 포함 여부 등은 간단한 쿼리로 처리된다.
-- 특정 날짜에 예약된 방 찾기
SELECT * FROM room_reservation
WHERE period @> '2026-04-04'::date;
-- 예약 충돌(겹침) 검사: 새로운 예약이 기존 예약과 겹치는지
SELECT * FROM room_reservation
WHERE room_id = 1
AND period && '[2026-04-04,2026-04-08)';
-- 범위의 하한 상한 조회
SELECT id, lower(period), upper(period) FROM room_reservation;
인덱싱 전략과 성능
범위 검색에서 인덱스는 성능에 큰 영향을 준다. PostgreSQL은 GiST와 SP-GiST를 통해 범위 연산에 대한 인덱스를 지원하며, 시계열적 연속 데이터에는 BRIN이 유용할 수 있다.
GiST 인덱스
GiST는 범위의 겹침(&&)과 포함(@>, <@) 같은 연산자에 최적화되어 있다. 일반적으로 가장 많이 쓰이는 선택지다.
CREATE INDEX idx_room_period_gist ON room_reservation USING GIST (period);
GiST 인덱스는 범위의 공간적 관계를 트리 구조로 관리해 겹침 검사를 빠르게 처리한다.
SP-GiST와 BRIN
SP-GiST는 일부 워크로드에서 더 나은 분포를 보이며, 특히 한 컬럼에 많은 부분이 비어 있거나 고르게 분포되지 않은 경우에 고려된다. BRIN은 디스크 상의 물리적 순서와 데이터의 정렬성이 높은 시계열(예: 연속된 날짜 범위)을 다룰 때 아주 적은 공간으로 좋은 성능을 낸다.
-- BRIN은 데이터가 날짜 순으로 적재되는 경우 유리
CREATE INDEX idx_room_period_brin ON room_reservation USING BRIN (lower(period));
인덱스 사용 시 주의점
- 범위 연산자 중 일부는 인덱스를 사용하지 못할 수 있으므로 EXPLAIN로 계획 확인이 필요하다.
- 복잡한 함수나 변환을 쿼리에 포함하면 인덱스 무효화가 발생할 수 있다. 가능한 한 컬럼 그대로 비교한다.
- 데이터 분포와 쿼리 패턴에 따라 GiST, SP-GiST, BRIN 중 적절한 유형 선택이 중요하다.
실무 적용 팁
범위 타입을 도입할 때 고려할 점을 정리한다.
- 범위를 표준화: 포맷(포함/비포함)을 팀 규칙으로 정하면 혼란을 줄인다.
- 빈 범위와 널 값 처리: 빈 범위(empty)와 NULL은 다르므로 의도에 맞게 설계한다.
- 정규화 여부 판단: 범위를 분해해 별도 시작/종료 칼럼으로 관리하는 방식과 장단점을 비교한다.
- 인덱스 유지 비용: 쓰기 비용이 증가하므로 빈번한 대량 삽입이 있다면 배치 전략을 고려한다.
예제: 예약 충돌 체크용 트리거(간단한 패턴)
-- 트리거는 동시성 문제를 완전히 해결하지 못할 수 있으므로
-- 필요한 경우 행 수준 잠금 또는 다른 동시성 제어를 함께 사용
CREATE FUNCTION check_reservation_conflict() RETURNS trigger AS $$
BEGIN
IF EXISTS (
SELECT 1 FROM room_reservation
WHERE room_id = NEW.room_id
AND period && NEW.period
AND id COALESCE(NEW.id, 0)
) THEN
RAISE EXCEPTION 'Reservation conflict';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_reservation_conflict
BEFORE INSERT OR UPDATE ON room_reservation
FOR EACH ROW EXECUTE FUNCTION check_reservation_conflict();
마무리
range 타입은 기간과 구간 문제를 표현할 때 코드와 쿼리를 단순화한다. 적절한 인덱스(GiST, SP-GiST, BRIN)를 선택하고 쿼리 패턴에 맞춰 구성하면 daterange 성능 postgres 측면에서 큰 이점을 얻을 수 있다. 도입 시 데이터 분포와 쓰기 패턴, 동시성 요구사항을 고려해 설계하는 것이 중요하다.