Pokud vás nezajímá vysvětlení a podrobnosti, použijte "Verze černé magie" níže.
Všechny dotazy uvedené v jiných odpovědích zatím fungují s podmínkami, které nelze proměnit - nemohou používat index a musí vypočítat výraz pro každý jednotlivý řádek v základní tabulce, aby našli odpovídající řádky. U malých stolků to moc nevadí. Záleží (hodně ) s velkými stoly.
Vzhledem k následující jednoduché tabulce:
CREATE TABLE event (
event_id serial PRIMARY KEY
, event_date date
);
Dotaz
Verze 1. a 2. níže mohou používat jednoduchý index ve tvaru:
CREATE INDEX event_event_date_idx ON event(event_date);
Ale všechna následující řešení jsou ještě rychlejší bez indexu .
1. Jednoduchá verze
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);
Poddotaz x
vypočítá všechna možná data v daném rozsahu let z CROSS JOIN
ze dvou generate_series()
hovory. Výběr se provádí konečným jednoduchým spojením.
2. Pokročilá verze
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);
Rozsah let se automaticky odečítá z tabulky – čímž se generované roky minimalizují.
Můžete mohli jděte o krok dále a destilujte seznam existujících let, pokud existují mezery.
Efektivita spoluzávisí na rozložení termínů. Několik let s mnoha řadami činí toto řešení užitečnějším. Mnoho let s několika řádky je méně užitečné.
Simple SQL Fiddle hrát.
3. Verze černé magie
Aktualizováno v roce 2016 k odstranění „generovaného sloupce“, který by blokoval H.O.T. aktualizace; jednodušší a rychlejší funkce.
Aktualizováno v roce 2018 pro výpočet MMDD pomocí IMMUTABLE
výrazy umožňující vkládání funkcí.
Vytvořte jednoduchou funkci SQL pro výpočet integer
ze vzoru 'MMDD'
:
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';
Měl jsem to_char(time, 'MMDD')
nejprve, ale přešel na výše uvedený výraz, který se ukázal jako nejrychlejší v nových testech na Postgres 9.6 a 10:
db<>zde hrajte
Umožňuje funkci vkládání, protože EXTRACT (xyz FROM date)
je implementován pomocí IMMUTABLE
funkce date_part(text, date)
vnitřně. A musí být IMMUTABLE
umožnit jeho použití v následujícím základním vícesloupcovém indexu výrazu:
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
Více sloupců z mnoha důvodů:
Může pomoci s ORDER BY
nebo s výběrem z daných let. Čtěte zde. Téměř bez dodatečných nákladů na index. date
vejde do 4 bajtů, které by jinak byly ztraceny při vyplnění kvůli zarovnání dat. Přečtěte si zde.
Také, protože oba sloupce indexu odkazují na stejný sloupec tabulky, žádná nevýhoda s ohledem na H.O.T. aktualizace. Přečtěte si zde.
Jedna tabulková funkce PL/pgSQL, která všem vládne
Vidíte jeden ze dvou dotazů na přelom 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;
Zavolejte pomocí výchozích hodnot:14 dní počínaje dnem:
SELECT * FROM f_anniversary();
Volejte 7 dní od 23. 8. 2014:
SELECT * FROM f_anniversary(date '2014-08-23', 7);
SQL Fiddle porovnání EXPLAIN ANALYZE
.
29. února
Když řešíte výročí nebo „narozeniny“, musíte definovat, jak se vypořádat se zvláštním případem „29. února“ v přestupných letech.
Při testování rozsahů dat Feb 29
je obvykle zahrnuta automaticky, i když aktuální rok není přestupný . Rozsah dnů se zpětně prodlouží o 1, když pokrývá tento den.
Na druhou stranu, pokud je aktuální rok přestupným rokem a chcete hledat 15 dní, můžete nakonec získat výsledky za 14 dnů v přestupných letech, pokud vaše data pocházejí z nepřestupných let.
Řekněme, že Bob se narodil 29. února:
Můj dotaz 1. a 2. zahrnuje 29. únor pouze v přestupném roce. Bob má narozeniny pouze každé ~ 4 roky.
Můj dotaz 3. zahrnuje 29. února v rozsahu. Bob má každý rok narozeniny.
Žádné magické řešení neexistuje. Pro každý případ musíte definovat, co chcete.
Test
Abych svůj názor potvrdil, provedl jsem rozsáhlý test se všemi předloženými řešeními. Každý z dotazů jsem přizpůsobil dané tabulce a poskytl identické výsledky bez ORDER BY
.
Dobrá zpráva:všechny jsou správné a přinese stejný výsledek – s výjimkou Gordonova dotazu, který měl syntaktické chyby, a dotazu @wildplasser, který selže, když se rok zalomí (snadno opravit).
Vložte 108 000 řádků s náhodnými daty z 20. století, což je obdoba tabulky žijících lidí (13 nebo starších).
INSERT INTO event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM generate_series (1, 108000);
Smažte ~ 8 %, abyste vytvořili nějaké mrtvé n-tice a udělali stůl více "reálným".
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
Můj testovací případ měl 99289 řádků, 4012 zásahů.
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ův nápad přepsán
Kromě drobných optimalizací je hlavním rozdílem přidat pouze přesný počet let date_trunc('year', age(current_date + 14, event_date))
abychom dosáhli letošního výročí, díky čemuž není potřeba 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
Viz „1. Jednoduchá verze“ výše.
E2 – Erwin 2
Viz "2. Pokročilá verze" výše.
E3 – Erwin 3
Viz "3. Verze černé magie" výše.
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_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;
Zjednodušeno, aby se vrátilo stejné jako všechny ostatní:
SELECT *
FROM event e
WHERE this_years_birthday( e.event_date::date )
BETWEEN current_date
AND current_date + '2weeks'::interval;
W1 – Wildplasserův dotaz přepsán
Výše uvedené trpí řadou neefektivních detailů (nad rámec tohoto již tak rozměrného příspěvku). Přepsaná verze je hodně rychleji:
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);
Výsledky testu
Tento test jsem provedl s dočasnou tabulkou na PostgreSQL 9.1.7. Výsledky byly shromážděny pomocí EXPLAIN ANALYZE
, nejlepší z 5.
Výsledky
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 --!!
Všechny ostatní dotazy fungují stejně s indexem nebo bez něj, protože používají neproměnitelné výrazy.
Závěr
-
Dosud byl @Danielův dotaz nejrychlejší.
-
@wildplassers (přepsaný) přístup funguje také přijatelně.
-
Verze @Catcall je něco jako můj obrácený přístup. Výkon se u větších stolů rychle vymkne kontrole.
Přepsaná verze však funguje docela dobře. Výraz, který používám, je něco jako jednodušší verzethis_years_birthday()
@wildplassser funkce. -
Moje "jednoduchá verze" je rychlejší i bez indexu , protože potřebuje méně výpočtů.
-
S indexem je "pokročilá verze" přibližně stejně rychlá jako "jednoduchá verze", protože
min()
amax()
stát se velmi levné s indexem. Oba jsou podstatně rychlejší než ostatní, které nemohou používat index. -
Moje "verze černé magie" je nejrychlejší s indexem nebo bez něj . A je to velmi jednoduché volání.
-
S reálnou tabulkou index bude ještě větší rozdíl. Více sloupců zvětší tabulku a sekvenční skenování je dražší, přičemž velikost indexu zůstává stejná.