Použití pomocné tabulky obsahující kalendář
Pomocné tabulky se v SQL používají poměrně často. Umožňují doplnit určitá data, která potřebujeme pro výpočet. Například, pro určení průměrných denních výdajů potřebujeme doplnit základní data i o dny, kdy nedošlo k výdaji (jenom proto aby se zvýšil počet dní). Typicky pomocná tabulka bude obsahovat číselnou řadu nebo kalendář. V PostgreSQL máme k dispozici funkci generate_series, která vygeneruje řádky, takže pomocná tabulka může být dynamická. Relativně obtížně se určuje, zda-li se jedná o pracovní den, či nikoliv. K tomu použiji vlastní funkce:
CREATE OR REPLACE FUNCTION velnedele(_rok INTEGER) RETURNS DATE AS $$ DECLARE B INTEGER; D INTEGER; E INTEGER; Q INTEGER; DD INTEGER; MM INTEGER; BEGIN IF _rok < 1900 OR _rok > 2099 THEN RAISE EXCEPTION 'Out of range'; END IF; B := 255 - 11 * ($1 % 19); D := ((B - 21) % 30) + 21; IF D > 38 THEN D := D - 1; END IF; E := ($1 + $1/4 + D + 1) % 7; Q := D + 7 - E; IF Q < 32 THEN DD:=Q; MM := 3; ELSE DD := Q - 31; MM := 4; END IF; RETURN to_date(to_char(DD, '00') || to_char(MM, '00') || to_char(_rok,'0000'), 'DD MM YYYY'); END; $$ LANGUAGE plpgsql IMMUTABLE STRICT; -- vrati nazev nebo NULL, pokud den neni statnim svatkem CREATE OR REPLACE FUNCTION StatniSvatek(d date) RETURNS varchar AS $$ DECLARE vnedele date; result varchar; BEGIN result := CASE to_char(d, 'MMDD') WHEN '0101' THEN 'Nový rok' WHEN '0501' THEN 'Svátek práce' WHEN '0508' THEN 'Den osvobození' WHEN '0705' THEN 'Den slovanských věrozvěstů Cyrila a Metoděje' WHEN '0706' THEN 'Den upálení mistra Jana Husa' WHEN '0928' THEN 'Den české státnosti' WHEN '1028' THEN 'Den vzniku samostatného československého státu' WHEN '1117' THEN 'Den boje za svobodu a demokracii' WHEN '1224' THEN 'Štědrý den' WHEN '1225' THEN '1. svátek vánoční' WHEN '1226' THEN '2. svátek vánoční' END; IF result IS NULL THEN vnedele := velnedele(EXTRACT('year' FROM d)::integer); result := CASE WHEN d = vnedele THEN 'Neděle velikonoční' WHEN d = vnedele + 1 THEN 'Pondělí velikonoční' END; END IF; RETURN result; END; $$ LANGUAGE plpgsql IMMUTABLE STRICT; CREATE OR REPLACE FUNCTION DenDlouze(d date) RETURNS varchar AS $$ SELECT CASE EXTRACT('dow' FROM $1) WHEN 0 THEN 'Neděle' WHEN 1 THEN 'Pondělí' WHEN 2 THEN 'Úterý' WHEN 3 THEN 'Středa' WHEN 4 THEN 'Čtvrtek' WHEN 5 THEN 'Pátek' WHEN 6 THEN 'Sobota' END; $$ LANGUAGE sql IMMUTABLE STRICT; CREATE OR REPLACE FUNCTION DenKratce(d date) RETURNS varchar AS $$ SELECT CASE EXTRACT('dow' FROM $1) WHEN 0 THEN 'Ne' WHEN 1 THEN 'Po' WHEN 2 THEN 'Út' WHEN 3 THEN 'St' WHEN 4 THEN 'Čt' WHEN 5 THEN 'Pá' WHEN 6 THEN 'So' END; $$ LANGUAGE sql IMMUTABLE STRICT;
Pomocna tabulka pro rok 2007 může mít následující obsah:
postgres=# SELECT d, DenDlouze(d), DenKratce(d), NOT(EXTRACT('dow' FROM d) IN (0,6) OR StatniSvatek(d) IS NOT NULL) AS prac_den, StatniSvatek(d) FROM (SELECT date '2007-01-01' + i AS d FROM generate_series(355,364) g(i))s; d | dendlouze | denkratce | prac_den | statnisvatek ------------+-----------+-----------+----------+------------------- 2007-12-22 | Sobota | So | f | 2007-12-23 | Neděle | Ne | f | 2007-12-24 | Pondělí | Po | f | Štědrý den 2007-12-25 | Úterý | Út | f | 1. svátek vánoční 2007-12-26 | Středa | St | f | 2. svátek vánoční 2007-12-27 | Čtvrtek | Čt | t | 2007-12-28 | Pátek | Pa | t | 2007-12-29 | Sobota | So | f | 2007-12-30 | Neděle | Ne | f | 2007-12-31 | Pondělí | Po | t | (10 rows)
Tuto tabulku si uložím do skutečné tabulky kalendar.
Jak s touto tabulkou pracovat?
- Počet pracovních dní (od, do)
postgres=# SELECT count(*) FROM kalendar WHERE d BETWEEN '2007-10-01' AND '2007-11-01' AND prac_den; count ------- 24 (1 row)
- Odečet n pracovních dnů od určitého dne. Např. 10 pracovních dnů od 20.12.2007 (pozn. ukázkový příklad síly neprocedurálního SQL, s použitím procedur a cyklu lze tuto úlohu řešit řádově rychleji, po delší době úloha, kde by bylo možné úspěšně použít kurzory):
postgres=# SELECT * FROM kalendar k WHERE k.prac_den AND 10 = (SELECT count(*) FROM kalendar k2 WHERE k2.d BETWEEN k.d AND date '2007-12-20' - 1 AND k2.prac_den); d | dendlouze | denkratce | prac_den | statnisvatek ------------+-----------+-----------+----------+-------------- 2007-12-06 | Čtvrtek | Čt | t | (1 row)
- všechny svátky během pracovního týdne:
postgres=# SELECT * FROM kalendar WHERE statnisvatek IS NOT NULL AND denkratce NOT IN ('So','Ne'); d | dendlouze | denkratce | prac_den | statnisvatek ------------+-----------+-----------+----------+---------------------------------------------- 2007-01-01 | Pondělí | Po | f | Nový rok 2007-04-09 | Pondělí | Po | f | Pondělí velikonoční 2007-05-01 | Úterý | Út | f | Svátek práce 2007-05-08 | Úterý | Út | f | Den osvobození 2007-07-05 | Čtvrtek | Čt | f | Den slovanských věrozvěstů Cyrila a Metoděje 2007-07-06 | Pátek | Pa | f | Den upálení mistra Jana Husa 2007-09-28 | Pátek | Pa | f | Den české státnosti 2007-12-24 | Pondělí | Po | f | Štědrý den 2007-12-25 | Úterý | Út | f | 1. svátek vánoční 2007-12-26 | Středa | St | f | 2. svátek vánoční (10 rows)
Inspirováno článkem http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html.
Joe Celko v článku http://www.simple-talk.com/content/article.aspx?article=632 ještě doporučuje přidat sloupec julian_business_nbr tj číslo pracovního dne. V případě svátků se toto číslo nemění. Pro výpočet tohoto čísla lze použít uloženou proceduru nebo window funkci sum - kumulativní:
CREATE OR REPLACE FUNCTION prac_den(date) RETURNS boolean AS $$ SELECT NOT(EXTRACT('dow' FROM $1) IN (0,6) OR StatniSvatek($1) IS NOT NULL); $$ LANGUAGE sql IMMUTABLE STRICT; postgres=# SELECT d, DenDlouze(d), DenKratce(d), prac_den(d), SUM(CASE WHEN prac_den(d) THEN 1 ELSE 0 END) OVER (ORDER BY d) AS cislo_prac_dne, StatniSvatek(d) FROM (SELECT date '2007-01-01' + i AS d FROM generate_series(0,366) g(i))s LIMIT 10; d | dendlouze | denkratce | prac_den | cislo_prac_dne | statnisvatek ------------+-----------+-----------+----------+----------------+------------------- 2007-12-22 | Sobota | So | f | 248 | ---- 2007-12-23 | Neděle | Ne | f | 248 | ---- 2007-12-24 | Pondělí | Po | f | 248 | Štědrý den 2007-12-25 | Úterý | Út | f | 248 | 1. svátek vánoční 2007-12-26 | Středa | St | f | 248 | 2. svátek vánoční 2007-12-27 | Čtvrtek | Čt | t | 249 | ---- 2007-12-28 | Pátek | Pá | t | 250 | ---- 2007-12-29 | Sobota | So | f | 250 | ---- 2007-12-30 | Neděle | Ne | f | 250 | ---- 2007-12-31 | Pondělí | Po | t | 251 | ---- (10 rows)
Díky sloupci cislo_prac_dne snadno můžeme provádět operace nad pracovním kalendářem:
postgres=# SELECT * FROM kalendar WHERE cislo_prac_dne = (SELECT cislo_prac_dne FROM kalendar WHERE d = '2009-07-30') + 0 AND prac_den; d | dendlouze | denkratce | prac_den | cislo_prac_dne | statnisvatek ------------+-----------+-----------+----------+----------------+-------------- 2009-07-30 | Čtvrtek | Čt | t | 650 | ---- (1 row) postgres=# SELECT * FROM kalendar WHERE cislo_prac_dne = (SELECT cislo_prac_dne FROM kalendar WHERE d = '2009-07-30') + 3 AND prac_den; d | dendlouze | denkratce | prac_den | cislo_prac_dne | statnisvatek ------------+-----------+-----------+----------+----------------+-------------- 2009-08-04 | Úterý | Út | t | 653 | ---- (1 row)