Skip to content

Диапазонные типы данных

Диапазонные типы представляют диапазоны значений некоторого типа данных (он также называется подтипом диапазона). Например, диапазон типа timestamp может представлять временной интервал, когда зарезервирован зал заседаний. В данном случае типом данных будет tsrange (сокращение от «timestamp range»), а подтипом — timestamp. Подтип должен быть полностью упорядочиваемым, чтобы можно было однозначно определить, где находится значение по отношению к диапазону: внутри, до или после него.

Диапазонные типы полезны тем, что позволяют представить множество возможных значений в одной структуре данных и чётко выразить такие понятия, как пересечение диапазонов. Наиболее очевидный вариант их использования — применять диапазоны даты и времени для составления расписания, но также полезными могут оказаться диапазоны цен, интервалы измерений и т. д.

ВАЖНО: Один диапазон представляет единый непрерывный интервал без внутренних разрывов. Для хранения нескольких разрывных интервалов используются мультидиапазонные типы (PostgreSQL 14+).

Встроенные диапазонные типы

PostgreSQL предоставляет несколько встроенных диапазонных типов:

sql
-- Целочисленные диапазоны
int4range — диапазон integer
int8range — диапазон bigint

-- Числовые диапазоны с плавающей точкой
numrange — диапазон numeric

-- Временные диапазоны
tsrange — диапазон timestamp без временной зоны
tstzrange — диапазон timestamp с временной зоной
daterange — диапазон date

Включение и исключение границ

Любой непустой диапазон имеет две границы, верхнюю и нижнюю, и включает все точки между этими значениями. В него также может входить точка, лежащая на границе, если диапазон включает эту границу. И наоборот, если диапазон не включает границу, считается, что точка, лежащая на этой границе, в него не входит.

В текстовой записи диапазона включение нижней границы обозначается символом «[», а исключением — символом «(». Для верхней границы включение обозначается аналогично, символом «]», а исключение — символом «)».

Для проверки, включается ли нижняя или верхняя граница в диапазон, предназначены функции lower_inc и upper_inc, соответственно.

Неограниченные (бесконечные) диапазоны

Нижнюю границу диапазона можно опустить и определить тем самым диапазон, включающий все значения, лежащие ниже верхней границы, например: (,3]. Подобным образом, если не определить верхнюю границу, в диапазон войдут все значения, лежащие выше нижней границы. Если же опущена и нижняя, и верхняя границы, такой диапазон будет включать все возможные значения своего подтипа. Указание отсутствующей границы как включаемой в диапазон автоматически преобразуется в исключающее; например, [,] преобразуется в (,). Можно воспринимать отсутствующие значения как плюс/минус бесконечность, но всё же это особые значения диапазонного типа, которые охватывают и возможные для подтипа значения плюс/минус бесконечность.

Для подтипов, в которых есть понятие «бесконечность», infinity может использоваться в качестве явного значения границы. При этом, например, в диапазон [today,infinity) с подтипом timestamp не будет входит специальное значение infinity данного подтипа, однако это значение будет входить в диапазон [today,infinity], как и в диапазоны [today,) и [today,].

Проверить, определена ли верхняя или нижняя граница, можно с помощью функций lower_inf и upper_inf, соответственно.

Базовое создание диапазонов

sql
-- создание диапазонов различными способами
select '[1,10]'::int4range;                     -- от 1 включительно до 10 включительно
select '(1,10)'::int4range;                     -- от 1 не включительно до 10 не включительно
select '[2023-01-01,2023-12-31]'::daterange;    -- от 2023-01-01 включительно до 2023-12-31 включительно
select '[100,500)'::numrange;                   -- от 100 включительно до 500 не включительно

Создание таблиц с диапазонными типами

sql
-- таблица для проектов
create table projects (
    id serial primary key,
    name varchar(100),
    execution_period tstzrange, --период реализации проекта
    budget_range numrange --диапазон бюджетной стоимости проекта
);

EXCLUDE

Ограничение-исключение, которое позволяет определить условие «непересечения» диапазонов.

sql
-- таблица для бронирования номеров
create table hotel_reservations (
    id serial primary key,
    room_number integer,
    reservation_period daterange, --период резерва номера
    price_range numrange, --диапазон стоимости аренды
    exclude using gist (room_number with =, reservation_period with &&) 
    --проверяем, что один и тот же номер нельзя арендовать при пересечении диапазонов аренды
); 
/*
Так как для типа integer по умолчанию не определен класс операторов GiST, то 
нужно установить расширение btree_gist, которое добавляет поддержку GiST индексов 
для обычных скалярных типов данных:
*/
create extension if not exists btree_gist;

Важно: EXCLUDE особенно важен в многопользовательских системах, так как гарантирует отсутствие пересекающихся диапазонов даже при конкурентных INSERT/UPDATE.

Функции для работы с диапазонами

sql
-- получение границ диапазона
select lower('[1,10)'::int4range), upper('[1,10)'::int4range);
text
lower|upper|
-----+-----+
    1|   10|
sql
-- проверка пустоты
select isempty('(1,1)'::int4range); -- true

-- извлечение границ
select lower_inc('[1,10]'::int4range), upper_inc('[1,10]'::int4range);
text
lower_inc|upper_inc|
---------+---------+
true     |false    |

