sql >> Databáze >  >> RDS >> PostgreSQL

Jak uděláte datovací matematiku, která ignoruje rok?

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šší verze this_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() a max() 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á.



  1. Instalace Microsoft SQL Server 2012 Enterprise Edition s aktualizací Service Pack 1

  2. Požadavek se nezdařil se stavem HTTP 401:Neautorizováno v SSRS

  3. SQL Server Blocking Query

  4. CURRENT_DATE/CURDATE() nefunguje jako výchozí hodnota DATE