PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Jak datuje się matematykę, która ignoruje rok?

Jeśli nie zależy Ci na wyjaśnieniach i szczegółach, skorzystaj z „Wersji czarnej magii” poniżej.

Wszystkie zapytania przedstawione w innych odpowiedziach do tej pory działają z warunkami, które nie są sargowalne - nie mogą używać indeksu i muszą obliczyć wyrażenie dla każdego wiersza w tabeli bazowej, aby znaleźć pasujące wiersze. Przy małych stołach nie ma to większego znaczenia. Sprawy (dużo ) z dużymi stołami.

Biorąc pod uwagę następującą prostą tabelę:

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

Zapytanie

W wersjach 1. i 2. poniżej można użyć prostego indeksu formularza:

CREATE INDEX event_event_date_idx ON event(event_date);

Ale wszystkie poniższe rozwiązania są jeszcze szybsze bez indeksu .

1. Wersja prosta

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);

Podzapytanie x oblicza wszystkie możliwe daty z danego zakresu lat z CROSS JOIN z dwóch generate_series() wzywa. Wybór odbywa się za pomocą ostatniego prostego połączenia.

2. Wersja zaawansowana

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);

Zakres lat jest automatycznie wyliczany z tabeli, minimalizując w ten sposób generowane lata.
Możesz mógł idź o krok dalej i przeanalizuj listę istniejących lat, jeśli są luki.

Skuteczność współzależna jest od rozkładu dat. Kilka lat z wieloma rzędami sprawia, że ​​to rozwiązanie jest bardziej przydatne. Wiele lat, każdy z kilkoma rzędami, czyni go mniej użytecznym.

Proste skrzypce SQL do zabawy.

3. Wersja czarnej magii

Zaktualizowano 2016, aby usunąć „wygenerowaną kolumnę”, która blokowałaby H.O.T. aktualizacje; prostsza i szybsza funkcja.
Zaktualizowano 2018, aby obliczyć MMDD za pomocą IMMUTABLE wyrażeń umożliwiających wstawianie funkcji.

Utwórz prostą funkcję SQL do obliczenia integer ze wzorca 'MMDD' :

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

Miałem to_char(time, 'MMDD') na początku, ale przerzuciłem się na powyższe wyrażenie, które okazało się najszybsze w nowych testach na Postgresie 9.6 i 10:

db<>graj tutaj

Pozwala na inlining funkcji, ponieważ EXTRACT (xyz FROM date) jest zaimplementowany za pomocą IMMUTABLE funkcja date_part(text, date) wewnętrznie. I musi być IMMUTABLE aby umożliwić jego użycie w następującym podstawowym indeksie wyrażeń wielokolumnowych:

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

Wielokolumnowe z wielu powodów:
Może pomóc w ORDER BY lub z wyborem z podanych lat. Przeczytaj tutaj. Prawie bez dodatkowych kosztów za indeks. date mieści się w 4 bajtach, które w przeciwnym razie zostałyby utracone przez wypełnienie z powodu wyrównania danych. Przeczytaj tutaj.
Ponadto, ponieważ obie kolumny indeksu odwołują się do tej samej kolumny tabeli, nie ma żadnych wad w odniesieniu do H.O.T. aktualizacje. Przeczytaj tutaj.

Jedna funkcja tabeli PL/pgSQL do zarządzania wszystkimi

Rozwidlaj jedno z dwóch zapytań dotyczących przełomu roku:

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;

Zadzwoń przy użyciu wartości domyślnych:14 dni od „dzisiaj”:

SELECT * FROM f_anniversary();

Zadzwoń na 7 dni od '2014-08-23':

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

Skrzypce SQL porównywanie EXPLAIN ANALYZE .

29 lutego

Kiedy masz do czynienia z rocznicami lub „urodzinami”, musisz określić, jak radzić sobie ze szczególnym przypadkiem „29 lutego” w latach przestępnych.

Podczas testowania zakresów dat Feb 29 jest zwykle uwzględniany automatycznie, nawet jeśli bieżący rok nie jest rokiem przestępnym . Zakres dni zostaje rozszerzony o 1 z mocą wsteczną, gdy obejmuje ten dzień.
Z drugiej strony, jeśli bieżący rok jest rokiem przestępnym i chcesz poszukać 15 dni, możesz otrzymać wyniki za 14 dni w latach przestępnych, jeśli dane pochodzą z lat innych niż przestępne.

