В этой статье рассматриваются преимущества такого редко используемого и, на мой взгляд, незаслуженно обойденного вниманием типа данных, как диапазон. Мы сначала спроектируем структуру базы для хранения коэффициента возраст-стаж при расчете стоимости полиса ОСАГО в рамках привычной многим MySQL. Затем перепроектируем под PostgreSQL и посмотрим, как выглядят sql запросы в обоих случаях. И в финале сравним, какие преимущества дает нам использование диапазонов.
Заметка адресована как пользователям MySQL, так и пользователям PostgreSQL, которые не работали с таким типом данных в своей практике. Если в вашей предметной области есть работа с диапазонами величин, то этот пост точно для вас.
Коэффициент страховых тарифов
Для начала немного погрузимся в предметную область. Схема расчета регламентируется документом «Указание Банка России от 19 сентября 2014 г. № 3384-У». При расчете стоимости полиса берется определенная сумма, называемая «Базовый тариф», и умножается на несколько различных коэффициентов, которые могут как повышать конечную стоимость полиса, так и понижать ее. Один из них – коэффициент страховых тарифов в зависимости от возраста и стажа водителя (КВС). Зависимость представлена в таблице:
Здесь мы видим диапазоны чисел по входным критериям (стаж и возраст), что располагает к использованию диапазонного типа данных. Но представим, что у нас в качестве РСУБД идет MySQL и что этот тип нам недоступен. Как будет выглядеть база? Спроектируем!
Структура базы для mysql
Сразу же в голову приходят два варианта реализации:
использование одной таблицы, в строках которой хранятся все варианты связок возраст-стаж-коэффициент;
использование трех таблиц, две из которых хранят диапазоны (одна для возраста, другая для стажа), а третья содержит связь возраст-стаж и значение коэффициента.
Рассмотрим оба этих варианта и заполним таблицы данными. Но для начала зададим граничные условия. Судя по википедии, самая старшая живая жительница планеты Люсиль Рандонлет в 2022 году дожила до 118 (Список старейших людей в мире). Права на управление можно получить с 16 лет. Потенциально водительский стаж Люсиль мог бы составлять 102 года. Для заполнения базы данными будем исходить из расчета: возраст от 16 до 118 лет, стаж вождения от 0 до 102 лет.
Теперь создадим нужные таблицы для обоих вариантов.
Одна таблица
Структура таблицы:
create or replace table kvs
(
age int not null comment 'Возраст',
experience int not null comment 'Стаж',
value decimal(4, 2) not null comment 'Значение КВС',
primary key (age, experience)
) comment 'Коэффициент Возраст-Стаж';
Производим вставку данных. В результате получаем 9314 записи:
Составной индекс по полям age-experience убережет нас от ошибочной вставки дублей при будущих изменениях коэффициента. Запрос на получение коэффициента для водителя 23 лет и стажем вождения 3 года в этом случае будет иметь вид:
SELECT value FROM kvs WHERE age = 22 AND experience = 3
Такой запрос достаточно простой, уникальный индекс не позволит вставить дубли, но из-за большого количества записей работать с такой таблицей может оказаться очень неудобно.
Три таблицы
Структура таблицы:
create table kvs_age_range
(
`id` integer not null AUTO_INCREMENT,
`from` integer not null comment "Начало диапазона",
`to` integer not null comment "Окончание диапазона",
primary key (id)
) comment 'Диапазоны возраста';
create table kvs_experience_range
(
`id` integer not null AUTO_INCREMENT,
`from` integer not null comment "Начало диапазона",
`to` integer not null comment "Окончание диапазона",
primary key (id)
) comment 'Диапазоны стажа';
create table kvs_value
(
age_id int not null,
experience_id int not null,
value decimal(4, 2) null comment 'Значение КВС',
constraint kvs_value_age_id_experience_id_uindex
unique (age_id, experience_id),
constraint kvs_age_id___fk
foreign key (age_id) references kvs_age_range (id)
on delete cascade,
constraint kvs_experience_id___fk
foreign key (experience_id) references kvs_experience_range (id)
) comment 'Величина КВС';
Производим вставку данных:
Количество записей сократилось, но сам запрос получения значения КСВ усложнился:
SELECT value FROM kvs_value AS v
INNER JOIN kvs_age_range AS age ON v.age_id = age.id
INNER JOIN kvs_experience_range AS experience ON v.experience_id = experience.id
WHERE
(age.`from` <= 22 AND age.`to` >= 22)
AND
(experience.`from` <= 3 AND experience.`to` >= 3)
Запрос значительно усложнился, контроля дублей нет (можно вставить пересекающиеся диапазоны), визуально работать с данными сложнее т.к. требуется переключать внимание между таблицами.
Структура базы для PostgreSQL
Для PostgreSQL воспользуемся схемой с одной таблицей:
create table if not exists kvs
(
age int4range not null,
experience int4range not null,
value numeric(4, 2),
exclude using gist (age WITH &&, experience WITH &&)
);
comment on table kvs is 'Коэффициент Возраст-Стаж';
comment on column kvs.age is 'Диапазоны возраста';
comment on column kvs.value is 'Величина КВС';
(О конструкции EXCLUDE USING GIST см. чуть ниже.) Производим вставку данных:
Синтаксис вставки диапазонов:
INSERT INTO kvs VALUES ('[16,21]', '[0,0]', 1.87)
Как видим, в этом случае получается 50 записей (их можно сократить до 20, предлагаю подумать и написать в комментариях как это сделать). Ограничение-исключения EXCLUDE USING GIST убережет нас от ошибочной вставки пересекающихся диапазонов. Например, если мы попытаемся добавить новый коэффициент для возраста 16-30 и стажа 5-6 лет, то получим ошибку:
Запрос на получение коэффициента имеет вид:
SELECT value FROM kvs WHERE age @> 22 AND experience @> 3
Запрос получился простой, схема обеспечивает контроль непересечения вставляемых диапазонов, работать с такой таблицей визуально удобнее ведь она похожа на таблицу полученную нами в ТЗ.
Сравнение вариантов
Как вы уже могли убедиться, при использовании диапазонного типа данных у нас получается минимальное количество строк, простой запрос и контроль «непересечения» диапазонов. Для большей наглядности я сделал сводную таблицу:
Запрос |
Контроль «непересечения» |
Визуальное удобство работы |
|
Одна таблица |
простой |
да |
среднее |
Три таблицы |
сложный |
нет |
сложное |
Таблица с диапазонами |
простой |
да |
простое |
Надеюсь, что вы убедились в силе использования данного механизма и примените его в своей практике. Он может быть использован, например, в системах бронирования номеров или при составлении расписания. Прошу вас поделиться в комментариях своим опытом использования диапазонного типа данных. Для каких кейсов вы использовали диапазоны?
Комментарии (8)
Fafhrd
01.11.2022 12:00+1Последний вариант с постгресом всё-равно требует нормализации.
А так да, постгрес хорош.
GoodGod
01.11.2022 15:02+3Как видим, в этом случае получается 50 записей (их можно сократить до 20, предлагаю подумать и написать в комментариях как это сделать).
Думаю для возраста 16-21 и стажа 0,1,2 коэффициент одинаковый, и их можно объединить в 1 запись. И тогда получится 20 записей.
При варианте с 3 таблицами, похоже никто не мешает вставить повторяющиеся диапазоны и в таблицу возраста и в опыта. Контроля уникальности нет. Тогда этот вариант можно заменить 1 таблицей: id, age_from, age_to, experience_from, experience_to, coefficient.
Akina
01.11.2022 18:15Думаю для возраста 16-21 и стажа 0,1,2 коэффициент одинаковый, и их можно объединить в 1 запись. И тогда получится 20 записей.
Это решение очевидно, думаю, автор имел в виду именно его. Более того, если прибегнуть к экстремальному сжатию диапазонов, то количество записей можно уменьшить до 16.
Но вот правильность такого "сжатия" - крайне сомнительна. Если в следующем году большие дяди решат, что для какой-то ячейки исходной таблицы надо подвинуть коэффициент на сотку, то внесение корректировки потребует минимум двух действий вместо одного (в обоих вариантах реализации), с количеством корректируемых записей от 2 до 5 в зависимости от использованного сжатия.
К тому же при таком сжатии сложнее хранить историю, вернее, работать с прошлыми периодами...
alekciy Автор
02.11.2022 18:04Предложенный вариант с 1 таблицей тоже не помешает вставить повторяющиеся диапазоны, т.к. на уровне РСУБД это будут int + уникальный индекс. Но он позволит создать диапазон 22-22 при существовании диапазона 16-24.
В этом и суть отдельного типа данных range. Кроме цифр (границ диапазона) PostgreSQL еще и знает, что это диапазоны и может контролировать консистентность данных из коробки. Плюс он дает над ними еще и арифметику (которая будет полезна при составлении расписаний).
gnome2_terminal_is_best
Как по мне, это просто синтаксический сахар. Мне ближе вариант с тремя таблицами в MySQL.
alekciy Автор
По сравнению с вариантом 3 таблиц не только. Тут есть контроль целостности, а так же богатый список операторов на пересечения, свободность и прочее. На КСВ может это не так очевидно. Система бронирования в отелях может быть более показательна, но я хотел делать слишком сложный пример.