Представление отсутствия даты закрытия в БД и производительность

Нередко при проектировании базы данных нужно описывать срок действия каких-либо сущностей. Почти всегда встаёт вопрос, как представлять сущность срок окончания действия, которой ещё неизвестен.

Четыре простых варианта:

  1. Два поля - дата открытия и дата закрытия. Если дата закрытия неизвестна ставим null.
  2. Два поля - дата открытия и дата закрытия. Если дата закрытия неизвестна ставим заранее оговоренную большая дата.
  3. Одно поле - только дата открытия. Всегда заполнена.
  4. Одно поле - только дата закрытия. Если дата закрытия неизвестна ставим 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 шт):

Автор решения: 0xdb

Для задачи как в вопросе - только у последнего президента дата окончания срока полномочий неизвестна, можно завершающую дату опрделить как 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)
→ Ссылка
Автор решения: 0xdb

Начиная с версии 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 по одному столбцу.

→ Ссылка