Подтвердить что ты не робот

Как вы делаете математику, которая игнорирует год?

Я пытаюсь выбрать даты с юбилеем в течение следующих 14 дней. Как я могу выбрать на основе дат, исключая год? Я пробовал что-то вроде следующего.

SELECT * FROM events
WHERE EXTRACT(month FROM "date") = 3
AND EXTRACT(day FROM "date") < EXTRACT(day FROM "date") + 14

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

SELECT * FROM events
WHERE (date > '2013-03-01' AND date < '2013-04-01')

Как я могу выполнить эту математику даты в Postgres?

4b9b3361

Ответ 1

Если вам не нужны объяснения и подробности, используйте "Черную магическую версию" ниже.

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

Учитывая следующую простую таблицу:

CREATE TABLE event (
  event_id   serial PRIMARY KEY
, event_date date
);

запрос

Версии 1. и 2. ниже могут использовать простой индекс вида:

CREATE INDEX event_event_date_idx ON event(event_date);

Но все следующие решения еще быстрее без индекса.

1. Простая версия

SELECT *
FROM  (
   SELECT ((current_date + d) - interval '1 year' * y)::date AS event_date
   FROM       generate_series( 0,  14) d
   CROSS JOIN generate_series(13, 113) y
   ) x
JOIN  event USING (event_date);

Подзапрос x вычисляет все возможные даты за заданный диапазон лет из CROSS JOIN двух вызовов generate_series(). Выбор осуществляется с помощью окончательного простого соединения.

2. Продвинутая версия

WITH val AS (
   SELECT extract(year FROM age(current_date + 14, min(event_date)))::int AS max_year
        , extract(year FROM age(current_date,      max(event_date)))::int AS min_year
   FROM   event
   )
SELECT e.*
FROM  (
   SELECT ((current_date + d.d) - interval '1 year' * y.y)::date AS event_date
   FROM   generate_series(0, 14) d
        ,(SELECT generate_series(min_year, max_year) AS y FROM val) y
   ) x
JOIN  event e USING (event_date);

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

Эффективность зависит от распределения дат. Несколько лет с большим количеством строк делают это решение более полезным. Многие годы с несколькими строками делают его менее полезным.

Простая SQL Fiddle для игры.

3. Черная магия версия

Обновлен 2016, чтобы удалить "сгенерированный столбец", который блокировал бы ГОРЯЧИЕ обновления;более простая и быстрая функция.
Обновлен 2018 для расчета MMDD с выражениями IMMUTABLE чтобы разрешить встраивание функций.

Создайте простую функцию SQL для вычисления integer из шаблона 'MMDD':

CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

to_char(time, 'MMDD') я использовал to_char(time, 'MMDD'), но переключился на приведенное выше выражение, которое оказалось самым быстрым в новых тестах на Postgres 9.6 и 10:

дБ <> скрипка здесь

Это позволяет встраивать функции, потому что EXTRACT EXTRACT (xyz FROM date) реализован с IMMUTABLE функции IMMUTABLE date_part(text, date) внутри. И он должен быть IMMUTABLE чтобы разрешить его использование в следующем IMMUTABLE индексе выражения из нескольких IMMUTABLE:

CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);

Многоколонна по ряду причин:
Может помочь с ORDER BY или с выбором из данных лет. Читайте здесь. Почти без дополнительных затрат на индекс. date вписывается в 4 байта, которые в противном случае были бы потеряны из-за выравнивания данных. Читайте здесь.
Кроме того, поскольку оба столбца индекса ссылаются на один и тот же столбец таблицы, недостатка в отношении обновлений HOT нет. Читайте здесь.

Одна табличная функция PL/pgSQL, чтобы управлять ими всеми

В ответ на один из двух запросов на конец года:

CREATE OR REPLACE FUNCTION f_anniversary(date = current_date, int = 14)
  RETURNS SETOF event AS
$func$
DECLARE
   d  int := f_mmdd($1);
   d1 int := f_mmdd($1 + $2 - 1);  -- fix off-by-1 from upper bound
BEGIN
   IF d1 > d THEN
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) BETWEEN d AND d1
      ORDER  BY f_mmdd(e.event_date), e.event_date;

   ELSE  -- wrap around end of year
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) >= d OR
             f_mmdd(e.event_date) <= d1
      ORDER  BY (f_mmdd(e.event_date) >= d) DESC, f_mmdd(e.event_date), event_date;
      -- chronological across turn of the year
   END IF;