--тип данных int4range не поддерживает включающую верхнюю границу при обычной записи. 
--если нужно получить 10, как верхнюю границу, то нужно указывать диапазон [1,11)
sql
-- расширение диапазона
select range_merge('[1,5]'::int4range, '[3,10]'::int4range); --[1,11) 

-- разность диапазонов
select '[1,10]'::int4range - '[4,10]'::int4range; -- [1,4)

-- длина диапазона
select numrange(1.5, 10.2),
    upper(numrange(1.5, 10.2)) - lower(numrange(1.5, 10.2)) diff;
text
numrange  |diff    |
----------+--------+
[1.5,10.2)|     8.7|

Операторы сравнения и содержания

sql
@> — содержит ли диапазон элемент или другой диапазон
select '[1,10]'::int4range @> 5; -- true
select '[1,10]'::int4range @> '[2,8]'::int4range; -- true

<@ — содержится ли элемент в диапазоне или диапазон в другом диапазоне
select 5 <@ '[1,10]'::int4range; -- true
select '[2,8]'::int4range <@ '[1,10]'::int4range; -- true

&& — пересекаются ли диапазоны
select '[1,5]'::int4range && '[3,7]'::int4range; -- true

<< — строго левее 
select '[1,5]'::int4range << '[6,10]'::int4range; -- true

>> — строго правее
select '[6,10]'::int4range >> '[1,5]'::int4range; -- true

&< — не расширяется правее правого диапазона 
select '[1,5]'::int4range &< '[5,10]'::int4range; -- true

&> — не расширяется левее левого диапазона 
select '[5,10]'::int4range &> '[1,5]'::int4range; -- true

-|- — примыкает (смежный)
select '[1,5]'::int4range -|- '[5,10]'::int4range; -- true

Создание пользовательских диапазонных типов

sql
-- создаем enum тип для приоритетов
create type priority_level as enum ('very low', 'low', 'medium', 'high', 'very high');

-- создаем диапазонный тип на основе enum
create type priority_range as range (
    subtype = priority_level
);

-- используем в таблице
create table tasks (
    id serial primary key,
    name varchar(100),
    allowed_priority priority_range
);

--внесем несколько строк
insert into tasks (name, allowed_priority) values
    ('обычная задача', '[very low,high]'),
    ('срочная задача', '[medium,very high]');

--получим задачи, которые включают значение high
select *
from tasks
where allowed_priority @> 'high'::priority_level;
text
id|name          |allowed_priority    |
--+--------------+--------------------+
 1|обычная задача|["very low",high]   |
 2|срочная задача|[medium,"very high"]|

Конструкторы диапазонов

Помимо приведения строк (например, '[1,10]'::int4range), PostgreSQL предоставляет функции-конструкторы для создания диапазонных значений. Синтаксис:

sql
диапазонный_тип( нижняя_граница, верхняя_граница [, текст_формат] )

Параметр текст_формат - строка, состоящая из одного или двух символов, определяющих включение/исключение границ: '[]' - обе границы включены '[)' - нижняя включена, верхняя исключена '(]' - нижняя исключена, верхняя включена '()' - обе границы исключены Если параметр формата опущен, по умолчанию подразумевается '[)' (полуоткрытый интервал) для всех типов. Важно: PostgreSQL внутри всегда хранит диапазоны в полуоткрытом виде [), поэтому '[]' нормализуется в '[)'.

Примеры для целочисленных диапазонов:

sql
-- без формата
select int4range(1, 10);          -- [1,10)

-- с форматом
select int4range(1, 10, '[)');    -- [1,10)
select int4range(1, 10, '[]');    -- [1,11)
select int4range(1, 10, '()');    -- [2,10)

-- открытые границы
select int4range(NULL, 10);       -- (,10)
select int4range(1, NULL);        -- [1,)
--NULL означает бесконечность только в семантике диапазона, но не является значением "infinity"

Примеры для дат

sql
select daterange('2024-01-01', '2024-12-31', '[]');   -- [2024-01-01,2025-01-01)
select daterange('2024-01-01', '2024-12-31', '[)');   -- [2024-01-01,2024-12-31)

Примеры для числовых диапазонов

sql
select numrange(1.5, 10.2, '[]');   -- [1.5,10.2]
select numrange(1.5, 10.2, '[)');   -- [1.5,10.2)

В некоторых случаях конструкторы:

  • безопаснее строкового кастинга при динамическом формировании значений
  • удобная работа с открытыми границами через NULL

Заключение

Диапазонные типы данных в PostgreSQL позволяют работать с интервалами значений, что дает возможность:

  • эффективно хранить и обрабатывать временные периоды, числовые диапазоны
  • обеспечивать целостность данных через ограничения исключения
  • создавать специализированные типы для конкретных бизнес-потребностей
  • для диапазонных типов чаще всего используются GiST или SP-GiST индексы.

Независимо от способа создания и реализации, PostgreSQL гарантирует единое поведение операций над диапазонами за счёт нормализации границ.

Освоение диапазонных типов открывает новые возможности для создания надежных и эффективных приложений, работающих с интервальными данными.

При дальнейшем углублении в SQL вы можете создавать собственные диапазонные типы с применением процедурных языков и моделировать диапазоны с прерываниями.