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 funkcjithis_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()
imax()
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.