END
$func$  LANGUAGE plpgsql;

Звоните по умолчанию: 14 дней, начиная с "сегодня":

SELECT * FROM f_anniversary();

Позвоните на 7 дней, начиная с 2014-08-23:

SELECT * FROM f_anniversary(date '2014-08-23', 7);

SQL Fiddle, сравнивающий EXPLAIN ANALYZE.

29 февраля

При работе с юбилеями или "днями рождения" необходимо определить, как обращаться с особым случаем "29 февраля" в високосные годы.

При тестировании диапазонов дат Feb 29 обычно включается автоматически, даже если текущий год не является високосным. Диапазон дней увеличивается на 1 задним числом, когда он охватывает этот день.
С другой стороны, если текущий год является високосным, и вы хотите искать 15 дней, вы можете получить результаты за 14 дней в високосные годы, если ваши данные взяты из не високосных лет.

Скажем, Боб родился 29 февраля:
Мои запросы 1. и 2. включают 29 февраля только в високосные годы. У Боба день рождения только каждые ~ 4 года.
Мой запрос 3. 29 февраля включает в себя диапазон. У Боба день рождения каждый год.

Там нет волшебного решения. Вы должны определить, что вы хотите для каждого случая.

Тестовое задание

Чтобы подтвердить свою точку зрения, я провел обширный тест со всеми представленными решениями. Я адаптировал каждый из запросов к данной таблице и вывел идентичные результаты без ORDER BY.

Хорошая новость: все они верны и дают одинаковый результат - за исключением запроса Гордона с синтаксическими ошибками и запроса @wildplasser, который завершается неудачно, когда год заканчивается (легко исправить).

Вставьте 108000 строк со случайными датами из 20-го века, который похож на таблицу живых людей (13 лет и старше).

INSERT INTO  event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM   generate_series (1, 108000);

Удалите ~ 8%, чтобы создать несколько мертвых кортежей и сделать таблицу более "реальной".

DELETE FROM event WHERE random() < 0.08;
ANALYZE event;

В моем тестовом примере было 99289 строк, 4012 просмотров.

C - Catcall

WITH anniversaries as (
   SELECT event_id, event_date
         ,(event_date + (n || ' years')::interval)::date anniversary
   FROM   event, generate_series(13, 113) n
   )
SELECT event_id, event_date -- count(*)   --
FROM   anniversaries
WHERE  anniversary BETWEEN current_date AND current_date + interval '14' day;

C1 - идея Catcall переписана

Помимо незначительных оптимизаций, основным отличием является добавление только точного количества лет date_trunc('year', age(current_date + 14, event_date)) чтобы получить годовщину этого года, что полностью исключает необходимость использования CTE:

SELECT event_id, event_date
FROM   event
WHERE (event_date + date_trunc('year', age(current_date + 14, event_date)))::date
       BETWEEN current_date AND current_date + 14;

Д - Даниэль

SELECT *   -- count(*)   -- 
FROM   event
WHERE  extract(month FROM age(current_date + 14, event_date))  = 0
AND    extract(day   FROM age(current_date + 14, event_date)) <= 14;

E1 - Эрвин 1

См. "1. Простая версия" выше.

Е2 - Эрвин 2

См. "2. Расширенная версия" выше.

Е3 - Эрвин 3

См. "3. Черная магия версия" выше.

G - Гордон

SELECT * -- count(*)   
FROM  (SELECT *, to_char(event_date, 'MM-DD') AS mmdd FROM event) e
WHERE  to_date(to_char(now(), 'YYYY') || '-'
                 || (CASE WHEN mmdd = '02-29' THEN '02-28' ELSE mmdd END)
              ,'YYYY-MM-DD') BETWEEN date(now()) and date(now()) + 14;

H - a_horse_with_no_name

WITH upcoming as (
   SELECT event_id, event_date
         ,CASE 
            WHEN date_trunc('year', age(event_date)) = age(event_date)
                 THEN current_date
            ELSE cast(event_date + ((extract(year FROM age(event_date)) + 1)
                      * interval '1' year) AS date) 
          END AS next_event
   FROM event
   )
SELECT event_id, event_date
FROM   upcoming
WHERE  next_event - current_date  <= 14;

W - wildplasser

CREATE OR REPLACE FUNCTION this_years_birthday(_dut date) RETURNS date AS
$func$
DECLARE
    ret date;
