postgres data type range - ghdrako/doc_snipets GitHub Wiki

Range types are a unique feature of PostgreSQL, managing two dimensions of data in a single column, and allowing advanced processing.range types come with implicit quality checks. Range types:

  • Time Ranges: You can use range data types to store time ranges, such as business hours, meeting times, or shifts.
  • Numeric Ranges: You can use range data types to store ranges of numeric values, such as temperature, age, or income ranges.
  • Geographic Ranges: You can use range data types to store geographic ranges, such as latitude and longitude ranges, or ranges of distances.
  • Text Ranges: You can use range data types to store ranges of text values, such as character or string ranges.

The main example is the daterange data type, which stores as a single value a lower and an upper bound of the range as a single value. This allows PostgreSQL to implement a concurrent safe check against overlapping ranges.

create table rates 
 ( 
 currency text, 
 validity daterange, 
 rate numeric, 
  exclude using gist (currency with =, validity with &&) 
 );

insert into rates(currency, validity, rate) 
 select currency, 
 daterange(date, 
 lead(date) over(partition by currency 
 order by date), 
 '[)' 
 ) 
 as validity, 
 rate 
 from raw.rates 
 order by date;

The ratę table registers the rate value for a currency and a validity period, and uses an exclusion constraint that guarantees non-overlapping validity period for any given currency:

1 exclude using gist (currency with =, validity with &&) 

This expression reads: exclude any tuple where the currency is=to an existing currency in our table And where the validityis overlapping with (&&) any existing validity in our table. This exclusion constraint is implemented in PostgreSQL using a GiST index.

By default,GiSTin PostgreSQL doesn’t support one-dimensional data types that are meant to be covered byB-treeindexes. With exclusion constraints though, it’s very interesting to extendGiSTsupport for one-dimensional data types, and so we install thebtree_gistextension, provided in PostgreSQL contrib package.

select rate 2 from rates 3 where currency = 'Euro' 4 and validity @> date '2017-05-18';

The operator @>reads contains, and PostgreSQL uses the exclusion constraint’s index to solve that query efficiently:

CREATE TABLE t_price_range (
    id              serial,
    product_name    text,
    price           numeric,
    price_range     daterange
);

Range formed

SELECT int4range(10, 20); -- 10 is included in the range, while 20 is not.

SELECT '[10, 19]'::int4range, '[10,20)'::int4range; -- different parantrsis but result the same
 int4range | int4range
-----------+-----------
 [10,20)   | [10,20)
 

SELECT daterange('2025-10-04', '2027-05-01');
INSERT INTO t_price_range (product_name, price, price_range)
    VALUES ('Apple', 1.5, '[2022-01-01, 2022-03-03]');

Querying ranges

SELECT 17 <@ '[10, 19]'::int4range;
Operator Description Example Result
= equal int4range(1,5) = '[1,4]'::int4range t
<> not equal numrange(1.1,2.2) <> numrange(1.1,2.3) t
< less than int4range(1,10) < int4range(2,3) t
> greater than int4range(1,10) > int4range(1,5) t
<= less than or equal numrange(1.1,2.2) <= numrange(1.1,2.2) t
>= greater than or equal numrange(1.1,2.2) >= numrange(1.1,2.0) t
@> contains range int4range(2,4) @> int4range(2,3) t
@> contains element '[2011-01-01,2011-03-01)'::tsrange @> '2011-01-10'::timestamp t
<@ range is contained by int4range(2,4) <@ int4range(1,7) t
< element is contained by 42 <@ int4range(1,7) f
&& overlap (have points in common) int8range(3,7) && int8range(4,12) t
<< strictly left of int8range(1,10) << int8range(100,110) t
>> strictly right of int8range(50,60) >> int8range(20,30) t

Multirange

A multirange consists of one or more ranges packed together in a single column.

test=# SELECT int4multirange('{(10, 20), (30, 40)}');
  int4multirange
-------------------
 {[11,20),[31,40)}
(1 row)
test=# SELECT 33 <@ int4multirange('{(10, 20), (30, 40)}');
 ?column?
----------
 t
(1 row)
test=# SELECT 25 <@ int4multirange('{(10, 20), (30, 40)}');
 ?column?
----------
 f
(1 row)

The contains operator (<@) works normally. We can also see that those ranges are simply passed to PostgreSQL as an ordinary array. Of course, we can also check whether ranges overlap with multiranges:

test=# SELECT int4multirange('{(10, 20), (30, 40)}')
    && int4range(18, 32);
 ?column?
----------
 t
(1 row)
test=# SELECT int4multirange('{(10, 20), (15, 30)}');
 int4multirange
----------------
 {[11,30)}

The database engine has figured out that those ranges are actually one. It folded those ranges into one big range.