postgres data type range - ghdrako/doc_snipets GitHub Wiki
- https://www.postgresonline.com/journal/index.php?/archives/401-Multirange-types-in-PostgreSQL-14.html#extended
- https://www.postgresql.org/docs/current/rangetypes.html
- https://www.postgresql.org/docs/current/functions-range.html
Postgres offer OVERLAPS operator who worki on two dates. No need range data typem. https://hakibenita.com/postgresql-unknown-features#find-overlapping-ranges
Range
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.
Many different data types can be ranges: numbers, dates, timestamps, and the like. Every representation of a number (and dates are just strange numbers) that can be sorted can also be a range.
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 validity is overlapping with (&&
) any existing validity in our table. This exclusion constraint is implemented in PostgreSQL using a GiST index.
Formed range type
SELECT int4range(10, 20);
int4range
----------
[10,20)
(1 row)
SELECT daterange('2025-10-04', '2027-05-01');
daterange
------------------------
[2025-10-04,2027-05-01)
(1 row)
SELECT '[10, 19]'::int4range, '[10,20)'::int4range;
int4range | int4range
-----------+----------
[10,20) | [10,20)
(1 row)
Example 1
CREATE TABLE bookings (
room_number int,
reservation tstzrange,
EXCLUDE USING gist (room_number WITH =, reservation WITH &&)
);
INSERT INTO meeting_rooms (
room_number, reservation
) VALUES (
5, '[2022-08-20 16:00:00+00,2022-08-20 17:30:00+00]',
5, '[2022-08-20 17:30:00+00,2022-08-20 19:00:00+00]',
);
Example 2
CREATE TABLE t_price_range (
id serial,
product_name text,
price numeric,
price_range daterange
);
INSERT INTO t_price_range (product_name, price, price_range) VALUES ('Apple', 1.5, '[2022-01-01, 2022-03-03]');
INSERT INTO t_price_range (product_name, price, price_range) VALUES ('Peach', 2.67, '[2055-01-01, 2022-03-03]');
ERROR: range lower bound must be less than or equal to
range upper bound
LINE 1: ..._name, price, price_range)
VALUES ('Peach', 2.67, '[2055-01-...
Range types come with implicit quality checks. PostgreSQL ensures that the boundaries make sense. Otherwise, an error is issued
Preventing e.g. multiple concurrent reservations for a meeting room - work can be offloaded to the database with an exclusion constraint that will prevent any overlapping ranges for the same room number.
By default, GiST in PostgreSQL doesn’t support one-dimensional data types that are meant to be covered byB-tree indexes. With exclusion constraints though, it’s very interesting to extend GiST support for one-dimensional data types, and so we install the btree_gist
extension, 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
PostgreSQL provides a rich set of operators, allowing us to quickly get a handle on the range data.
One of the most commonly used operators is the contains operator:
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 |
&< |
does not extend to the right of | int8range(1,20) &< int8range(18,20) |
t |
&> |
does not extend to the left of | int8range(7,20) &> int8range(5,10) | t |
`- | -` | is adjacent to | `numrange(1.1,2.2) - |
+ |
union | `numrange(5,15) + numrange(10,20) | [5,20) |
* |
intersection | int8range(5,15) * int8range(10,20) |
[10,15) |
- |
difference | int8range(5,15) - int8range(10,20) |
[5,10) |
Multirange (Postgres 14+)
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.
Usage
The main problem is that is Postgres specyfic feature and is not potrable to other database. To hide this you can use views:
CREATE VIEW v AS
SELECT
id,
product_name,
price,
lower(price_range),
upper(price_range)
FROM t_price_range;
CREATE VIEW
test=# \x
Expanded display is on.
test=# SELECT * FROM v;
-[ RECORD 1 ]+----------
id | 1
product_name | Apple
price | 1.5
lower | 2022-01-01
upper | 2022-03-04