Použití CROSS JOIN
Dnes jsem měl poprvé za celou svou praxi příležitost použít křížové spojení pomocí CROSS JOIN
. Tento druh spojení vytvoří kombinace všech řádků ze spojovaných tabulek. Potřeboval jsem sestavit jakýsi kalendář obsazenosti jednotlivých místností. Vytvořil jsem si tabulku pokojů, tabulku rezervací, ale problém nastal s vlastním kalendářem. Ani MySQL, ani PostgreSQL nemají možnost vypsání všech datumů v určitém intervalu pomocí jednoduchého SELECTu
.
Na úvod jsem si vytvořil tabulku pokojů:
CREATE TABLE pokoje ( pokoj_cislo INTEGER, pokoj_oznaceni VARCHAR(32), pokoj_pocetluzek INTEGER, pokoj_pocetpristylek INTEGER, PRIMARY KEY (pokoj_cislo) );
V dalším kroku si vytvořím tabulku číselníků a jejich položek a uložím do ní několik záznamů:
CREATE TABLE ciselniky ( ciselnik_cislo INTEGER, ciselnik_nazev VARCHAR(32), PRIMARY KEY (ciselnik_cislo) ); CREATE TABLE polozky( polozka_cislo INTEGER, polozka_ciselnik INTEGER, polozka_nazev VARCHAR(64), polozka_hodnota TEXT, PRIMARY KEY (polozka_cislo), FOREIGN KEY (polozka_ciselnik) REFERENCES ciselniky(ciselnik_cislo) ON DELETE CASCADE ON UPDATE CASCADE ); INSERT INTO ciselniky VALUES (1, 'Stavy pobytu'); INSERT INTO polozky VALUES (0, 1, 'Volný', '255,255,255'); INSERT INTO polozky VALUES (1, 1, 'Počátek pobytu', '255,255,0'); INSERT INTO polozky VALUES (2, 1, 'Celodenní pobyt', '255, 0, 255'); INSERT INTO polozky VALUES (3, 1, 'Konec pobytu', '0, 255, 255');
Mimochodem číselníky jsou velmi užitečná věc :-)
V dalších krocích vytvořím tabulky objednávek:
CREATE TABLE objednavky ( objednavka_cislo INTEGER, objednavka_objednavajici VARCHAR(64), objednavka_datum DATE, objednavka_potvrzeno DATE, objednavka_odsouhlaseno DATE, PRIMARY KEY (objednavka_cislo) );
A tabulky vlastních rezervací:
CREATE TABLE rezervace ( rezervace_pokoj INTEGER, rezervace_druh INTEGER, rezervace_objednavka INTEGER, rezervace_datum DATE, rezervace_cena NUMERIC(12,2), FOREIGN KEY (rezervace_pokoj) REFERENCES pokoje(pokoj_cislo) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (rezervace_druh) REFERENCES polozky(polozka_cislo) ON DELETE CASCADE ON UPDATE CASCADE );
Základní tabulky bych měl, ale vyvstává problém s kalendářem. Jak na něj? Nabízí se několik řešení, mě napadly dvě:
- Vytvořit tabulku datumů
- Vytvořit funkci, generující datumy v určitém intervalu
Výhodou prvního řešení je fakt, že je poměrně jednoduché a snadno přenositelné. Nevýhodou je to, že je pro každý den v roce nutné vytvořit záznam, takže pro cca 20 let to je bratru 7300 záznamů. Druhé řešení je poměrně elegantní, bohužel je také platformově závislé. Následující funkce je určena pro PostgreSQL (verze 7.4).
CREATE OR REPLACE FUNCTION dnyvintervalu(DATE, DATE) RETURNS SETOF DATE AS ' DECLARE startdate ALIAS FOR $1; enddate ALIAS FOR $2; st date; BEGIN st := startdate; WHILE st < enddate + 1 LOOP RETURN NEXT st; st := st + 1; END LOOP; RETURN; END; ' LANGUAGE plpgsql;
Nyní mám vytvořenou funkci, která mi vypíše jednotlivé dny v daném rozmezí. Převést ji do T-SQL nebo do MySQL nebude takový problém.
SELECT dnyvintervalu FROM dnyvintervalu('2005-5-2','2005-5-5');
dnyvintervalu |
---|
2005-5-2 |
2005-5-3 |
2005-5-4 |
2005-5-5 |
Mám funkce, mám tabulky, teď ještě doplním nějaká data o pokojích a o některých rezervacích.
INSERT INTO pokoje (pokoj_cislo, pokoj_oznaceni, pokoj_pocetluzek) VALUES (101, 'p101', 2); INSERT INTO pokoje (pokoj_cislo, pokoj_oznaceni, pokoj_pocetluzek) VALUES (102, 'p102', 3); INSERT INTO pokoje (pokoj_cislo, pokoj_oznaceni, pokoj_pocetluzek) VALUES (103, 'p103', 3); INSERT INTO pokoje (pokoj_cislo, pokoj_oznaceni, pokoj_pocetluzek) VALUES (104, 'p104', 2); INSERT INTO pokoje (pokoj_cislo, pokoj_oznaceni, pokoj_pocetluzek) VALUES (105, 'p105', 3); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (101, 1, '2006.5.5'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (101, 2, '2006.5.6'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (101, 3, '2006.5.7'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (102, 1, '2006.5.4'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (102, 2, '2006.5.5'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (102, 2, '2006.5.6'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (102, 2, '2006.5.7'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (102, 3, '2006.5.8'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 1, '2006.5.5'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 2, '2006.5.6'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 2, '2006.5.7'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 2, '2006.5.8'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 2, '2006.5.9'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 2, '2006.5.10'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 2, '2006.5.11'); INSERT INTO rezervace (rezervace_pokoj, rezervace_druh, rezervace_datum) VALUES (105, 2, '2006.5.12');
Mám data, vzhůru do dotazů. Prvním dotazem spojím pokoje s jednotlivými datumy
SELECT p.pokoj_cislo AS cislo, p.pokoj_oznaceni AS oznaceni, p.pokoj_pocetluzek AS luzka, p.pokoj_pocetpristylek AS pristylky, den FROM pokoje AS p CROSS JOIN dnyvintervalu('2006-5-5','2006-5-7') AS den;
-------+----------+-------+-----------+------------ cislo | oznaceni | luzka | pristylky | den -------+----------+-------+-----------+------------ 101 | p101 | 2 | 0 | 2006-05-05 101 | p101 | 2 | 0 | 2006-05-06 101 | p101 | 2 | 0 | 2006-05-07 102 | p102 | 3 | 0 | 2006-05-05 102 | p102 | 3 | 0 | 2006-05-06 102 | p102 | 3 | 0 | 2006-05-07 103 | p103 | 3 | 0 | 2006-05-05 103 | p103 | 3 | 0 | 2006-05-06 103 | p103 | 3 | 0 | 2006-05-07 104 | p104 | 2 | 0 | 2006-05-05 104 | p104 | 2 | 0 | 2006-05-06 104 | p104 | 2 | 0 | 2006-05-07 105 | p105 | 3 | 0 | 2006-05-05 105 | p105 | 3 | 0 | 2006-05-06 105 | p105 | 3 | 0 | 2006-05-07
Uvedený výpis sice není špatný, ale nic nám neříká o tom, v jakém stavu se v konkrétní den konkrétní pokoj nachází.
Spojíme tedy tuto tabulku s tabulkou rezervací.
SELECT p.pokoj_cislo AS cislo, p.pokoj_oznaceni AS oznaceni, p.pokoj_pocetluzek AS luzka, p.pokoj_pocetpristylek AS pristylky, den, r.rezervace_druh AS stav FROM pokoje AS p CROSS JOIN dnyvintervalu('2006-5-5','2006-5-7') AS den LEFT JOIN rezervace AS r ON r.rezervace_datum = den AND r.rezervace_pokoj=p.pokoj_cislo;
cislo | oznaceni | luzka | pristylky | den | stav -------+----------+-------+-----------+------------+------ 101 | p101 | 2 | 0 | 2006-05-05 | 1 101 | p101 | 2 | 0 | 2006-05-06 | 2 101 | p101 | 2 | 0 | 2006-05-07 | 3 102 | p102 | 3 | 0 | 2006-05-05 | 2 102 | p102 | 3 | 0 | 2006-05-06 | 2 102 | p102 | 3 | 0 | 2006-05-07 | 2 103 | p103 | 3 | 0 | 2006-05-05 | 103 | p103 | 3 | 0 | 2006-05-06 | 103 | p103 | 3 | 0 | 2006-05-07 | 104 | p104 | 2 | 0 | 2006-05-05 | 104 | p104 | 2 | 0 | 2006-05-06 | 104 | p104 | 2 | 0 | 2006-05-07 | 105 | p105 | 3 | 0 | 2006-05-05 | 1 105 | p105 | 3 | 0 | 2006-05-06 | 2 105 | p105 | 3 | 0 | 2006-05-07 | 2
Už sice víme jak to se kterým pokojem vypadá, ale ještě by to chtělo textové popisky.
SELECT p.pokoj_cislo AS cislo, p.pokoj_oznaceni AS oznaceni, p.pokoj_pocetluzek AS luzka, p.pokoj_pocetpristylek AS pristylky, den, po.polozka_nazev AS stav FROM pokoje AS p CROSS JOIN dnyvintervalu('2006-5-5','2006-5-7') AS den LEFT JOIN rezervace AS r ON r.rezervace_datum = den AND r.rezervace_pokoj=p.pokoj_cislo LEFT JOIN polozky AS po ON r.rezervace_druh = po.polozka_cislo;
-------+----------+-------+-----------+------------+----------------- cislo | oznaceni | luzka | pristylky | den | stav -------+----------+-------+-----------+------------+----------------- 101 | p101 | 2 | 0 | 2006-05-05 | Počátek pobytu 101 | p101 | 2 | 0 | 2006-05-06 | Celodenní pobyt 101 | p101 | 2 | 0 | 2006-05-07 | Konec pobytu 102 | p102 | 3 | 0 | 2006-05-05 | Celodenní pobyt 102 | p102 | 3 | 0 | 2006-05-06 | Celodenní pobyt 102 | p102 | 3 | 0 | 2006-05-07 | Celodenní pobyt 103 | p103 | 3 | 0 | 2006-05-05 | 103 | p103 | 3 | 0 | 2006-05-06 | 103 | p103 | 3 | 0 | 2006-05-07 | 104 | p104 | 2 | 0 | 2006-05-05 | 104 | p104 | 2 | 0 | 2006-05-06 | 104 | p104 | 2 | 0 | 2006-05-07 | 105 | p105 | 3 | 0 | 2006-05-05 | Počátek pobytu 105 | p105 | 3 | 0 | 2006-05-06 | Celodenní pobyt 105 | p105 | 3 | 0 | 2006-05-07 | Celodenní pobyt
Konečně už víme, jak je to s kterým pokojem.
Ještě bychom mohli výpis zlepšit a zaplnit prázdná místa.
Celý dotaz se tím bohužel poněkud více zamotá.
Vrátíme se k předchozímu dotazu a pomocí CASE
jej vylepšíme tím, že pokud bude stav NULL
, místo toho se vypíše hodnota
:
SELECT p.pokoj_cislo AS cislo, p.pokoj_oznaceni AS oznaceni, p.pokoj_pocetluzek AS luzka, p.pokoj_pocetpristylek AS pristylky, den, CASE WHEN r.rezervace_druh IS NULL THEN 0 ELSE r.rezervace_druh END AS stav_cislo FROM pokoje AS p CROSS JOIN dnyvintervalu('2006-5-5','2006-5-7') AS den LEFT JOIN rezervace AS r ON r.rezervace_datum = den AND r.rezervace_pokoj=p.pokoj_cislo;
-------+----------+-------+-----------+------------+------------ cislo | oznaceni | luzka | pristylky | den | stav_cislo -------+----------+-------+-----------+------------+------------ 101 | p101 | 2 | 0 | 2006-05-05 | 1 101 | p101 | 2 | 0 | 2006-05-06 | 2 101 | p101 | 2 | 0 | 2006-05-07 | 3 102 | p102 | 3 | 0 | 2006-05-05 | 2 102 | p102 | 3 | 0 | 2006-05-06 | 2 102 | p102 | 3 | 0 | 2006-05-07 | 2 103 | p103 | 3 | 0 | 2006-05-05 | 0 103 | p103 | 3 | 0 | 2006-05-06 | 0 103 | p103 | 3 | 0 | 2006-05-07 | 0 104 | p104 | 2 | 0 | 2006-05-05 | 0 104 | p104 | 2 | 0 | 2006-05-06 | 0 104 | p104 | 2 | 0 | 2006-05-07 | 0 105 | p105 | 3 | 0 | 2006-05-05 | 1 105 | p105 | 3 | 0 | 2006-05-06 | 2 105 | p105 | 3 | 0 | 2006-05-07 | 2
No a teď konečně můžeme připojit výpis stavů a nahradit tak jeho číselnou reprezentaci reprezentací textovou. Bohužel nemůžeme tabulky spojit přímo, musíme na to jít oklikou přes závorky.
SELECT v.cislo, v.oznaceni, v.luzka, v.pristylky, v.den, po.polozka_nazev AS stav FROM (SELECT p.pokoj_cislo AS cislo, p.pokoj_oznaceni AS oznaceni, p.pokoj_pocetluzek AS luzka, p.pokoj_pocetpristylek AS pristylky, den, CASE WHEN r.rezervace_druh IS NULL THEN 0 ELSE r.rezervace_druh END AS stav_cislo FROM pokoje AS p CROSS JOIN dnyvintervalu('2006-5-5','2006-5-7') AS den LEFT JOIN rezervace AS r ON r.rezervace_datum = den AND r.rezervace_pokoj=p.pokoj_cislo) AS v INNER JOIN polozky AS po ON v.stav_cislo=po.polozka_cislo;
-------+----------+-------+-----------+------------+----------------- cislo | oznaceni | luzka | pristylky | den | stav -------+----------+-------+-----------+------------+----------------- 101 | p101 | 2 | 0 | 2006-05-05 | Počátek pobytu 101 | p101 | 2 | 0 | 2006-05-06 | Celodenní pobyt 101 | p101 | 2 | 0 | 2006-05-07 | Konec pobytu 102 | p102 | 3 | 0 | 2006-05-05 | Celodenní pobyt 102 | p102 | 3 | 0 | 2006-05-06 | Celodenní pobyt 102 | p102 | 3 | 0 | 2006-05-07 | Celodenní pobyt 103 | p103 | 3 | 0 | 2006-05-05 | Volný 103 | p103 | 3 | 0 | 2006-05-06 | Volný 103 | p103 | 3 | 0 | 2006-05-07 | Volný 104 | p104 | 2 | 0 | 2006-05-05 | Volný 104 | p104 | 2 | 0 | 2006-05-06 | Volný 104 | p104 | 2 | 0 | 2006-05-07 | Volný 105 | p105 | 3 | 0 | 2006-05-05 | Počátek pobytu 105 | p105 | 3 | 0 | 2006-05-06 | Celodenní pobyt 105 | p105 | 3 | 0 | 2006-05-07 | Celodenní pobyt
Nabízí se tu ještě možnost doplnění o aktuální ceník v závislosti na datumu pobytu, ale to si nechám na příště. Tento dotaz je už sám o sobě dost složitý.