BEGIN
    ret :=
    date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
         - date_trunc( 'year' , _dut));
    RETURN ret;
END
$func$ LANGUAGE plpgsql;

Упрощенно вернуть так же, как и все остальные:

SELECT *
FROM   event e
WHERE  this_years_birthday( e.event_date::date )
        BETWEEN current_date
        AND     current_date + '2weeks'::interval;

W1 - переписан запрос wildplasser

Вышесказанное страдает от ряда неэффективных деталей (выходящих за рамки этого уже значительного поста). Переписанная версия намного быстрее:

CREATE OR REPLACE FUNCTION this_years_birthday(_dut INOUT date) AS
$func$
SELECT (date_trunc('year', now()) + ($1 - date_trunc('year', $1)))::date
$func$ LANGUAGE sql;

SELECT *
FROM   event e
WHERE  this_years_birthday(e.event_date)
        BETWEEN current_date
        AND    (current_date + 14);

Результаты теста

Я запустил этот тест с временной таблицей на PostgreSQL 9.1.7. Результаты были собраны с EXPLAIN ANALYZE, лучший из 5.

Результаты

Without index
C:  Total runtime: 76714.723 ms
C1: Total runtime:   307.987 ms  -- !
D:  Total runtime:   325.549 ms
E1: Total runtime:   253.671 ms  -- !
E2: Total runtime:   484.698 ms  -- min() & max() expensive without index
E3: Total runtime:   213.805 ms  -- !
G:  Total runtime:   984.788 ms
H:  Total runtime:   977.297 ms
W:  Total runtime:  2668.092 ms
W1: Total runtime:   596.849 ms  -- !

With index
E1: Total runtime:    37.939 ms  --!!
E2: Total runtime:    38.097 ms  --!!

With index on expression
E3: Total runtime:    11.837 ms  --!!

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

Conclusio

  • Пока что запрос @Daniel был самым быстрым.

  • Подход @wildplassers (переписанный) тоже работает приемлемо.

  • Версия @Catcall - это что-то вроде моего обратного подхода. Производительность быстро выходит из-под контроля с большими столами.
    Переписанная версия работает довольно хорошо. Выражение, которое я использую, напоминает более простую версию функции @wildplassser this_years_birthday().

  • Моя "простая версия" быстрее даже без индекса, потому что она требует меньше вычислений.

  • С индексом "расширенная версия" примерно так же быстро, как и "простая версия", потому что min() и max() становятся очень дешевыми с индексом. Оба существенно быстрее, чем остальные, которые не могут использовать индекс.

  • Моя "версия черной магии" самая быстрая с индексом или без него. И это очень просто позвонить.
    Обновленная версия (после бенчмарка) пока немного быстрее.

  • В реальной таблице индекс будет иметь еще большее значение. Чем больше столбцов, тем больше таблица, а последовательное сканирование дороже, а размер индекса остается прежним.

Ответ 2

Я считаю, что следующий тест работает во всех случаях, предполагая столбец с именем anniv_date:

select * from events
where extract(month from age(current_date+interval '14 days', anniv_date))=0
  and extract(day from age(current_date+interval '14 days', anniv_date)) <= 14

В качестве примера того, как он работает, когда вы пересекаете год (а также месяц), допустим, что юбилейная дата 2009-01-04, а дата запуска теста - 2012-12-29.

Мы хотим рассмотреть любую дату между 2012-12-29 и 2013-01-12 (14 дней)

age('2013-01-12'::date, '2009-01-04'::date) 4 years 8 days.

extract(month...) из этого 0, а extract(days...) - 8, что ниже, чем 14, чтобы оно соответствовало.

Ответ 3

Как насчет этого?

select *
from events e
where to_char(e."date", 'MM-DD') between to_char(now(), 'MM-DD') and 
                                         to_char(date(now())+14, 'MM-DD')

Вы можете выполнить сравнение как строки.

Чтобы уйти в прошлое, мы вернемся к датам:

select *
from events e
where to_date(to_char(now(), 'YYYY')||'-'||to_char(e."date", 'MM-DD'), 'YYYY-MM-DD')
           between date(now()) and date(now())+14

Вам нужно сделать небольшую корректировку 29 февраля. Я мог бы предложить:

select *
from (select e.*,
             to_char(e."date", 'MM-DD') as MMDD
      from events
     ) e
where to_date(to_char(now(), 'YYYY')||'-'||(case when MMDD = '02-29' then '02-28' else MMDD), 'YYYY-MM-DD')
           between date(now()) and date(now())+14

