На третьем курсе в СПБПУ Петра Великого у меня был экзамен по SQL в БД Oracle. Экзамен состоял из трех задач. Этот экзамен был одним из самых сложных за все четыре года обучения в университете. Дается три задачи на несколько часов. Если решил все три задачи, то получаешь оценку 5, один недочет (например, лишний пробел) - 4, одна задача решена неверна - 3, два недочета - 3.

Сегодня я хочу рассказать об одной из задач, которая была в этом экзамене.

Что-то пошло не так...

Экзамен этот я завалил. Смог я его сдать только со второй попытки. Это было очень неожиданно для меня, а для моих родителей чем-то за гранью, ведь я учился на 4 и 5, и раньше такого не было.

Используя обращение только к таблице DUAL, построить SQL-запрос, возвращающий один столбец, содержащий календарь на заданный месяц заданного года:

  • номер дня в месяце (цифрами),

  • полное название месяца по-английски заглавными буквами (в верхнем регистре),

  • год (четыре цифры),

  • полное название дня недели по-английски строчными буквами (в нижнем регистре).

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

Календарь должен создаваться для любых допустимых значений дат Oracle. Задачу решить без использования разделов Model и рекурсивного With.

Пример вывода результата:

1 MAY 2020 friday

2 MAY 2020 saturday

Задача показалась мне самой простой. Приведу запрос, с помощью которого я решил данную задачу.

with input as (
    select to_date(:dt, 'mm.syyyy') dt
    from dual
)
select to_number(to_char(level + dt - 1, 'dd'),'99') ||
       to_char(level + dt - 1, ' MONTH ', 'nls_date_language=american') ||
       to_char(level + dt - 1, 'syyyy ' , 'nls_date_language=american') ||
       to_char(level + dt - 1, 'day', 'nls_date_language=american') calendar
from dual
     join input on 1 = 1
connect by level + dt - 1 <= last_day(dt);

На первый взгляд все выглядит хорошо, но рассмотрим один интересный момент. Допустимый диапазон для года - это целое число от -4172 до 9999, кроме 0. Возьмем декабрь 1 года до н.э. и январь 1 года н.э. В результате мы увидим, что после среды идет суббота, а значит задача решена неверно.

Пример работы запроса с -0001 и 0001 годами
Пример работы запроса с -0001 и 0001 годами

В григорианском и юлианском календарях отсутствует нулевой год. Также и в Oracle нет нулевого года, а -0001 год Oracle соответствует 2 году до н.э. по традиционной хронологии.

--Результат 01/01/0001
select to_date('31.12.-0001', 'dd.mm.syyyy') + 1
from dual; 
Сопоставление стандарта ISO 8601 и традиционной хронологии
Сопоставление стандарта ISO 8601 и традиционной хронологии

Тогда даты, которые относятся к нашей эре продолжаем создавать, как это делалось в первом запросе. -0002 год Oracle "сдвинем" вправо, то есть он станет 2 годом до н.э. по традиционной хронологии, -0003 Oracle, станет 3 до н.э. и т.д. Но мы не можем также сдвинуть -0001 год Oracle, добавляя дни/месяцы и др. Однако можно найти год, который будет похож на нулевой год. Например, это 420 год н.э. Тогда будем использовать этот год для вывода месяца нулевого года, а при выводе указывать, что это -0001. И тогда можно отойти от понятия нулевого года, ведь остались ..., -2, -1, 1, 2, ... года.

with input as (
    select to_date(:dt, 'mm.syyyy') dt
    from dual
)
select case
        when extract (year from dt) > 0 then 
          to_char(level + dt - 1, 'fm dd ') || 
          to_char(level + dt - 1, 'MONTH ', 'nls_date_language=american') ||
          to_char(level + dt - 1, 'syyyy ', 'nls_date_language=american') ||
          to_char(level + dt - 1, 'day', 'nls_date_language=american')
        when extract (year from dt) = -1 then
          to_char(level + to_date(regexp_replace(to_char(dt, 'mm.syyyy'), '-0001', '0420'), 'mm.syyyy') - 1, 'fm dd ') ||
          to_char(level + to_date(regexp_replace(to_char(dt, 'mm.syyyy'), '-0001', '0420'), 'mm.syyyy') - 1, 'MONTH ', 'nls_date_language=american') ||
          to_char(level + dt - 1, 'syyyy ', 'nls_date_language=american') ||
          to_char(level + to_date(regexp_replace(to_char(dt,'mm.syyyy'), '-0001', '0420'),'mm.syyyy') - 1, 'day', 'nls_date_language=american')
        else
          to_char(level + add_months(dt, 12) - 1, 'fm dd ') ||
          to_char(level + add_months(dt, 12) - 1, 'MONTH ', 'nls_date_language=american') ||
          to_char(level + dt - 1, 'syyyy ', 'nls_date_language=american') ||
          to_char(level + add_months(dt, 12) - 1, 'day', 'nls_date_language=american')
        end calendar
