PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Как се среща с математика, която игнорира годината?

Ако не ви интересуват обясненията и подробностите, използвайте „Версия на черната магия“ по-долу.

Всички запитвания, представени в други отговори до момента, работят с условия, които не подлежат на sargable - те не могат да използват индекс и трябва да изчислят израз за всеки отделен ред в основната таблица, за да намерят съвпадащи редове. Няма голямо значение за малките маси. Въпроси (много ) с големи маси.

Като се има предвид следната проста таблица:

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_y
        , extract(year FROM age(current_date,      max(event_date)))::int AS min_y
   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_y, max_y) AS y FROM val) y
   ) x
JOIN  event e USING (event_date);

Диапазонът от години се извежда от таблицата автоматично - като по този начин се минимизират генерираните години.
Вие можете направете още една крачка напред и направете списък със съществуващи години, ако има пропуски.

Ефективността зависи от разпределението на датите. Няколко години с много редове правят това решение по-полезно. Много години с няколко реда всеки го правят по-малко полезен.

Проста SQL цигулка за игра с.

3. Версия на черна магия

Актуализирано през 2016 г. за премахване на „генерирана колона“, която би блокирала H.O.T. актуализации; по-проста и по-бърза функция.
Актуализирано 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') в началото, но премина към горния израз, който се оказа най-бърз в новите тестове на Postgres 9.6 и 10:

db<>цигулка тук

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

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

Многоколона поради редица причини:
Може да помогне с ORDER BY или с избор от дадени години. Прочетете тук. Почти без допълнителни разходи за индекса. date се вписва в 4-те байта, които иначе биха били загубени при подреждане поради подравняване на данните. Прочетете тук.
Освен това, тъй като и двете колони на индекса препращат към една и съща колона на таблицата, няма недостатък по отношение на H.O.T. актуализации. Прочетете тук.

Една таблична функция 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, която се проваля, когато годината приключи (лесна за коригиране).

Вмъкнете 108 000 реда със произволни дати от 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;

D - Даниел

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. Опростена версия“ по-горе.

E2 - Ервин 2

Вижте "2. Разширена версия" по-горе.

E3 - Ервин 3

Вижте "3. Black magic версия" по-горе.

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  --!!

Всички други заявки изпълняват същото със или без индекс, тъй като използват не-sargable изрази.

Заключение

  • Досега заявката на @Daniel беше най-бързата.

  • Подходът на @wildplassers (пренаписан) също се представя приемливо.

  • Версията на @Catcall е нещо като моя обратен подход. Производителността бързо излиза извън контрол с по-големи таблици.
    Пренаписаната версия обаче се представя доста добре. Изразът, който използвам е нещо като по-опростена версия на this_years_birthday() на @wildplassser функция.

  • Моята "проста версия" е по-бърза дори без индекс , защото се нуждае от по-малко изчисления.

  • С индекс "разширената версия" е толкова бърза, колкото и "простата версия", защото min() и max() станете много евтино с индекс. И двете са значително по-бързи от останалите, които не могат да използват индекса.

  • Моята "черна магическа версия" е най-бърза със или без индекс . И е много лесен за обаждане.

  • С реална таблица синдекс ще направи още по-голямо разлика. Повече колони правят таблицата по-голяма, а последователното сканиране по-скъпо, докато размерът на индекса остава същият.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. SQLAlchemy:филтриране на стойности, съхранени във вложен списък на полето JSONB

  2. Не може да се свърже с Postgres чрез PHP, но може да се свърже от командния ред и PgAdmin на друга машина

  3. Връщане на списък с часови зони, поддържани от PostgreSQL

  4. Инсталиране на PDO-драйвери за PostgreSQL на Mac (с помощта на Zend за eclipse)

  5. Използване на прозоречни функции в изявление за актуализиране