Moderní SQL
Autor: Pavel Stěhule 2016
SQL dnes už umí více než spojování tabulek a poddotazy. Prostřednictvím analytických funkcí a rekurzivních dotazů se SQL dostává za rámec relačního modelu. To je schopnost, která některým teoretikům vadila, nám ale může často pomoci s efektivním řešením relativně běžných problémů.
Novější (cca 10 let staré) techniky se pokusím demonstrovat na mém oblíbeném příkladu - nalezení N největších prvků z M skupin. Například - tři nejlépe hodnocené produkty z každé kategorie, tři největší obce z každého okresu, .. Tato úloha je zajímavá v tom, že čistě relačním aparátem není jednoduše řešitelná (složitost je relativní - v každém případě není efektivně řešitelná). Jedná se ale o relativně častou úlohu, na které se lze mnohé naučit.
Testovací data
Všechny příklady testuji v databázi obce.
postgres=# \d obce View "public.obce" ┌────────────────┬───────────────────────┬───────────┐ │ Column │ Type │ Modifiers │ ╞════════════════╪═══════════════════════╪═══════════╡ │ id │ integer │ │ │ okres_id │ character varying(6) │ │ │ nazev │ character varying(40) │ │ │ pocet_obyvatel │ integer │ │ └────────────────┴───────────────────────┴───────────┘ postgres=# \sv obce CREATE OR REPLACE VIEW public.obce AS SELECT obce2.id, obce2.okres_id, obce2.nazev, obce2.pocet_muzu + obce2.pocet_zen AS pocet_obyvatel FROM obce2 postgres=# \d obce2 Table "public.obce2" ┌────────────┬───────────────────────┬───────────────────────────────────────────────────┐ │ Column │ Type │ Modifiers │ ╞════════════╪═══════════════════════╪═══════════════════════════════════════════════════╡ │ id │ integer │ not null default nextval('obce_id_seq'::regclass) │ │ okres_id │ character varying(6) │ │ │ nazev │ character varying(40) │ │ │ pocet_muzu │ integer │ │ │ pocet_zen │ integer │ │ │ vek_muzu │ numeric(3,1) │ │ │ vek_zen │ numeric(3,1) │ │ └────────────┴───────────────────────┴───────────────────────────────────────────────────┘ Indexes: "_obce_pkey" PRIMARY KEY, btree (id) "obce_okres_id_expr_idx" btree (okres_id, (pocet_muzu + pocet_zen) DESC) Foreign-key constraints: "obce_okres_id_fk" FOREIGN KEY (okres_id) REFERENCES okresy(id) postgres=# SELECT * FROM obce; ┌──────┬──────────┬───────────────────────────────────────┬────────────────┐ │ id │ okres_id │ nazev │ pocet_obyvatel │ ╞══════╪══════════╪═══════════════════════════════════════╪════════════════╡ │ 1 │ CZ0100 │ Praha │ 1249026 │ │ 2 │ CZ0201 │ Benešov │ 16382 │ │ 3 │ CZ0201 │ Bernartice │ 223 │ │ 4 │ CZ0201 │ Bílkovice │ 182 │ │ 5 │ CZ0201 │ Blažejovice │ 100 │ │ 6 │ CZ0201 │ Borovnice │ 76 │ │ 7 │ CZ0201 │ Bukovany │ 736 │ │ 8 │ CZ0201 │ Bystřice │ 4220 │ postgres=# \d okresy Table "public.okresy" ┌────────┬──────┬───────────┐ │ Column │ Type │ Modifiers │ ╞════════╪══════╪═══════════╡ │ id │ text │ not null │ │ nazev │ text │ │ └────────┴──────┴───────────┘ Indexes: "okresy_pkey" PRIMARY KEY, btree (id) "okresy_nuts_idx" UNIQUE, btree (id) Referenced by: TABLE "obce2" CONSTRAINT "obce_okres_id_fk" FOREIGN KEY (okres_id) REFERENCES okresy(id) postgres=# SELECT * FROM okresy; ┌────────┬─────────────────────┐ │ id │ nazev │ ╞════════╪═════════════════════╡ │ CZ0201 │ Benešov │ │ CZ0202 │ Beroun │ │ CZ0203 │ Kladno │ │ CZ0204 │ Kolín │ │ CZ0205 │ Kutná Hora │ │ CZ0206 │ Mělník │ │ CZ0207 │ Mladá Boleslav │ │ CZ0208 │ Nymburk │ │ CZ020B │ Příbram │ │ CZ020C │ Rakovník │ │ CZ0311 │ České Budějovice │ │ CZ0312 │ Český Krumlov │ │ CZ0313 │ Jindřichův Hradec │
Dotaz s korelovaným poddotazem s klauzulí LIMIT
Využití korelovaného dotazu s klauzulí LIMIT
je asi nejklasičtější řešení úlohy. Vazba mezi vnějším dotazem a
poddotazem umožňuje zafixovat jeden nebo více atributů v poddotazu a na takto omezené množině spočítat
agregační funkci nebo vzít prvních N hodnot:
SELECT * FROM obce ext WHERE pocet_obyvatel = (SELECT max(pocet_obyvatel) FROM obce o WHERE o.okres_id = ext.okres_id);
případně:
SELECT * FROM obce ext WHERE pocet_obyvatel IN (SELECT pocet_obyvatel FROM obce o WHERE o.okres_id = ext.okres_id ORDER BY pocet_obyvatel DESC LIMIT 3);
Tyto dotazy PostgreSQL řeší pomocí nested loopu (cyklu), kdy pro každý řádek vnějšího dotazu se provede vnitřní dotaz. V tomto případě to znamená 6200 dotazů do indexu.
postgres=# EXPLAIN ANALYZE SELECT * FROM obce ext WHERE (okres_id,pocet_obyvatel) IN (SELECT o.okres_id, pocet_obyvatel FROM obce o WHERE o.okres_id = ext.okres_id ORDER BY pocet_obyvatel DESC LIMIT 3); ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ QUERY PLAN │ ╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ Seq Scan on obce2 (cost=0.00..12977.53 rows=3125 width=25) (actual time=0.091..111.277 rows=227 loops=1) │ │ Filter: (SubPlan 1) │ │ Rows Removed by Filter: 6023 │ │ SubPlan 1 │ │ -> Limit (cost=0.28..3.81 rows=3 width=11) (actual time=0.013..0.015 rows=3 loops=6250) │ │ -> Index Scan using obce_okres_id_expr_idx on obce2 obce2_1 (cost=0.28..95.43 rows=81 width=11) (actual time=0.013..0.014 rows=3 loops=6250) │ │ Index Cond: ((okres_id)::text = (obce2.okres_id)::text) │ │ Planning time: 0.646 ms │ │ Execution time: 111.387 ms │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
V tomto případě to není kdoví jak tragické, ale jsou tu i další (a lepší nebo horší možnosti).
Při hledání maxim ze skupiny můžeme použít více hodnotové porovnání a vyhnout se tak korelovanému poddotazu:
SELECT * FROM obce WHERE (okres_id, pocet_obyvatel) IN (SELECT okres_id, max(pocet_obyvatel) FROM obce GROUP BY okres_id); postgres=# EXPLAIN ANALYZE SELECT * FROM obce WHERE (okres_id, pocet_obyvatel) IN (SELECT okres_id, max(pocet_obyvatel) FROM obce GROUP BY okres_id); ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ QUERY PLAN │ ╞═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ Hash Semi Join (cost=171.07..329.88 rows=234 width=25) (actual time=8.336..13.275 rows=77 loops=1) │ │ Hash Cond: (((obce2.okres_id)::text = (obce2_1.okres_id)::text) AND ((obce2.pocet_muzu + obce2.pocet_zen) = (max((obce2_1.pocet_muzu + obce2_1.pocet_zen))))) │ │ -> Seq Scan on obce2 (cost=0.00..121.50 rows=6250 width=29) (actual time=0.020..1.421 rows=6250 loops=1) │ │ -> Hash (cost=169.92..169.92 rows=77 width=11) (actual time=8.291..8.291 rows=77 loops=1) │ │ Buckets: 1024 Batches: 1 Memory Usage: 12kB │ │ -> HashAggregate (cost=168.38..169.15 rows=77 width=11) (actual time=8.202..8.246 rows=77 loops=1) │ │ Group Key: obce2_1.okres_id │ │ -> Seq Scan on obce2 obce2_1 (cost=0.00..121.50 rows=6250 width=15) (actual time=0.005..1.617 rows=6250 loops=1) │ │ Planning time: 1.265 ms │ │ Execution time: 13.412 ms │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
versus
postgres=# EXPLAIN ANALYZE SELECT * FROM obce ext WHERE pocet_obyvatel = (SELECT max(pocet_obyvatel) FROM obce o WHERE o.okres_id = ext.okres_id); ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ QUERY PLAN │ ╞═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ Seq Scan on obce2 (cost=0.00..9338.10 rows=31 width=25) (actual time=0.094..113.746 rows=77 loops=1) │ │ Filter: ((pocet_muzu + pocet_zen) = (SubPlan 2)) │ │ Rows Removed by Filter: 6173 │ │ SubPlan 2 │ │ -> Result (cost=1.46..1.47 rows=1 width=4) (actual time=0.017..0.017 rows=1 loops=6250) │ │ InitPlan 1 (returns $1) │ │ -> Limit (cost=0.28..1.46 rows=1 width=4) (actual time=0.016..0.016 rows=1 loops=6250) │ │ -> Index Scan using obce_okres_id_expr_idx on obce2 obce2_1 (cost=0.28..95.63 rows=81 width=4) (actual time=0.015..0.015 rows=1 loops=6250) │ │ Index Cond: (((okres_id)::text = (obce2.okres_id)::text) AND ((pocet_muzu + pocet_zen) IS NOT NULL)) │ │ Planning time: 0.837 ms │ │ Execution time: 113.834 ms │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Je mnohem efektivnější jednou spočítat agregaci, a pak join, než 6200x hledat maximum.
Bohužel pro hledání N prvků ze skupiny nic podobného nemáme (pokud neznáme window funkce, o kterých se zmíním později). Aby výpočet byl efektivní, tak bychom potřebovali
iterovat přes okresy, jelikož okresy mají cca 100x méně řádků. Starší SQL nám ale nic takového nedovolí. V novějším SQL máme LATERAL
join. Omezení, které mne nyní
tlačí, je omezení na tvar výsledku korelovaného poddotazů - výsledkem může být pouze jedna hodnota:
postgres=# SELECT *, (SELECT * FROM obce WHERE okresy.id = okres_id ORDER BY pocet_obyvatel DESC LIMIT 3) FROM okresy; ERROR: subquery must return only one column LINE 1: SELECT *, (SELECT * FROM obce WHERE okresy.id = okres_id ORD... ^
V PostgreSQL mohu toto omezení obejít pomocí pole kompozitních hodnot. Poznámka - V Postgresu můžete použít sloupec stejného jména jako má tabulka ve smyslu kompozitní hodnoty složené ze všech sloupců tabulky. Následující dotaz projde bez chyby:
SELECT *, ARRAY(SELECT obce FROM obce WHERE okresy.id = okres_id ORDER BY pocet_obyvatel DESC LIMIT 3) FROM okresy; ┌────────┬─────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ id │ nazev │ array │ ╞════════╪═════════════════════╪══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ CZ0201 │ Benešov │ {"(2,CZ0201,Benešov,16382)","(104,CZ0201,Vlašim,12024)","(101,CZ0201,\"Týnec nad Sázavou\",5463)"} │ │ CZ0202 │ Beroun │ {"(117,CZ0202,Beroun,18616)","(145,CZ0202,\"Králův Dvůr\",6816)","(128,CZ0202,Hořovice,6751)"} │ │ CZ0203 │ Kladno │ {"(234,CZ0203,Kladno,69938)","(270,CZ0203,Slaný,15091)","(274,CZ0203,Stochov,5847)"} │ │ CZ0204 │ Kolín │ {"(326,CZ0204,Kolín,30935)","(310,CZ0204,\"Český Brod\",6842)","(348,CZ0204,Pečky,4501)"} │ │ CZ0205 │ Kutná Hora │ {"(423,CZ0205,\"Kutná Hora\",21425)","(399,CZ0205,Čáslav,10120)","(475,CZ0205,\"Zruč nad Sázavou\",4962)"} │ │ CZ0206 │ Mělník │ {"(515,CZ0206,Mělník,19173)","(504,CZ0206,\"Kralupy nad Vltavou\",17635)","(520,CZ0206,Neratovice,16494)"} │ │ CZ0207 │ Mladá Boleslav │ {"(613,CZ0207,\"Mladá Boleslav\",44750)","(614,CZ0207,\"Mnichovo Hradiště\",8507)","(550,CZ0207,\"Benátky nad Jizerou\",7362)"} │ │ CZ0208 │ Nymburk │ {"(716,CZ0208,Nymburk,14568)","(727,CZ0208,Poděbrady,13838)","(713,CZ0208,Milovice,9145)"} │
Nicméně výsledek je ve tvaru, který uživatelé nedocení. S použitím funkce unnest
a trochou magie (přístup k atributu kompozitní hodnoty) se dostanu k výsledku:
SELECT nazev, (o).nazev, (o).pocet_obyvatel FROM (SELECT nazev, unnest(ARRAY(SELECT obce FROM obce WHERE okresy.id = okres_id ORDER BY pocet_obyvatel DESC LIMIT 3)) AS o FROM okresy) s; postgres=# EXPLAIN ANALYZE SELECT nazev, (o).nazev, (o).pocet_obyvatel FROM (SELECT nazev, unnest(ARRAY(SELECT obce FROM obce WHERE okresy.id = okres_id ORDER BY pocet_obyvatel DESC LIMIT 3)) AS o FROM okresy) s; ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ QUERY PLAN │ ╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ Subquery Scan on s (cost=0.00..410.75 rows=7700 width=112) (actual time=0.132..3.649 rows=227 loops=1) │ │ -> Seq Scan on okresy (cost=0.00..333.75 rows=7700 width=42) (actual time=0.125..3.424 rows=227 loops=1) │ │ SubPlan 1 │ │ -> Limit (cost=0.28..3.81 rows=3 width=36) (actual time=0.027..0.033 rows=3 loops=77) │ │ -> Index Scan using obce_okres_id_expr_idx on obce2 (cost=0.28..95.63 rows=81 width=36) (actual time=0.025..0.030 rows=3 loops=77) │ │ Index Cond: (okresy.id = (okres_id)::text) │ │ Planning time: 0.671 ms │ │ Execution time: 3.779 ms │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Dotaz je fantasticky rychlý, ale cesta k němu je relativně obtížná. Vyžaduje trochu odvahy a znalostí tmavých zákoutí Postgresu.
Dotaz s window funkcí
I když jsou window funkce relativně dlouho ve standardu, a už pár let i v Postgresu (PostgreSQL 8.4 - rok 2009), tak nepatří mezi často používané. Což je škoda, protože díky
nim mohou být dotazy velmi názorné a většinou i docela rychlé. Window funkce jsou funkce, které se počítají nad množinou (podobně jako agregační
funkce), ale na rozdíl od agregačních funkcí může být hodnota window funkce pro každý prvek množiny jiná (u agregačních funkcí je výsledek společný
pro všechny prvky množiny). Je tu několik paralel s agregačními funkcemi, ale možná ještě více rozdílů. Window funkce neredukují počet řádků, a také
pro window funkce nelze použít klauzuli HAVING
.
Mou oblíbenou window funkcí je funkce rank()
, která vrací pořadí hodnoty v množině:
postgres=# SELECT *, rank() OVER (PARTITION BY okres_id ORDER BY pocet_obyvatel DESC) FROM obce; ┌──────┬──────────┬───────────────────────────────────────┬────────────────┬──────┐ │ id │ okres_id │ nazev │ pocet_obyvatel │ rank │ ╞══════╪══════════╪═══════════════════════════════════════╪════════════════╪══════╡ │ 1 │ CZ0100 │ Praha │ 1249026 │ 1 │ │ 918 │ CZ020A │ Roztoky │ 7538 │ 1 │ │ 882 │ CZ020A │ Hostivice │ 7275 │ 2 │ │ 890 │ CZ020A │ Jesenice │ 6284 │ 3 │ │ 869 │ CZ020A │ Černošice │ 6179 │ 4 │ │ 919 │ CZ020A │ Rudná │ 4494 │ 5 │ │ 906 │ CZ020A │ Mníšek pod Brdy │ 4442 │ 6 │
Pro filtrování musím použít derivovaný poddotaz - tj SELECT FROM SELECT
SELECT * FROM (SELECT *, rank() OVER (PARTITION BY okres_id ORDER BY pocet_obyvatel DESC) FROM obce) s WHERE rank <= 3; ┌──────┬──────────┬──────────────────────────────────┬────────────────┬──────┐ │ id │ okres_id │ nazev │ pocet_obyvatel │ rank │ ╞══════╪══════════╪══════════════════════════════════╪════════════════╪══════╡ │ 1 │ CZ0100 │ Praha │ 1249026 │ 1 │ │ 918 │ CZ020A │ Roztoky │ 7538 │ 1 │ │ 882 │ CZ020A │ Hostivice │ 7275 │ 2 │ │ 890 │ CZ020A │ Jesenice │ 6284 │ 3 │ │ 1025 │ CZ020B │ Příbram │ 34217 │ 1 │ │ 958 │ CZ020B │ Dobříš │ 8464 │ 2 │ │ 1033 │ CZ020B │ Sedlčany │ 7643 │ 3 │ │ 1120 │ CZ020C │ Rakovník │ 16503 │ 1 │ │ 1106 │ CZ020C │ Nové Strašecí │ 5198 │ 2 │ │ 1096 │ CZ020C │ Lužná │ 1802 │ 3 │ │ 2 │ CZ0201 │ Benešov │ 16382 │ 1 │ │ 104 │ CZ0201 │ Vlašim │ 12024 │ 2 │ │ 101 │ CZ0201 │ Týnec nad Sázavou │ 5463 │ 3 │ │ 117 │ CZ0202 │ Beroun │ 18616 │ 1 │ postgres=# EXPLAIN ANALYZE SELECT * FROM (SELECT *, rank() OVER (PARTITION BY okres_id ORDER BY pocet_obyvatel DESC) FROM obce) s WHERE rank <= 3; ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ QUERY PLAN │ ╞═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ Subquery Scan on s (cost=0.28..556.29 rows=2083 width=33) (actual time=0.038..18.265 rows=227 loops=1) │ │ Filter: (s.rank <= 3) │ │ Rows Removed by Filter: 6023 │ │ -> WindowAgg (cost=0.28..478.17 rows=6250 width=33) (actual time=0.035..16.824 rows=6250 loops=1) │ │ -> Index Scan using obce_okres_id_expr_idx on obce2 (cost=0.28..353.17 rows=6250 width=25) (actual time=0.027..6.304 rows=6250 loops=1) │ │ Planning time: 0.242 ms │ │ Execution time: 18.342 ms │ └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Výsledek je docela rychlý (i když nikoliv maximálně rychlý, PostgreSQL nedokáže včas zaříznout výpočet window funkce) na to, jak jednoduše jsem se k němu dostal. S window funkcemi si lze hrát dlouho - můžeme počítat klouzavé agregační funkce, můžeme počítat kumulativní funkce. Zájemnce o podrobnější studium odkáži na <a href="https://www.postgresql.org/docs/9.5/static/tutorial-window.html">dokumentaci</a>.
Common Table Expression
Podporou rekurzivních dotazů SQL vybočuje z relačního modelu (v PostgreSQL 8.4). Nicméně jakmile máme rekurzi v datech (organizační struktura, kusovník), tak se samozřejmě hodí. Začíná se výpočtem nulté množiny. Vypočtu spojení této množiny a množiny nad kterou rekurzivní dotaz počítám. Pokud výsledek není prázdný, tak jej použiji znovu pro spojení s primární množinou. Výsledkem je sjednocení všech výsledků.
Chvíli mi trvalo, než jsem přišel na způsob, jak CTE vnutit do této úlohy (také odladění dotazu trvalo nejdéle)- pro výpočet N největších hodnot z M skupin se CTE vůbec nehodí. Tato úloha je ale perfektní pro procvičení a naučení CTE:
Nultou množinou budou největší obce z okresů. V CTE nelze použít klauzule ORDER BY
a LIMIT
- je proto nutné spočítat následující
menší obci jiným způsobem (v poli si udržuji seznam největších obcí, které pak vylučuji z výpočtu agregace):
WITH RECURSIVE x AS (-- hledám největší obce z okresů SELECT *, ARRAY[id] AS ids FROM obce WHERE (okres_id, pocet_obyvatel) IN (SELECT okres_id, max(pocet_obyvatel) FROM obce GROUP BY okres_id) UNION ALL -- hledám následující menší obce z okresů SELECT o.*, o.id || ids FROM obce o JOIN x ON x.okres_id = o.okres_id AND o.id <> ALL(x.ids) AND cardinality(x.ids) < 3 AND o.pocet_obyvatel = (SELECT max(pocet_obyvatel) FROM obce o2 WHERE o2.okres_id = x.okres_id AND o2.id <> ALL(x.ids)) ) SELECT * FROM x;
K mému překvapení je tento dotaz docela rychlý:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ QUERY PLAN │ ╞═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ CTE Scan on x (cost=18418.28..18425.36 rows=354 width=166) (actual time=8.111..20.104 rows=227 loops=1) │ │ CTE x │ │ -> Recursive Union (cost=171.07..18418.28 rows=354 width=57) (actual time=8.106..19.753 rows=227 loops=1) │ │ -> Hash Semi Join (cost=171.07..329.88 rows=234 width=57) (actual time=8.103..12.652 rows=77 loops=1) │ │ Hash Cond: (((obce2_1.okres_id)::text = (obce2_2.okres_id)::text) AND ((obce2_1.pocet_muzu + obce2_1.pocet_zen) = (max((obce2_2.pocet_muzu + obce2_2.pocet_zen))))) │ │ -> Seq Scan on obce2 obce2_1 (cost=0.00..121.50 rows=6250 width=29) (actual time=0.019..1.289 rows=6250 loops=1) │ │ -> Hash (cost=169.92..169.92 rows=77 width=11) (actual time=8.055..8.055 rows=77 loops=1) │ │ Buckets: 1024 Batches: 1 Memory Usage: 12kB │ │ -> HashAggregate (cost=168.38..169.15 rows=77 width=11) (actual time=7.977..8.019 rows=77 loops=1) │ │ Group Key: obce2_2.okres_id │ │ -> Seq Scan on obce2 obce2_2 (cost=0.00..121.50 rows=6250 width=15) (actual time=0.005..1.529 rows=6250 loops=1) │ │ -> Nested Loop (cost=1.76..1808.13 rows=12 width=57) (actual time=0.081..2.262 rows=50 loops=3) │ │ -> WorkTable Scan on x x_1 (cost=0.00..58.50 rows=780 width=60) (actual time=0.011..0.068 rows=51 loops=3) │ │ Filter: (cardinality(ids) < 3) │ │ Rows Removed by Filter: 25 │ │ -> Index Scan using obce_expr_okres_id_idx on obce2 obce2_3 (cost=1.76..2.23 rows=1 width=29) (actual time=0.006..0.007 rows=1 loops=152) │ │ Index Cond: (((pocet_muzu + pocet_zen) = (SubPlan 2)) AND ((okres_id)::text = (x_1.okres_id)::text)) │ │ Filter: (id <> ALL (x_1.ids)) │ │ SubPlan 2 │ │ -> Result (cost=1.47..1.48 rows=1 width=4) (actual time=0.028..0.029 rows=1 loops=152) │ │ InitPlan 1 (returns $3) │ │ -> Limit (cost=0.28..1.47 rows=1 width=4) (actual time=0.026..0.027 rows=1 loops=152) │ │ -> Index Scan using obce_okres_id_expr_idx on obce2 (cost=0.28..96.64 rows=81 width=4) (actual time=0.025..0.025 rows=1 loops=152) │ │ Index Cond: (((okres_id)::text = (x_1.okres_id)::text) AND ((pocet_muzu + pocet_zen) IS NOT NULL)) │ │ Filter: (id <> ALL (x_1.ids)) │ │ Rows Removed by Filter: 1 │ │ SubPlan 2 │ │ -> Result (cost=1.47..1.48 rows=1 width=4) (actual time=0.028..0.029 rows=1 loops=152) │ │ InitPlan 1 (returns $3) │ │ -> Limit (cost=0.28..1.47 rows=1 width=4) (actual time=0.026..0.027 rows=1 loops=152) │ │ -> Index Scan using obce_okres_id_expr_idx on obce2 (cost=0.28..96.64 rows=81 width=4) (actual time=0.025..0.025 rows=1 loops=152) │ │ Index Cond: (((okres_id)::text = (x_1.okres_id)::text) AND ((pocet_muzu + pocet_zen) IS NOT NULL)) │ │ Filter: (id <> ALL (x_1.ids)) │ │ Rows Removed by Filter: 1 │ │ Planning time: 2.883 ms │ │ Execution time: 20.430 ms │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ovšem snadno se zde udělá chyba - docela obtížně se tyto dotazy ladí, a stačí trochu jinak referencovat a při stejném správném výsledku mít 10x pomalejší dotaz.
Dotaz s LATERAL
joinem
LATERAL
join je v PostgreSQL relativně horkou novinkou (PostgreSQL 9.3 - rok 2013). Umožňuje elegantně (a standardní cestou) vyřešit problémy, které jsem měl při psaní
dotazu s korelovaným poddotazem, když jsem chtěl iterovat přes okresy. Při LATERAL
spojení se pro každý řádek levé relace provede
poddotaz, jehož výsledek se páruje s řádkem, pro který se poddotaz počítal. Je to korelované (vázané) spojení relací.
SELECT * FROM okresy, LATERAL (SELECT * FROM obce WHERE okresy.id = obce.okres_id ORDER BY pocet_obyvatel DESC LIMIT 3) s; ┌────────┬─────────────────────┬──────┬──────────┬──────────────────────────────────┬────────────────┐ │ id │ nazev │ id │ okres_id │ nazev │ pocet_obyvatel │ ╞════════╪═════════════════════╪══════╪══════════╪══════════════════════════════════╪════════════════╡ │ CZ0201 │ Benešov │ 2 │ CZ0201 │ Benešov │ 16382 │ │ CZ0201 │ Benešov │ 104 │ CZ0201 │ Vlašim │ 12024 │ │ CZ0201 │ Benešov │ 101 │ CZ0201 │ Týnec nad Sázavou │ 5463 │ │ CZ0202 │ Beroun │ 117 │ CZ0202 │ Beroun │ 18616 │ │ CZ0202 │ Beroun │ 145 │ CZ0202 │ Králův Dvůr │ 6816 │ │ CZ0202 │ Beroun │ 128 │ CZ0202 │ Hořovice │ 6751 │ │ CZ0203 │ Kladno │ 234 │ CZ0203 │ Kladno │ 69938 │ │ CZ0203 │ Kladno │ 270 │ CZ0203 │ Slaný │ 15091 │ │ CZ0203 │ Kladno │ 274 │ CZ0203 │ Stochov │ 5847 │ │ CZ0204 │ Kolín │ 326 │ CZ0204 │ Kolín │ 30935 │ │ CZ0204 │ Kolín │ 310 │ CZ0204 │ Český Brod │ 6842 │
Dotaz je velice rychlý:
postgres=# EXPLAIN ANALYZE SELECT * FROM okresy, LATERAL (SELECT * FROM obce WHERE okresy.id = obce.okres_id ORDER BY pocet_obyvatel DESC LIMIT 3) s; ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ QUERY PLAN │ ╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ Nested Loop (cost=0.28..299.49 rows=231 width=42) (actual time=0.086..2.346 rows=227 loops=1) │ │ -> Seq Scan on okresy (cost=0.00..1.77 rows=77 width=17) (actual time=0.019..0.047 rows=77 loops=1) │ │ -> Limit (cost=0.28..3.81 rows=3 width=25) (actual time=0.023..0.027 rows=3 loops=77) │ │ -> Index Scan using obce_okres_id_expr_idx on obce2 (cost=0.28..95.43 rows=81 width=25) (actual time=0.022..0.025 rows=3 loops=77) │ │ Index Cond: (okresy.id = (okres_id)::text) │ │ Planning time: 0.659 ms │ │ Execution time: 2.459 ms │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Navíc zápis je velmi expresivní, jednoduchý (téměř zde nelze udělat chybu).
Závěr
Na pár příkladech jsem se snažil demonstrovat výhody používání novějších funkcí v SQL (novějších než ANSI SQL 92).
Pokud by mi šlo o názornost a jednoduchost, tak bych patrně silně preferoval použití window funkcí. Pokud
bych musel řešit výkon, nebo kladl důraz na výkon, pak v řadě případů mohu použít LATERAL
join.