Представление отсутствия даты закрытия в БД и производительность
Нередко при проектировании базы данных нужно описывать срок действия каких-либо сущностей. Почти всегда встаёт вопрос, как представлять сущность срок окончания действия, которой ещё неизвестен.
Четыре простых варианта:
- Два поля - дата открытия и дата закрытия. Если дата закрытия неизвестна ставим null.
- Два поля - дата открытия и дата закрытия. Если дата закрытия неизвестна ставим заранее оговоренную большая дата.
- Одно поле - только дата открытия. Всегда заполнена.
- Одно поле - только дата закрытия. Если дата закрытия неизвестна ставим null.
Первый вариант семантически правильней, но неудобен при построении запросов, может привести к проблемам с оптимизацией.
Второй вариант семантически уродливей, но удобней и вроде как производительней.
Третий и четвертый варианты самые удобные и производительные, но не позволяют фиксировать перерывы между действиями сущностей.
Я чаще пользовался вторым.
create table Presidents
(
FullName Varchar2(200 CHAR) NOT NULL ENABLE,
StartDate Date NOT NULL ENABLE,
EndDate Date NOT NULL ENABLE
);
INSERT INTO Presidents (FullName, StartDate, EndDate)
VALUES ('Ельцин', to_date('dd.mm.yyyy','10.07.1991'),
to_date('dd.mm.yyyy','31.12.1999'));
INSERT INTO Presidents (FullName, StartDate, EndDate)
VALUES ('Путин', to_date('dd.mm.yyyy','07.05.2000'),
to_date('dd.mm.yyyy','07.05.2008'));
INSERT INTO Presidents (FullName, StartDate, EndDate)
VALUES ('Медведев', to_date('dd.mm.yyyy','07.05.2008'),
to_date('dd.mm.yyyy','07.05.2012'));
INSERT INTO Presidents (FullName, StartDate, EndDate)
VALUES ('Путин', to_date('dd.mm.yyyy','07.05.2012'),
to_date('dd.mm.yyyy','31.12.2900'));
И вот я наткнулся на информацию о том, что использование слишком большой даты, как даты закрытия может навредить производительности.
Из https://stackoverflow.com/questions/687510/how-do-you-obtain-the-maximum-possible-date-in-oracle
Вопрос
Есть в Оракле встроенная функция, которая возвращает максимально возможную дату, и которую можно использовать для вставки в поле?
Комментарий
Зачем? Если вы начнёте вставлять заглушку 'большая-дата' в таблицу, это может 'сбить с толку' оптимизатор, который будет полагать, что у вас есть весь диапазон значений даты на следующие 8 тысяч лет
Насколько верен этот комментарий 2009 года и актуален в наши дни?
Ответы (2 шт):
Для задачи как в вопросе - только у последнего президента дата окончания срока полномочий неизвестна, можно завершающую дату опрделить как NULL (=неизвестно) вместо большой даты.
На этапе проектирования не следует задумываться об оптимизации каких-то конкретных SQL запросов. На этом этапе главное соответствие модели данных, ограничения значений (доменов), бизнес задаче.
Попробуем так: уникальный индекс на начало и маленький функциональный индекс на завершающую дату. В итоге, подготовим данные:
create table ranges (
id number primary key, startd date not null, endd date
);
create unique index ix_ranges on ranges (startd,endd);
create index ix_ranges_e on ranges (nvl2(endd,null,1));
insert into ranges
select rownum, date'2020-01-01'+(rownum-1),
case level when 10e3 then null else date'2020-01-01'+rownum end
from dual connect by level <= 10e3;
exec dbms_stats.gather_table_stats(user, 'ranges', cascade=>true)
Самые распостранённые запросы выполняются наиболее оптимально.
Какой временной период сответствует заданной дате:
SQL> set autotrace on
SQL> select *
from ranges where sysdate between startd and endd;
Execution Plan
----------------------------------------------------------
Plan hash value: 3894254438
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 135 | 2700 | 3 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| RANGES | 135 | 2700 | 3 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IX_RANGES | 135 | | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ENDD">=SYSDATE@! AND "STARTD"<=SYSDATE@!)
filter("ENDD">=SYSDATE@!)
Или ещё не закрыт (текущий):
SQL> select *
from ranges where nvl2(endd,null,1) = 1;
Execution Plan
----------------------------------------------------------
Plan hash value: 2817486554
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 21 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| RANGES | 1 | 21 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | FI_RANGES_END | 1 | | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access(NVL2("ENDD",NULL,1)=1)
Начиная с версии 12c появилась возможность определять временной интервал в записи как временная действенность (Тemporal validity)
Temporal Validity Support lets you associate one or more valid time dimensions with a table and have data be visible depending on its time-based validity, as determined by the start and end dates or time stamps of the period for which a given record is considered valid.
Эта семантика определяет столбцы начала и конца временного интервала как действенный, если время конца всегда больше начала или возможно NULL, и действенность этого временного интервала включает время начала, но исключает время конца. Подробнее см. искать по period_definition.
Для данных как в ответе столбец (скрытый) временной действенности можно добавить так:
alter table ranges add (period for validity (startd, endd));
Тогда, запросы будут выглядеть так:
select *
from ranges as of period for validity date'2047-05-21';
ID STARTD ENDD
---------- ------------------- -----------------------------
10000 2047-05-18 00:00:00
select
from ranges versions period for validity between sysdate and sysdate+1;
ID STARTD ENDD
---------- ------------------- -------------------
138 2020-05-17 00:00:00 2020-05-18 00:00:00
139 2020-05-18 00:00:00 2020-05-19 00:00:00
Пока это новшество никаких преимушеств в производительности не обещает, это только часть преобразования запроса (т.н. Query Transformation). Например, первый запрос выше до парсинга выглядит так:
Final query after transformations:******* UNPARSED QUERY IS *******
SELECT "T"."ID" "ID","T"."STARTD" "STARTD","T"."ENDD" "ENDD"
FROM "DB"."RANGES" "T"
WHERE "T"."STARTD"<=TO_DATE(' 2047-05-21 00:00:00', 'syyyy-mm-dd hh24:mi:ss')
AND ("T"."ENDD" IS NULL
OR "T"."ENDD">TO_DATE(' 2047-05-21 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
Но можно надеятся, что, возможно, для него будут произведены улучшения производительности в будущих версиях.
Например, после введения долгожданного SQL Assertions, и установки ограничения на непересекающиеся временные интервалы, оптимизатор найдёт более лучший способ найти запись, чем не самый оптимальный INDEX RANGE SCAN по одному столбцу.