PostgreSQL 범위 타입 — daterange, tstzrange와 겹침 방지

PostgreSQL 범위 타입(int4range, daterange, tstzrange 등)의 경계 기호, @>, <@, &&, *, + 연산자, EXCLUDE USING GIST로 예약 시스템 겹침 방지 제약을 구현하는 실무 패턴을 정리합니다.

· 6 min read · PALDYN Team

지난 글에서 JSONB GIN 인덱스의 동작 원리를 살펴봤다. 이번에는 PostgreSQL의 또 다른 고유 타입인 **범위 타입(Range Type)**을 다룬다. 예약 시스템, 기간 할인, 유효 기간 조건 등 “시작~끝”을 표현하는 모든 시나리오에서 강력하다.

범위 타입 종류

타입기반 타입예시
int4rangeinteger[1,10)
int8rangebigint[1000000,9999999]
numrangenumeric[1.5,3.7]
daterangedate[2026-01-01,2026-12-31]
tsrangetimestamp[2026-01-01 00:00, 2026-01-02 00:00)
tstzrangetimestamptz[2026-01-01 00:00+09, 2026-01-02 00:00+09)

PostgreSQL 14+에서는 multirange 타입도 도입됐다(int4multirange, datemultirange 등).

경계 기호

-- [ ] : 포함(닫힘), ( ) : 제외(열림)
'[3, 9)'::int4range  -- 3 이상 9 미만
'(3, 9]'::int4range  -- 3 초과 9 이하
'[3, 9]'::int4range  -- 3 이상 9 이하
'(,9]'::int4range    -- 음의 무한대 ~ 9 이하
'[3,)'::int4range    -- 3 이상 양의 무한대

-- 빈 범위
'empty'::int4range

-- 생성 함수
SELECT int4range(3, 9, '[)');  -- [3,9)
SELECT daterange('2026-01-01', '2026-12-31', '[]');

daterange는 이산(discrete) 범위이므로 [2026-01-01,2026-12-31][2026-01-01,2027-01-01)은 동일하게 정규화된다.

범위 타입 연산자 시각화

범위 연산자

연산자의미
@>왼쪽이 오른쪽(범위 또는 값)을 포함
<@왼쪽이 오른쪽에 포함됨
&&두 범위가 겹침
<<왼쪽이 오른쪽 앞에 완전히 위치
>>왼쪽이 오른쪽 뒤에 완전히 위치
`--`
+합집합 (겹치거나 인접해야 함)
*교집합
-차집합
-- 현재 시점이 범위 안에 있는지
SELECT '[2026-01-01, 2026-12-31]'::daterange @> CURRENT_DATE;

-- 두 기간이 겹치는지
SELECT '[2026-06-01, 2026-06-10)'::daterange
    && '[2026-06-05, 2026-06-15)'::daterange;  -- true

-- 교집합
SELECT '[2026-06-01, 2026-06-10)'::daterange
    * '[2026-06-05, 2026-06-15)'::daterange;  -- [2026-06-05,2026-06-10)

범위 함수

SELECT lower('[3,9)'::int4range);         -- 3
SELECT upper('[3,9)'::int4range);         -- 9
SELECT lower_inc('[3,9)'::int4range);     -- true (하한 포함)
SELECT upper_inc('[3,9)'::int4range);     -- false (상한 제외)
SELECT lower_inf('(,9]'::int4range);     -- true (무한대)
SELECT isempty('empty'::int4range);      -- true
SELECT range_length('[3,9)'::int4range); -- 6

EXCLUDE 제약 — 겹침 방지의 핵심

CREATE TABLE reservation (
    id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    room_id integer NOT NULL,
    user_id bigint  NOT NULL,
    period  tstzrange NOT NULL,
    EXCLUDE USING GIST (
        room_id WITH =,   -- 같은 방이면서
        period  WITH &&   -- 기간이 겹치면 → 오류
    )
);

EXCLUDE USING GIST는 PostgreSQL 고유 기능이다. “조건을 만족하는 두 행이 동시에 존재할 수 없다”는 제약을 GIST 인덱스 기반으로 검사한다. 이 한 줄로 애플리케이션 레벨의 중복 예약 체크를 대체한다.

범위 타입 실전 패턴

-- 예약 삽입 (겹치면 오류 발생)
INSERT INTO reservation (room_id, user_id, period)
VALUES (101, 42, '[2026-06-10 14:00+09, 2026-06-12 12:00+09)');

-- 이미 위 예약이 있으면 아래는 실패
INSERT INTO reservation (room_id, user_id, period)
VALUES (101, 99, '[2026-06-11 00:00+09, 2026-06-13 00:00+09)');
-- ERROR: conflicting key value violates exclusion constraint

현재 시점 포함 예약 조회

-- GIST 인덱스 활용 (period @> 조건)
SELECT r.id, r.room_id, r.period
FROM reservation r
WHERE r.period @> now()
  AND r.room_id = 101;

-- 오늘 체크인/체크아웃 대상
SELECT *
FROM reservation
WHERE lower(period)::date = CURRENT_DATE
   OR upper(period)::date = CURRENT_DATE;

날짜 범위 겹침 (daterange 활용)

CREATE TABLE promotion (
    id       serial PRIMARY KEY,
    name     text,
    valid    daterange NOT NULL,
    discount numeric(5,2)
);

CREATE INDEX idx_promo_valid ON promotion USING GIST (valid);

-- 오늘 유효한 프로모션
SELECT * FROM promotion
WHERE valid @> CURRENT_DATE;

-- 특정 기간 동안 유효한 프로모션
SELECT * FROM promotion
WHERE valid && '[2026-06-01, 2026-06-30]'::daterange;

히스토리 테이블 패턴

범위 타입을 활용하면 테이블 레이어의 시간 이력 관리도 간단해진다.

CREATE TABLE price_history (
    product_id  integer,
    price       numeric(10,2),
    valid_during daterange NOT NULL,
    EXCLUDE USING GIST (product_id WITH =, valid_during WITH &&)
);

-- 특정 날짜 시점의 가격 조회
SELECT price
FROM price_history
WHERE product_id = 100
  AND valid_during @> '2026-03-15'::date;

-- 현재 가격
SELECT price
FROM price_history
WHERE product_id = 100
  AND valid_during @> CURRENT_DATE;

사용자 정의 범위 타입

기본 범위 타입이 부족하면 직접 정의할 수 있다.

-- float8range가 없으므로 직접 생성
CREATE TYPE float8range AS RANGE (
    subtype = float8,
    subtype_diff = float8mi
);

SELECT '[1.5, 3.7]'::float8range @> 2.5::float8;  -- true

정리

PostgreSQL 범위 타입은 “기간 데이터”를 두 컬럼(시작, 종료)으로 분리하는 전통적 방식의 강력한 대안이다. && 겹침 연산자와 EXCLUDE USING GIST의 조합은 예약 시스템의 중복 방지 로직을 애플리케이션이 아닌 DB 제약으로 내재화한다. @> 포함 검색은 GIST 인덱스를 활용해 “특정 시점에 활성인 행 찾기”를 효율적으로 처리한다.


지난 글: JSONB GIN 인덱스 — 문서 검색 최적화

다음 글: 사용자 정의 타입과 도메인 — CREATE TYPE, CREATE DOMAIN


읽어주셔서 감사합니다. 😊