from dual
     join input on 1 = 1
connect by level + dt - 1 <= last_day(dt);

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

Комментарии (13)


  1. ITMatika
    17.11.2021 13:41
    +8

    В Oracle достаточно много "особенностей" при работе с датами. Все эти add_months, interval и прочие имеют каждый свои грабли и область применения. Я не считаю верным строить экзамен, как преодоление полосы препятствий через ряд неочевидных грабель в дефиците времени.


    1. rat1
      17.11.2021 14:47

      Я подозреваю обо всех граблях рассказывали на лекциях и проблема во внимательности, а не в поиске граблей.


      1. ITMatika
        17.11.2021 16:37
        +2

        Я подозреваю, что вы не в теме количества граблей в Oracle. И с каждой версией появляются новые. Лучше бы на лекциях про полезные вещи рассказывали, а не про очередные грабли :-)


        1. rat1
          17.11.2021 21:47
          -1

          Не понимаю как связаны все грабли в Oracle и грабли для задания к экзамену, которые скорее всего проработали на лекциях?


          1. IT4R
            18.11.2021 22:48

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

            Здесь от личного отношения препода зависит, может завалить такой проверкой, а может и нет... К сожалению, это задание лишь одно из многих, на которых можно отлично научиться писать костыльные запросы.


            1. rat1
              19.11.2021 11:13

              Тогда извиняюсь, ошибся. Это было лишь предположение - обычно люди всех остальных винят в своих проблемах, только не себя)


  1. Paskin
    17.11.2021 13:48
    +8

    Я бы подумал - стоит ли продолжать учиться в университете, где на экзамене спрашивают такую хрень, да еще и зависимую от производителя БД. При этом, думаю, реальные проблемы вроде нормализации БД под разные задачи, сегментирования, выбора типа индекса, оценки и оптимизации запросов - скорее всего оказались обойдены стороной.


  1. Z55
    17.11.2021 13:53
    +4

    Отличный пример того, каким не должно быть задание.
    Если есть подобная проблема (?), об этом должно было быть указано в задании.
    Студент не обязан знать, что случается в начале эпохи с современной СУБД. Но если бы этот аспект был указан в задании, то всё встало бы на свои места.


  1. lexore
    17.11.2021 14:49
    +2

    Мне кажется, проблема в позиционировании задачи. То есть, она про шашечки или ехать?

    Сожно спросить: "Зачем в принципе смотреть даты раньше X (или даже XIX) века?". Ответы в стиле "должно работать с любой датой" - это утопия. Этот путь приведет туда, куда пришли вы - вставка в запрос костылей для особенностей учета времени в стародавние времена. Нулевой год - это только цветочки. Например, нужно учитывать, в каком году страна перешла на григорианский календарь (страны мира переходили на него более более 400 лет). А для более ранних дат введены специальные календари. И там другие правила учета високосного года.

    Ответы в стиле "интересно" или "почему нет?" - это более понятно. В этом случае можно не вставлять костыли. Можно просто сказать "Запрос отлично справляется с датами от такого-то года. А до этого года вылезают разные баги, вызванные историческими причинами."


    1. Mingun
      17.11.2021 22:30

      Зачем учитывать момент перехода в григорианский календарь, если только не нужно вывести NULL для дат до того момента, как этот календарь был введен? А так-то его можно экстраполировать в прошлое. Правда полученные даты будут полностью искусственными — в исторических документах такие не встретишь.


      1. lexore
        17.11.2021 22:56
        +1

        Я на самом деле про то же - тут настоящее раздолье для костылей на любой лад :-)


  1. sergarcada
    17.11.2021 17:01
    +1

    Интересно, приведенное решение учитывает, что 1896 високосный, а 1900 - нет?

    По этой же причине интересно узнать о выборе 420 года.


  1. Oraclist
    18.11.2021 17:21

    Это ещё перевода на зимнее/летнее время не было в задаче! </sarcasm>

    Часто встречал, что нижнюю границу задают явно 01/01/1900, верхнюю - 01/01/4000.

    Как-то поймали "магию" при обработке дат/времени 01/01/1970 на Java (учет часовых поясов и перевода на летнее/зимнее время) и сменили нижнюю границу на 01/01/1970.