V předchozím příspěvku na blogu My Favorite PostgreSQL Queries and Why They Matter jsem navštívil zajímavé dotazy, které jsou pro mě smysluplné, když se učím, rozvíjím a vyrůstám v roli vývojáře SQL.
Jedna z nich, konkrétně víceřádková AKTUALIZACE s jediným výrazem CASE, vyvolala zajímavou konverzaci na Hacker News.
V tomto blogovém příspěvku chci pozorovat srovnání mezi tímto konkrétním dotazem a dotazem zahrnujícím několik jednoduchých příkazů UPDATE. Pro dobro nebo zmar.
Specifikace stroje/prostředí:
- CPU Intel(R) Core(TM) i5-6200U @ 2,30 GHz
- 8 GB RAM
- 1TB úložiště
- Xubuntu Linux 16.04.3 LTS (Xenial Xerus)
- PostgreSQL 10.4
Poznámka:Pro začátek jsem vytvořil 'pracovní' tabulku se všemi sloupci typu TEXT, abych načetl data.
Ukázkovou datovou sadu, kterou používám, naleznete na tomto odkazu zde.
Ale mějte na paměti, že v tomto příkladu jsou použita samotná data, protože jde o slušnou sadu s více sloupci. Jakákoli „analýza“ nebo AKTUALIZACE/VLOŽENÍ tohoto souboru dat neodráží skutečné operace GPS/GIS v „reálném světě“ a nejsou tak zamýšleny.
location=# \d data_staging; Tabulka "public.data_staging" Sloupec | Typ | Porovnání | S možností null | Výchozí ---------------+---------+-----------+----------+ --------- segment_num | text | | | point_seg_num | text | | | zeměpisná šířka | text | | | zeměpisná délka | text | | | nad_rok_cd | text | | | kód_proj | text | | | x_cord_loc | text | | | y_cord_loc | text | | | poslední_rev_datum | text | | | verze_datum | text | | | asbuilt_flag | text | | | location=# SELECT COUNT(*) FROM data_staging;count--------546895(1 řádek)
V této tabulce máme asi půl milionu řádků dat.
Pro toto první srovnání budu AKTUALIZOVAT sloupec proj_code.
Zde je průzkumný dotaz k určení jeho aktuálních hodnot:
location=# SELECT DISTINCT proj_code FROM data_staging;proj_code-----------"70""""72""71""51""15""16"(7 řádků )
Použiji trim k odstranění uvozovek z hodnot a přetypování na INT a určení, kolik řádků existuje pro každou jednotlivou hodnotu:
Použijme k tomu CTE a poté z něj VYBERTE:
location=# WITH clean_nums AS (SELECT NULLIF(trim(oba '"' FROM proj_code), '') AS p_code FROM data_staging)SELECT COUNT(*),CASEWHEN p_code::int =70 THEN '70 'KDYŽ p_code::int =72 POTOM '72'KDYŽ p_code::int =71 POTOM '71'KDYŽ p_code::int =51, PAK '51'KDYŽ p_code::int =15 POTOM '15'KDYŽ p_code::int =16 THEN '16'ELSE '00'END AS proj_code_numFROM cleaner_numsGROUP BY p_codeORDER BY p_code DESC;count | proj_code_num--------+---------------353087 | 013905 | 7225460 | 713254 | 701 | 5112648 | 1613388 | 15 (7 řádků)
Před spuštěním těchto testů budu pokračovat a ALTER sloupec proj_code zadejte INTEGER:
BEGIN;ALTER TABLE data_staging ALTER COLUMN proj_code NASTAVIT DATOVÝ TYP CELÉ ČÍSLO POMOCÍ NULLIF(trim(oba '"' FROM proj_code), '')::INTEGER;SAVEPOINT my_save;COMMIT;
A vyčistěte tuto hodnotu sloupce NULL (kterou představuje ELSE '00' ve výše uvedeném průzkumném výrazu CASE) a nastavte ji na libovolné číslo, 10, pomocí této AKTUALIZACE:
UPDATE data_stagingSET proj_code =10WHERE proj_code IS NULL;
Nyní mají všechny sloupce proj_code hodnotu INTEGER.
Pojďme do toho a spustíme jeden výraz CASE, který aktualizuje všechny hodnoty sloupce proj_code a uvidíme, co hlásí časování. Umístím všechny příkazy do zdrojového souboru .sql pro snadnou manipulaci.
Zde je obsah souboru:
BEGIN;\timing onUPDATE data_stagingSET proj_code =(CASE proj_codeWHEN 72 THEN 7272WHEN 71 THEN 7171WHEN 15 THEN 1515WHEN 51 THEN 5151WHEN 70 THEN 6HE2THRE 70170 THEN 71070 WHN , 70, 10, 16);SAVEPOINT my_save;
Spusťte tento soubor a zkontrolujte, co hlásí časování:
location=# \i /case_insert.sqlBEGINČas:0,265 msNačasování je zapnuto.UPDATE 546895Čas:6779,596 ms (00:06,780)SAVEPOINTČas:0,300 ms
Něco málo přes půl milionu řádků za více než 6 sekund.
Zde jsou dosavadní změny v tabulce:
location=# SELECT DISTINCT proj_code FROM data_staging;proj_code-----------7070161610107171151572725151(7 řádků)
Tyto změny ROLLBACK (nezobrazeno), abych je mohl také otestovat spouštěním jednotlivých příkazů INSERT.
Níže jsou uvedeny úpravy zdrojového souboru .sql pro tuto sérii srovnání:
BEGIN;\timing onUPDATE data_stagingSET proj_code =7222WHERE proj_code =72;UPDATE data_stagingSET proj_code =7171WHERE proj_code =71;AKTUALIZACE data_staging SET proj_code =1515WHERE proj_staging SET proj_code;DATE5_UPDATE_code_UPJaging_code1_update_code_staging datast 51_st 7070WHERE proj_code =70;UPDATE data_stagingSET proj_code =1010WHERE proj_code =10;UPDATE data_stagingSET proj_code =1616WHERE proj_code =16;SAVEPOINT my_save;
A ty výsledky,
location =# \ i /case_insert.sqlbegintime:0,264 mstiming je on.update 139057time:795,610 msupdate 25460time:116.268 msupdate 13388time (239,007 msUpdate 1time (msUpdate 1time (msUpdate 1time:msUpdate 1time:19971 msUpdate 1time:msUpdate 1time:72,699 msUpdate 3254time:)UPDATE 12648Čas:321,223 msSAVEPOINTČas:0,108 ms
Zkontrolujeme hodnoty:
location=# SELECT DISTINCT proj_code FROM data_staging;proj_code-----------7222161670701010717115155151(7 řádků)
A načasování (Poznámka:Udělám to spočítat v dotazu, protože \timing tento běh nehlásil celé sekundy):
location=# SELECT round((795,610 + 116,268 + 239,007 + 72,699 + 162,199 + 1987,857 + 321,223) / 1000, 3) AS sekund;sekundy---3-91---
Jednotlivé INSERTy zabraly asi polovinu času než jeden CASE.
Tento první test zahrnoval celou tabulku se všemi sloupci. Jsem zvědavý na nějaké rozdíly v tabulce se stejným počtem řádků, ale méně sloupců, proto další série testů.
Vytvořím tabulku se 2 sloupci (složenou z datového typu SERIAL pro PRIMARY KEY a INTEGER pro sloupec proj_code) a přesunu se přes data:
location=# CREATE TABLE proj_nums(n_id SÉRIOVÝ PRIMÁRNÍ KLÍČ, proj_code INTEGER);CREATE TABLElocation=# INSERT INTO proj_nums(proj_code) SELECT proj_code FROM data_staging;INSERT 0 546895
(Poznámka:Příkazy SQL z první sady operací se používají s příslušnými úpravami. Z důvodu stručnosti a zobrazení na obrazovce je zde vynechávám )
Nejprve spustím výraz single CASE:
location=# \i /case_insert.sqlBEGINČasování je zapnuto.UPDATE 546895Čas:4355,332 ms (00:04,355)SAVEPOINTČas:0,137 ms
A pak jednotlivé AKTUALIZACE:
location =# \ i /case_insert.sqlbegintime:0,282 mstiming je on.update 139057time:1042.133 ms (00:01.042) Aktualizace 25460time:123,337 MSUPDATE 13388time:212.698 MSUPDATE 1Time:43,107:43,107:43,107:43,107:43,107:43,107:43,107:43,107. 2787,295 ms (00:02,787)AKTUALIZACE 12648Čas:99,813 msSAVEPOINTČas:0,059 mslocation=# SELECT kolo((1042,133 + 123,337 + 212,6107 + 49,09 + 09,629 + 43,029 ---4,361(1 řádek)
Načasování je poněkud rovnoměrné mezi oběma sadami operací v tabulce s pouhými 2 sloupci.
Řeknu, že použití výrazu CASE je o něco snazší na psaní, ale ne nutně nejlepší volbou pro všechny příležitosti. Stejně jako to, co bylo uvedeno v některých komentářích k výše uvedenému vláknu Hacker News, to normálně „závisí“ na mnoha faktorech, které mohou nebo nemusí být optimální volbou.
Uvědomuji si, že tyto testy jsou přinejlepším subjektivní. Jeden z nich byl na tabulce s 11 sloupci, zatímco druhý měl pouze 2 sloupce, přičemž oba byly datového typu number.
Výraz CASE pro aktualizace více řádků je stále jedním z mých oblíbených dotazů, i když jen pro snadné psaní v kontrolovaném prostředí, kde je druhou alternativou mnoho jednotlivých UPDATE dotazů.
Nyní však vidím, kde to není vždy optimální volba, protože stále rostu a učím se.
Jak říká staré přísloví:„Půl tuctu v jedné ruce, 6 ve druhé ."
Další oblíbený dotaz – pomocí PLpgSQL CURSOR's
Začal jsem ukládat a sledovat všechny své statistiky cvičení (trail walking) pomocí PostgreSQL na svém místním vývojovém stroji. Zahrnuje více tabulek, jako u každé normalizované databáze.
Na konci měsíce však chci uložit statistiky konkrétních sloupců do jejich vlastní samostatné tabulky.
Zde je „měsíční“ tabulka, kterou použiji:
fitness=> \d turistický_měsíc_celkem; Tabulka "public.hiking_month_total" Sloupec | Typ | Porovnání | S možností null | Výchozí -----------------+------------------------+------- -----+----------+--------- day_hiked | datum | | | spálené kalorie | číselný(4,1) | | | míle | číselný(4,2) | | | trvání | čas bez časového pásma | | | tempo | číselný(2,1) | | | trail_hiked | text | | | boty_nošené | text | | |
Zaměřím se na květnové výsledky pomocí tohoto SELECT dotazu:
fitness=> VYBERTE hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brandfitness-> FROM hiking_stats JAKO hsfitness-> VNITŘNÍ PŘIPOJTE se k turistické_stezce JAKO htfitness -> ON hs.hike_id =ht.th_idfitness-> INNER JOIN trail_route AS trfitness-> ON ht.tr_id =tr.trail_idfitness-> INNER JOIN shoe_brand AS sbfitness-> ON hs.shoe_id =sb.shoe_idfitness extrakt-> WHERE FROM hs.day_walked) =5 fitness-> ORDER BY hs.day_walked ASC;
A zde jsou 3 ukázkové řádky vrácené z tohoto dotazu:
day_walked | cal_burned | míle_chůze | trvání | mph | jméno | název_značky------------+------------+--------------+-------- --+-----+------------------------+---------------- ------------------------2018-05-02 | 311,2 | 3,27 | 00:57:13 | 3,4 | Stromová stezka prodloužená | New Balance Trail Runners-All Terrain2018-05-03 | 320,8 | 3,38 | 00:58:59 | 3,4 | Sandy Trail-Drive | New Balance Trail Runners-All Terrain2018-05-04 | 291,3 | 3,01 | 00:53:33 | 3,4 | Trasa domovního elektrického vedení | Keen Koven WP(keen-dry)(3 řádky)
Po pravdě řečeno, mohu naplnit cílovou tabulku hiking_month_total pomocí výše uvedeného dotazu SELECT v příkazu INSERT.
Ale kde je v tom zábava?
Odpustím si nudu pro funkci PLpgSQL s CURSORem.
Přišel jsem s touto funkcí, abych provedl INSERT s KURZOREM:
CREATE OR REPLACE function month_total_stats()RETURNS voidAS $month_stats$DECLAREv_day_walked date;v_cal_burned numeric(4, 1);v_miles_walked numeric(4, 2);v_duration time bez časového pásma;v_mph numeric(2, 1);v_name text;v_name_brand text;v_cur CURSOR for SELECT hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brandFROM hiking_stats JAKO hsINNER JOIN Turistická_stezkahi_htON h. .th_idINNER JOIN trail_route AS trON ht.tr_id =tr.trail_idINNER JOIN shoe_brand AS sbON hs.shoe_id =sb.shoe_idWHERE extrakt (měsíc OD hs.day_walked) =5ORDER BYLOOPcurstat_FCH;BEGINOP INTO v_day_walked, v_cal_burned, v_miles_walked, v_duration, v_mph, v_name, v_name_brand;EXIT WHEN WEN NOT FOUND;INSERT INTO hiking_month_total(day_hiked,calorie_burned, miles,duration,tempo, trail_hiked, shoes,ph_day_worn)VALUES(v_walk_m_day_worn)VALUES(v_walk_m_day_worn)VALUES v_name, v_name_brand);END LOOP get_stats;CLOSE v_cur; END;$month_stats$ LANGUAGE PLpgSQL;
Zavolejte funkci month_total_stats(), aby provedla INSERT:
fitness=> SELECT month_total_stats();monthly_total_stats---------------------(1 řádek)
Protože je funkce definována RETURNS void, vidíme, že se volajícímu nevrací žádná hodnota.
V tuto chvíli mě konkrétně nezajímají žádné návratové hodnoty,
pouze to, že funkce provede definovanou operaci a vyplní tabulku turistické_měsíc_celkem.
Zeptám se na počet záznamů v cílové tabulce a potvrdím, že má data:
fitness=> VYBERTE POČET (*) FROM turistický_měsíc_celkem;počet-------25(1 řádek)
Funkce month_total_stats() funguje, ale možná lepším případem použití CURSORu je procházení velkého počtu záznamů. Možná tabulka s přibližně půl milionem záznamů?
Tento další CURSOR je spojen s dotazem, který cílí na tabulku data_staging ze série srovnání v sekci výše:
VYTVOŘTE NEBO NAHRAĎTE FUNKCI location_curs()RETURNS refcursorAS $location$DECLAREv_cur refcursor;BEGINOPEN v_cur pro SELECT segment_num, latitude, longitude, proj_code, asbuilt_flag FROM data_staging;RETURN v_cur;END;$location kód>
Chcete-li pak tento KURZOR použít, pracujte v rámci TRANSAKCE (jak je uvedeno v dokumentaci zde).
location=# BEGIN;BEGINlocation=# SELECT location_curs();location_curs --------------------(1 řádek)
Co tedy můžete dělat s tímto „
Zde je jen několik věcí:
První řádek z KURZORu můžeme vrátit buď pomocí first nebo ABSOLUTE 1:
location=# FETCH first FROM "";číslo_segmentu | zeměpisná šířka | zeměpisná délka | kód_proj | asbuilt_flag -------------+------------------+----------------- ---+-----------+--------------" 3571" | " 29.0202942600" | " -90,2908612800" | 72 | "Y"(1 řádek)location=# NAČTÍT ABSOLUTNÍ 1 Z "";číslo_segmentu | zeměpisná šířka | zeměpisná délka | kód_proj | asbuilt_flag -------------+------------------+----------------- ---+-----------+--------------" 3571" | " 29.0202942600" | " -90,2908612800" | 72 | "Y" (1 řádek)
Chcete hádku téměř v polovině sady výsledků? (Za předpokladu, že víme, že ke KURZORU je vázáno odhadem půl milionu řádků.)
Můžete být tak 'konkrétní' s KURZOREM?
Ano.
Můžeme umístit a NAČÍST hodnoty pro záznam na řádek 234888 (jen náhodné číslo, které jsem vybral):
location=# NAČÍST ABSOLUTE 234888 Z "";číslo_segmentu | zeměpisná šířka | zeměpisná délka | kód_proj | asbuilt_flag -------------+------------------+----------------- ---+-----------+--------------" 11261" | " 28.1159541400" | " -90,7778003500" | 10 | "Y" (1 řádek)
Jakmile se tam umístíme, můžeme posunout KURZOR o „jeden vzad“:
location=# NAČTENÍ ZPĚT OD "";číslo_segmentu | zeměpisná šířka | zeměpisná délka | kód_proj | asbuilt_flag -------------+------------------+----------------- ---+-----------+--------------" 11261" | " 28.1159358200" | " -90,7778242300" | 10 | "Y" (1 řádek)
Což je stejné jako:
location=# NAČÍST ABSOLUTE 234887 Z "";číslo_segmentu | zeměpisná šířka | zeměpisná délka | kód_proj | asbuilt_flag -------------+------------------+----------------- ---+-----------+--------------" 11261" | " 28.1159358200" | " -90,7778242300" | 10 | "Y" (1 řádek)
Poté můžeme KURZOR přesunout zpět na ABSOLUTE 234888 pomocí:
location=# NAČÍST VPŘED Z "";číslo_segmentu | zeměpisná šířka | zeměpisná délka | kód_proj | asbuilt_flag -------------+------------------+----------------- ---+-----------+--------------" 11261" | " 28.1159541400" | " -90,7778003500" | 10 | "Y" (1 řádek)
Užitečný tip:Chcete-li přemístit KURZOR, použijte MOVE místo FETCH, pokud nepotřebujete hodnoty z tohoto řádku.
Viz tento úryvek z dokumentace:
"MOVE přemístí kurzor bez načtení dat. MOVE funguje přesně jako příkaz FETCH, s tím rozdílem, že pouze umístí kurzor a nevrací řádky."
Název "
Znovu navštívím svá data statistik kondice, abych napsal funkci a pojmenoval CURSOR spolu s potenciálním případem použití v „reálném světě“.
CURSOR se zaměří na tuto dodatečnou tabulku, která ukládá výsledky neomezené na měsíc květen (v podstatě všechny, které jsem dosud shromáždil), jako v předchozím příkladu:
fitness=> VYTVOŘIT TABULKU cp_hiking_total JAKO VÝBĚR * Z turistické_měsíc_celkem BEZ DAT;VYTVOŘIT TABULKU JAKO
Poté jej naplňte daty:
fitness=> INSERT INTO cp_hiking_total VYBERTE hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brandFROM hiking_stats JAKO hsINNER JOIN hiking_trail AS htON hs. =ht.th_idINNER JOIN trail_route AS trON ht.tr_id =tr.trail_idINNER JOIN shoe_brand AS sbON hs.shoe_id =sb.shoe_idORDER BY hs.day_walked ASC;INSERT 0 51
Nyní pomocí níže uvedené funkce PLpgSQL VYTVOŘTE 'pojmenovaný' KURZOR:
VYTVOŘIT NEBO NAHRADIT FUNKCI stats_cursor(refcursor)RETURNS refcursorAS $$BEGINOTEVŘÍT $1 FORSELECT *FROM cp_hiking_total;RETURN $1;END;$$ LANGUAGE plpgsql;
Tento CURSOR budu nazývat „statistiky“:
fitness=> BEGIN;BEGINfitness=> SELECT stats_cursor('stats');stats_cursor --------------stats(1 řádek)
Předpokládejme, že chci, aby byl „12.“ řádek svázán s CURSORem.
Mohu umístit KURZOR na tento řádek a získat tyto výsledky pomocí níže uvedeného příkazu:
fitness=> NAČÍST ABSOLUTE 12 ZE statistik;day_hiked | spálené kalorie | míle | trvání | tempo | trail_hiked | boty_nošené ------------+-----------------+-------+---------- +------+---------------------+--------------------- -------------------2018-05-02 | 311,2 | 3,27 | 00:57:13 | 3,4 | Stromová stezka prodloužená | New Balance Trail Runners-All Terrain (1 řádek)
Pro účely tohoto blogového příspěvku si představte, že z první ruky vím, že hodnota sloupce Tempo pro tento řádek je nesprávná.
Konkrétně si pamatuji, že jsem byl toho dne ‚na nohou unavený‘ a během té túry jsem udržoval tempo pouze 3,0. (Hej, to se stává.)
Dobře, jen AKTUALIZUJI tabulku cp_hiking_total, aby odrážela tuto změnu.
Bezpochyby poměrně jednoduché. Nuda…
A co místo toho se statistikami CURSOR?
fitness=> AKTUALIZACE cp_hiking_totalfitness-> NASTAVIT tempo =3.0fitness-> KDE AKTUÁLNÍ statistiky;AKTUALIZACE 1
Chcete-li, aby byla tato změna trvalá, zadejte COMMIT:
fitness=> COMMIT;COMMIT
Pojďme se zeptat a uvidíme, že se UPDATE projeví v tabulce cp_hiking_total:
fitness=> VYBERTE * Z cp_hiking_totalfitness-> WHERE day_hiked ='2018-05-02';day_hiked | spálené kalorie | míle | trvání | tempo | trail_hiked | boty_nošené ------------+-----------------+-------+---------- +------+---------------------+--------------------- -------------------2018-05-02 | 311,2 | 3,27 | 00:57:13 | 3,0 | Stromová stezka prodloužená | New Balance Trail Runners-All Terrain (1 řádek)
Jak skvělé to je?
Pohybujte se v sadě výsledků CURSORu a v případě potřeby spusťte UPDATE.
Docela silný, když se mě ptáte. A pohodlné.
Některá „upozornění“ a informace z dokumentace k tomuto typu CURSOR:
"Obecně se doporučuje použít FOR UPDATE, pokud je kurzor určen k použití s UPDATE ... WHERE CURRENT OF nebo DELETE ... WHERE CURRENT OF. Použití FOR UPDATE zabrání jiným relacím ve změně řádků mezi časem jsou načteny a čas jejich aktualizace. Bez FOR UPDATE nebude mít následný příkaz WHERE CURRENT OF žádný účinek, pokud byl řádek od vytvoření kurzoru změněn.
Dalším důvodem pro použití FOR UPDATE je to, že bez něj by následný WHERE CURRENT OF mohl selhat, pokud kurzorový dotaz nesplňuje pravidla standardu SQL pro „jednoduchou aktualizaci“ (konkrétně kurzor musí odkazovat pouze na jednu tabulku a nepoužívejte seskupování nebo OBJEDNAT PODLE). Kurzory, které nelze jednoduše aktualizovat, mohou nebo nemusí fungovat, v závislosti na podrobnostech výběru plánu; takže v nejhorším případě může aplikace fungovat při testování a pak selhat v produkci."
S KURZOREM, který jsem zde použil, jsem se řídil standardními pravidly SQL (z pasáží výše) v aspektu:Odkazoval jsem pouze na jednu tabulku, bez seskupování nebo ORDER by klauzule.
Proč na tom záleží.
Stejně jako u mnoha operací, dotazů nebo úloh v PostgreSQL (a SQL obecně) existuje obvykle více než jeden způsob, jak dosáhnout a dosáhnout svého konečného cíle. Což je jeden z hlavních důvodů, proč mě přitahuje SQL a snažím se dozvědět více.
Doufám, že jsem prostřednictvím tohoto navazujícího příspěvku na blogu poskytl určitý pohled na to, proč byla víceřádková AKTUALIZACE pomocí CASE zahrnuta jako jeden z mých oblíbených dotazů v prvním doprovodném blogovém příspěvku. Už jen mít to jako možnost se mi vyplatí.
Kromě toho prozkoumejte KURZORY pro procházení velkých sad výsledků. Provádění operací DML, jako jsou UPDATES a/nebo DELETES, pomocí správného typu CURSOR, je jen „třešničkou na dortu“. Chci je dále studovat pro další případy použití.