Powiedzmy, że Bob urodził się 29 lutego:
Moje zapytania 1. i 2. uwzględniają datę 29 lutego tylko w latach przestępnych. Bob ma urodziny tylko co ~ 4 lata.
Moje zapytanie 3. obejmuje 29 lutego w zakresie. Bob co roku obchodzi urodziny.

Nie ma magicznego rozwiązania. Musisz określić, czego chcesz w każdym przypadku.

Test

Na poparcie swojej tezy przeprowadziłem obszerny test ze wszystkimi przedstawionymi rozwiązaniami. Każde z zapytań dostosowałem do podanej tabeli i dałem identyczne wyniki bez ORDER BY .

Dobra wiadomość:wszystkie są poprawne i dają ten sam wynik - z wyjątkiem zapytania Gordona, które zawierało błędy składniowe, oraz zapytania @wildplasser, które kończy się niepowodzeniem, gdy rok się kończy (łatwe do naprawienia).

Wstaw 108000 wierszy z losowymi datami z XX wieku, które przypominają tabelę żywych ludzi (13 lat lub więcej).

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

Usuń ~8%, aby utworzyć martwe krotki i uczynić stół bardziej „prawdziwym”.

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

Mój przypadek testowy miał 99289 wierszy, 4012 trafień.

C — Wykrzyknik

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 - Pomysł Catcall przepisany

Oprócz drobnych optymalizacji, główną różnicą jest dodanie tylko dokładnej liczby lat date_trunc('year', age(current_date + 14, event_date)) aby uzyskać tegoroczną rocznicę, co całkowicie eliminuje potrzebę 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 — Daniel

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 — Erwin 1

Zobacz „1. ​​Wersja prosta” powyżej.

E2 — Erwin 2

Zobacz „2. Wersja zaawansowana” powyżej.

E3 — Erwin 3

Zobacz „3. Wersja czarnej magii” powyżej.

G — Gordon

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_koń_bez_imienia

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 - dziki plaster

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;

Uproszczone, aby zwrócić takie same jak wszystkie inne:

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

W1 - przepisane zapytanie wildplasera

Powyższe cierpi z powodu wielu nieefektywnych szczegółów (poza zakresem tego i tak już sporego stanowiska). Przepisana wersja to dużo szybciej:

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);

Wyniki testów

Przeprowadziłem ten test z tabelą tymczasową na PostgreSQL 9.1.7. Wyniki zostały zebrane za pomocą EXPLAIN ANALYZE , najlepsze z 5.

Wyniki

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

Wszystkie inne zapytania działają tak samo z indeksem lub bez niego, ponieważ używają niepodlegających sargowaniu wyrażenia.

Wniosek

  • Jak dotąd zapytanie @Daniela było najszybsze.

  • Podejście @wildplassers (przepisane) również działa akceptowalnie.

  • Wersja @Catcall to coś w rodzaju mojego odwrotnego podejścia. Wydajność szybko wymyka się spod kontroli przy większych stołach.
    Jednak przepisana wersja działa całkiem nieźle. Wyrażenie, którego używam, jest czymś w rodzaju prostszej wersji funkcji this_years_birthday() @wildplassser funkcja.

  • Moja „prosta wersja” jest szybsza nawet bez indeksu , ponieważ wymaga mniej obliczeń.

  • Dzięki indeksowi „wersja zaawansowana” jest mniej więcej tak szybka jak „wersja prosta”, ponieważ min() i max() stać się bardzo tanie z indeksem. Oba są znacznie szybsze niż pozostałe, które nie mogą korzystać z indeksu.

  • Moja „wersja czarnej magii” jest najszybsza z indeksem lub bez . I jest bardzo łatwo zadzwonić.

  • Z tabelą z prawdziwego życia i indeksem uczyni jeszcze większym różnica. Więcej kolumn sprawia, że ​​tabela jest większa, a skanowanie sekwencyjne droższe, podczas gdy rozmiar indeksu pozostaje taki sam.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak Tan() działa w PostgreSQL

  2. Jak zmienić typ kolumny w Heroku?

  3. Sprawdź, czy wartość istnieje w tablicy Postgres

  4. Wstaw obraz do bazy postgresql

  5. Pięć fajnych rzeczy, których nauczyłem się na konferencji PostgreSQL w Europie 2018