Ответ 4

Для удобства я создал две функции, которые дают (ожидаемый или прошлый) birsthday в текущем году и предстоящий день рождения.

CREATE OR REPLACE FUNCTION this_years_birthday( _dut DATE) RETURNS DATE AS
$func$

DECLARE
        ret DATE;
BEGIN
        ret =
        date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
          - date_trunc( 'year' , _dut)
          )
        ;
        RETURN ret;
END;
$func$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION next_birthday( _dut DATE) RETURNS DATE AS
$func$

DECLARE
        ret DATE;
BEGIN
        ret =
        date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
          - date_trunc( 'year' , _dut)
          )
        ;
        IF (ret < date_trunc( 'day' , current_timestamp))
           THEN ret = ret + '1year'::interval; END IF;
        RETURN ret;
END;
$func$ LANGUAGE plpgsql;

      --
      -- call the function
      --
SELECT date_trunc( 'day' , t.topic_date) AS the_date
        , this_years_birthday( t.topic_date::date ) AS the_day
        , next_birthday( t.topic_date::date ) AS next_day
FROM topic t
WHERE this_years_birthday( t.topic_date::date )
        BETWEEN  current_date
        AND  current_date + '2weeks':: interval
        ;

ПРИМЕЧАНИЕ: приведение в действие необходимо, потому что у меня есть только временные метки.

Ответ 5

Вы можете создать виртуальную таблицу юбилеев и выбрать ее.

with anniversaries as (
  select event_date, 
         (event_date + (n || ' years')::interval)::date anniversary
  from events, generate_series(1,10) n
)
select event_date, anniversary
from anniversaries
where anniversary between current_date and current_date + interval '14' day
order by event_date, anniversary

Вызов generate_series(1,10) приводит к созданию десятилетних юбилеев для каждого события event_date. Я бы не использовал буквальное значение 10 в производстве. Вместо этого я либо вычислил правильное количество лет для использования в подзапросе, либо использовал бы большой литерал, например 100.

Вы хотите настроить предложение WHERE в соответствии с вашим приложением.

Если у вас есть проблема с производительностью с виртуальной таблицей (когда у вас много строк в "событиях" ), замените общее табличное выражение базовой таблицей, имеющей идентичную структуру. Сохранение юбилеев в базовой таблице делает их ценности очевидными (особенно, например, для юбилеев 29 февраля), а запросы на такую ​​таблицу могут использовать индекс. Запрос таблицы юниониций из полумиллиона строк, используя только оператор SELECT выше, занимает 25 мс на моем рабочем столе.

Ответ 6

Это также должно обрабатывать обертывания в конце года:

with upcoming as (
  select name, 
         event_date,
         case 
           when date_trunc('year', age(event_date)) = age(event_date) then current_date
           else cast(event_date + ((extract(year from age(event_date)) + 1) * interval '1' year) as date) 
         end as next_event
  from events
)
select name, 
       next_event, 
       next_event - current_date as days_until_next
from upcoming
order by next_event - current_date 

Вы можете фильтровать, чем на выражение next_event - current_date, чтобы применить "следующие 14 дней"

case ... необходим только в том случае, если вы рассматриваете события, которые были бы "сегодня" как "предстоящие". В противном случае это можно свести к else части инструкции case.

Обратите внимание, что я "переименовал" столбец "date" в event_date. Главным образом потому, что зарезервированные слова не должны использоваться в качестве идентификатора, а также потому, что date - ужасное имя столбца. Он ничего не говорит о том, что он хранит.

Ответ 7

Я нашел способ сделать это.

SELECT EXTRACT(DAYS FROM age('1999-04-10', '2003-05-12')), 
       EXTRACT(MONTHS FROM age('1999-04-10', '2003-05-12'));
 date_part | date_part 
-----------+-----------
        -2 |        -1

Затем я могу просто проверить, что месяц равен 0, а дни меньше 14.

Если у вас есть более элегантное решение, отправьте его. Я немного оставлю вопрос открытым.

Ответ 8

Я не работаю с postgresql, поэтому я искал его функции даты и нашел это: http://www.postgresql.org/docs/current/static/functions-datetime.html

Если я прочитал его правильно, поиск событий в следующие 14 дней будет таким же простым, как:

 where mydatefield >= current_date
 and mydatefield < current_date + integer '14'

Конечно, я, возможно, не читал его правильно.