PostgreSQL · 2026-04-08

PostgreSQL 범위 타입(range) 활용과 인덱싱

PostgreSQL range 타입의 기본 개념과 int4range·tsrange·daterange 예제, 주요 연산자와 GiST·SP-GiST·BRIN 인덱스 구성 및 성능 고려사항을 정리한 내용

작성일 : 2026-04-08 ㆍ 작성자 : 관리자
post
목차

개요: 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 측면에서 큰 이점을 얻을 수 있다. 도입 시 데이터 분포와 쓰기 패턴, 동시성 요구사항을 고려해 설계하는 것이 중요하다.

range 타입 postgres 사용 postgres range 인덱스 daterange 성능 postgres PostgreSQL range int4range tsrange GiST BRIN