SQL Triky IV.
Starších dvacet triků Nejnovější triky
Dotaz pomocí kurzoru je pomalý
PostgreSQL podporuje dva přístupy k zpracování dotazů. Klasický EXECUTE a prostřednictvím kurzoru. Pokud potřebujeme rychle prvních N řádků, pak je vhodnější použít kurzor. Prováděcí plán je vybrán tak, aby generoval data co nejdříve. Nehledí se na celkový čas provádění dotazu - jednak se předpokládá, že se nezpracuje kompletní dotaz, druhak se počítá s prodlevami (pro uživatele) během čtení kurzoru. Pokud potřebujeme kompletní výsledek, tak naopak použijeme EXECUTE. Chování optimalizátoru lze ovlivnit - pomocí proměnné cursor_tuple_fraction, která určuje odhad poměru přečtených řádků ku celkovému počtu řádků. Nastavením proměnné na hodnotu 1.0 určujeme, že předpokládáme čtení kompletní výsledku dotazu - a planner tomu přizpůsobí prováděcí plán.
Vícenásobné přiřazení do pole v rámci jednoho příkazu INSERT
Opakovaný přístup ke sloupci má smysl pouze v případě polí (z vestavěných typů). Do konference zaslal Tom Lane:
postgres=# create table bar(a varchar[]); CREATE TABLE postgres=# insert into bar(a[2],a[7]) values('Pavel','Petr'); INSERT 0 1 postgres=# select * from bar; a ──────────────────────────────────────── [2:7]={Pavel,NULL,NULL,NULL,NULL,Petr} (1 row)
Rozdělení lidí do skupin s omezením
Hezký trik založený na window funkcích se objevil na stránkách PostgreSQL Online Journalu. Představte si, že máte seznam lidí, které musíte rozdělit do skupin, přičem pro hmotnost skupiny platí, že nesmí přesáhnout 750Kg. Naznačené řešení nepochybně nebude optimální, také se nejedná o optimalizaci:
CREATE TABLE passengers(passenger_name varchar(20) PRIMARY KEY, weight_kg integer); INSERT INTO passengers(passenger_name, weight_kg) VALUES ('Johnny', 60), ('Jimmy', 120), ('Jenny', 50), ('Namy', 20), ('Grendel', 500), ('Charlie', 200), ('Gongadin', 400), ('Tin Tin', 10), ('Thumb Twins', 10), ('Titan', 600), ('Titania', 550), ('Titan''s Groupie', 55); SELECT carriage,COUNT(passenger_name) As cnt_pass, array_agg(passenger_name) As passengers, SUM(weight_kg) As car_kg FROM (SELECT passenger_name, weight_kg, ceiling(SUM(weight_kg) OVER(ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)/750.00) As carriage FROM passengers) As congregation GROUP BY carriage ORDER BY carriage;
Výsledek:
carriage | cnt_pass | passengers | car_kg ----------+----------+--------------------------------------------+-------- 1 | 5 | {Johnny,Jimmy,Jenny,Namy,Grendel} | 750 2 | 4 | {Charlie,Gongadin,"Tin Tin","Thumb Twins"} | 620 3 | 1 | {Titan} | 600 4 | 2 | {Titania,"Titan's Groupie"} | 605
Analogie funkce GROUP_CONCAT v PostgreSQL
MySQL má poměrně silnou agregační funkci group_concat. Přestože v pg lze definovat vlastní agregační funkce, syntakticky kompatibilní agregační funkci, která by byla obdobou group_concat v postresql vytvořit nelze. Nicméně díky vestavěné podpoře polí je možné suplovat funkcionalitu:
postgres=# SELECT * FROM x; kat | mesto -----+--------- 1 | Benešov 1 | Tábor 1 | Písek 2 | Praha 2 | Hradec 3 | Cheb 3 | Aš (7 rows) postgres=# SELECT kat, array_to_string(ARRAY(SELECT unnest(array_agg(mesto)) ORDER BY 1),',') FROM x GROUP BY kat; kat | array_to_string -----+--------------------- 1 | Benešov,Písek,Tábor 3 | Aš,Cheb 2 | Hradec,Praha (3 rows)
Ekvivalent funkce field() z MySQL v PostgreSQL
Zdroj: http://stackoverflow.com/questions/1309624/simulating-mysqls-order-by-field-in-postgresql
V případě, že chcete určit pořadí výpisu seznamem hodnot, tak v MySQL máte možnost použít funkci field:
select * from pet order by field(species, 'cat', 'dog', 'bird') desc; +----------+--------+---------+------+------------+------------+ | name | owner | species | sex | birthday | death | +----------+--------+---------+------+------------+------------+ | Fluffy | Harold | cat | f | 1993-02-04 | NULL | | Claws | Gwen | cat | m | 1994-03-17 | NULL | | Buffy | Harold | dog | f | 1989-05-13 | NULL | | Fang | Benny | dog | m | 1990-08-27 | NULL | | Bowser | Diane | dog | m | 1989-08-31 | 1995-07-29 | | Chirpy | Gwen | bird | f | 1998-09-11 | NULL | | Whistler | Gwen | bird | NULL | 1997-12-09 | NULL | +----------+--------+---------+------+------------+------------+
V PostgreSQL taková funkce není - buďto ji můžeme nahradit konstrukcí CASE nebo si ji implementovat - lze díky podpoře variadických parametrů:
select * from pet order by species, case species when 'cat' then 1 when 'dog' then 2 when 'bird' then 3 else 0 end desc;
Zdrojový kód funkce field:
CREATE OR REPLACE FUNCTION field(varchar, VARIADIC text[]) RETURNS int AS $$ SELECT i FROM generate_subscripts($2,1) g(i) WHERE $1 = $2[i] UNION ALL SELECT 0 LIMIT 1 1 $$ LANGUAGE sql;
Pro úplnost přidám zdrojový kód komplementární funkce elt:
CREATE OR REPLACE FUNCTION elt(int, VARIADIC text[]) RETURNS text AS $$ SELECT $2[$1]; $$ LANGUAGE sql;
Nejlépe placený zaměstnanec po n-té
Tentokrát s využitím window funkcí:
postgres=# SELECT * FROM zamestnanci; ┌───────────┬───────────┬──────────┬──────────┐ │ jmeno │ prijmeni │ oddeleni │ mzda │ ├───────────┴───────────┴──────────┴──────────┤ │ Pavel │ Stehule │ 1 │ 10000.00 │ │ Zdenek │ Stehule │ 1 │ 9000.00 │ │ Vladimira │ Stehulova │ 2 │ 9000.00 │ └─────────────────────────────────────────────┘ (3 rows) postgres=# SELECT * FROM (SELECT *, max(mzda) OVER (PARTITION BY oddeleni) FROM zamestnanci) x WHERE max = mzda; ┌───────────┬───────────┬──────────┬──────────┬──────────┐ │ jmeno │ prijmeni │ oddeleni │ mzda │ max │ ├───────────┴───────────┴──────────┴──────────┴──────────┤ │ Pavel │ Stehule │ 1 │ 10000.00 │ 10000.00 │ │ Vladimira │ Stehulova │ 2 │ 9000.00 │ 9000.00 │ └────────────────────────────────────────────────────────┘ (2 rows)
Další variace:
Pozor, zde je drobná zrada - intuitivně by člověk použil HAVING. Podle standardu ale nejsou výsledky window funkcí v dotazu vůbec dostupné, takže je nutné použít derivovanou tabulku.
Převod bigintu na ip
Do konference zaslal Jasen Betts
create function bigint_to_inet(bigint) returns inet as $$ select (($1>>24&255)||'.'||($1>>16&255)||'.'||($1>>8&255)||'.'||($1>>0&255))::inet $$ language sql;
Označení originálních souborů na základě patche
Při vývoji PostgreSQL se často používají příkazy difforig a cporig. Pokud se chci podílet na vývoji patche, pak je nutné, ještě před samotnou aplikací patche, označit modifikované soubory pomocí příkazu cporig. Potom jsme schopni vytvořit patch příkazem difforig. Ruční značkování se lze vyhnout následujícím skriptem:
cat named.diff | egrep -e "^\*\*\*.*(sql|out|h|c|sgml|y|lex|Makefile)$" | replace "*** a/" ""| xargs cporig cat mnnotation.diff | egrep -o -e "^\*\*\*.*(sql|out|h|c|sgml|y|lex|Makefile)" | replace "*** " "" | xargs cporig
replace lze nahradit:
sed 's/reg//'
Doplnění základních konstant do systému
Zdroj: http://www.simple-talk.com/sql/t-sql-programming/avoiding-the-eav-of-destruction/
CREATE VIEW Constants (pi, e, phi) AS SELECT X.* FROM (VALUES (CAST (3.141592654 AS FLOAT), CAST (2.718281828 AS FLOAT), CAST (1.6180339887 AS FLOAT)) ) AS X (pi, e, phi);
Výpočet mediánu s využitím Window funkcí
O možnostech určení mediánu se se rozepsal Depesz. Použil řešení, které navrhl Andrew Gierth (RhodiumToad):
select avg(x) from ( select x, row_number() over (order by x),count(*) over () from zdrojdat ) s where row_number between floor((count::float8-1)/2+1) and ceil((count::float8-1)/2+1)
pro zajímavost přidávám řešení Joe Celka (SQL2005):
WITH SortedData (x, hi, lo) AS (SELECT x, ROW_NUMBER() OVER (ORDER BY x ASC), ROW_NUMBER() OVER (ORDER BY x DESC) FROM RawData) SELECT AVG(x * 1.0) AS median FROM SortedData WHERE hi IN (lo, lo+1, lo-1);
v úpravě pro PostgreSQL:
select avg(x) from (select x, row_number() over (order by x asc) as hi, row_number() over (order by x desc) as lo from foobar) s where hi in (lo-1,lo, lo+1);
Na webu jsem narazil ještě na Itzik Ben-Ganovu metodu,:
SELECT CustomerId,AVG(TotalDue) FROM (SELECT CustomerId, TotalDue, ROW_NUMBER() OVER (PARTITION BY CustomerId ORDER BY TotalDue) AS RowNum, COUNT(*) OVER (PARTITION BY CustomerId) AS RowCnt FROM Sales.SalesOrderHeader ) x WHERE RowNum IN ((RowCnt + 1) / 2, (RowCnt + 2) / 2) GROUP BY CustomerId ORDER BY CustomerId;
která by měla být efektivnější než Celkovo řešení (reálně ovšem není) a pravděpodobně je vzorem pro Gierthův způsob. Pozn: fráze COUNT(*) OVER () je z nějakého důvodu pomalá - mnohem rychlejší je subselect (SELECT COUNT(*) FROM tab).
Pozor! Originální Celkova metoda vrací správný výsledek pouze pokud jsou vstupní hodnoty unikátní - což většinou není pravda - a je nutné použít modifikovanou metodu Joe Celka:
SELECT avg(x)::float FROM (SELECT x, row_number() OVER (ORDER BY x asc) AS hi, count(*) OVER () + 1 - row_number() OVER (ORDER BY x) AS lo FROM foobar) qs WHERE hi IN (lo-1,lo,lo+1);
Bezpečná konverze řetězce na číslo
Vestavěné konverzní rutiny vyhodí výjimku, pokud detekují, že řetězec není číslo. To v některých případech může být příliš agresivní chování. Nicméně velice jednoduše lze detekovat, zda řetězec je číslo a jinak vrátit NULL:
CREATE OR REPLACE FUNCTION read_int(varchar) RETURNS int AS $$ SELECT CASE WHEN $1 ~ e'^\\d+$' THEN $1::int END; $$ LANGUAGE SQL IMMUTABLE STRICT;
Odstranění milisekund z timestampu
Obyčejně nepotřebujeme timestamp v maximální přesnosti - někdy dokonce může být maximální dostupná přesnost na úkor přehlednosti - proto je možné určit přesnost pro typ timestamp:
postgres=# select current_timestamp; now ------------------------------ 2009-05-23 20:42:21.57899+02 (1 row) Time: 196,784 ms postgres=# select current_timestamp::timestamp(2); now ------------------------ 2009-05-23 20:42:27.74 (1 row) Time: 51,861 ms postgres=# select current_timestamp::timestamp(0); now --------------------- 2009-05-23 20:42:31 (1 row) Time: 0,729 ms
Zobrazení čísla ve vědeckém formátu
PostgreSQL nepodporuje formát EEEE ve funkci to_char. Tento nedostatek můžeme překonat voláním externí funkce. Jednou z možností je použít implementaci v perlu (funkce sprintf):
create or replace function floating_format(double precision, int) returns varchar as $$ sprintf("%.$_[1]e", $_[0]) $$ language plperl; create or replace function floating_format(double precision) returns varchar as $$ sprintf("%e", $_[0]) $$ language plperl; postgres=# select floating_format(10.223); floating_format ----------------- 1.022300e+01 (1 row) postgres=# select floating_format(10.223,2); floating_format ----------------- 1.02e+01 (1 row) postgres=# select floating_format(10.223,4); floating_format ----------------- 1.0223e+01 (1 row)
Konfigurace terminálů
Pro přístup k PostgreSQL používám dvě rozhraní - emacs a psql. Pro obé si lze práci zpříjemnit následujícími nastaveními:
- emacs (.emacs)
(setq-default truncate-lines t) (ansi-color-for-comint-mode-on) (setq lazy-lock-defer-on-scrolling t) (setq inhibit-startup-echo-area-message t) (setq inhibit-startup-message t) (show-paren-mode t) (setq show-paren-style 'mixed) (fset 'yes-or-no-p 'y-or-n-p) (global-set-key "\M-g" 'goto-line) (setq sql-database "postgres") ;; nejčastěji používaná databáze (setq sql-postgres-options '("-P" "pager=off" "-P" "title= ")) ;; nepoužívat pager, nastavit falešný title (defun psql-init () (sql-highlight-postgres-keywords)) (add-hook 'sql-mode-hook 'psql-init)
- psql
export PAGER="less -RSX" psql ... -P pager=always
Vyplatí se nastavit i automatický rollback chybových SQL příkazů:
\set ON_ERROR_ROLLBACK interactive
Pozor na chování operátorů IS NULL a IS NOT NULL pro složené typy
Člověk tak nějak počítá s tím, že platí !(x IS NULL) = x IS NOT NULL. To ovšem v případě složených typů neplatí. Pokud je alespoň jedna položka NULL a alespoň jedna položka NOT NULL, pak výsledkem obou operátorů je nepravda. IS NULL je pravdivý, pokud jsou všechny položky NULL. IS NOT NULL pokud jsou všechny položky NOT NULL. Pokud je něco mezi, tak oba oparátory vrací false.
CREATE OR REPLACE FUNCTION test_isnull() RETURNS TABLE (a int, b int, isnull bool, isnotnull bool) AS $$ DECLARE r foo; BEGIN isnull := r IS NULL; isnotnull := r IS NOT NULL; RETURN NEXT; a := NULL; b := 10; r := ROW(a, b); isnull := r IS NULL; isnotnull := r IS NOT NULL; RETURN NEXT; a := 10; b := 10; r := ROW(a, b); isnull := r IS NULL; isnotnull := r IS NOT NULL; RETURN NEXT; RETURN; END; $$ LANGUAGE plpgsql; SELECT * FROM test_isnull(); a | b | isnull | isnotnull ----+----+--------+----------- | | t | f | 10 | f | f 10 | 10 | f | t (3 rows)
Pozor na implicitní přetypování v PL/pgSQL
Typ výsledeku každého vyhodnoceného výrazu v PL/pgSQL se porovnává s cílovým typem a v případě, že se neshoduje, dochází k IO přetypování (konverze na string/string na hodnotu). Tato konverze je relativně náročná a navíc je nutné dohledat odpovídající konverzní rutiny. Pokud se typy shodují, tak je výsledek bez jakýchkoliv změn přiřazen cílové proměnné. Ke konverzím dochází nejčastěji v přiřazovacím příkazu:
create or replace function test1() returns int as $$ declare s int := 0; begin for i in 1..100000 loop s := 4e3; end loop; -- numeric return s; end; $$ language plpgsql immutable; create or replace function test2() returns int as $$ declare s int := 0; begin for i in 1..100000 loop s := 4e3::int; end loop; -- int return s; end; $$ language plpgsql immutable; postgres=# select test1(); test1 ------- 4000 (1 row) Time: 176,623 ms postgres=# select test2(); test2 ------- 4000 (1 row) Time: 47,673 ms
nebo v konverzi návratové hodnoty:
create or replace function test1() returns int as $$begin return 4e1; end; $$ language plpgsql; create or replace function test2() returns int as $$begin return 4e1::int; end; $$ language plpgsql; postgres=# select count(test1()) from generate_series(1,100000); count -------- 100000 (1 row) Time: 682,005 ms postgres=# select count(test2()) from generate_series(1,100000); count -------- 100000 (1 row) Time: 528,099 ms
Zabránění paralelnímu spuštění funkce
Teoreticky lze z interních údajů vyčíst, zda ta či ona funkce běží - nicméně tyto údaje nejsou běžně k dispozici (bez vývoje v C). Jedinou dostupnou informací ohledně ostatních procesů je pohled pg_stat_activity. Pokud název funkce nenajdeme v prováděných dotazech, tak víme, že funkce nebyla spuštěna přímo z SQL - je ale možné, že funkce byla spuštěna z uložené procedury a to tímto způsobem detekovat nelze.
create or replace function long_run() returns void as $$ begin if exists(select procpid from pg_stat_activity where procpid <> pg_backend_pid() and current_query like '%long_run(%') then raise notice 'procedura jiz bezi'; return; end if; perform pg_sleep(10); end$$ language plpgsql volatile; CREATE FUNCTION
Další možností je použití tzv. advisory locks http://www.postgresql.org/docs/8.3/interactive/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS.
create or replace function long_run() returns void as $$ declare foid oid:= 'long_run'::regproc::oid; begin if pg_try_advisory_lock(foid::bigint) then perform pg_sleep(10); pg_advisory_unlock(foid::bigint); else raise notice 'procedura jiz bezi'; end if; return; end; $$ language plpgsql;
Přesun tabulek z jednoho schématu do druhého
V PostgreSQL není příkaz, kterým bychom přesunuli tabulky z jednoho schématu do druhého. Díky uloženým procedurám to ovšem není problém:
create or replace function mvtable(src varchar, dest varchar, mask varchar) returns void as $$ declare r record; begin for r in select * from information_schema.tables where table_schema = src and table_name like mask loop execute 'alter table ' || quote_ident(r.table_schema) || '.' || quote_ident(r.table_name) || ' set schema ' || quote_ident(dest); end loop; return; end; $$ language plpgsql;
Převod čísla do libovolné číselné soustavy
CTE můžeme využít pro převod čísla do libovolné číselné soustavy:
postgres=# create function to_base(num int, base int = 10) returns varchar as $$ with recursive z as ( select $1 as a, null::int as b, 0 as i union all select a/$2, a % $2, i+1 from z where a > 0 ) select array_to_string(array(select substring('0123456789abcdef' from b + 1 for 1) from z where i > 0 order by i desc),''); $$ language sql immutable strict; postgres=# select to_base(255); to_base --------- 255 (1 row) postgres=# select to_base(255,16); to_base --------- ff (1 row) postgres=# select to_base(255,8); to_base --------- 377 (1 row) postgres=# select to_base(255,2); to_base ---------- 11111111 (1 row)
Inverzní funkce:
create or replace function base(str varchar, base int = 10) returns int as $$ select sum((position(substring($1 from i for 1) in '0123456789abcdef')- 1) * $2 ^ (char_length($1) - i))::int from generate_series(1,char_length($1)) g(i); $$ language sql immutable strict
Funkce to_bin
V PostgreSQL chybí funkce, která by provedla konverzi čísla do binárního tvaru. Zde je jedna varianta:
create or replace function to_bin(int) returns varchar as $$ declare r varchar; begin execute 'select ' || $1 || '::bit(' || case when $1 = 0 then 1 else trunc(log(2, $1)) + 1 end || ')' into r; return r; end; $$ language plpgsql immutable strict; create or replace function bin(varchar) returns varchar as $$ declare r varchar; begin execute 'select b' || quote_literal($1) || '::int' into r; return r; end; $$ language plpgsql immutable strict; postgres=# select to_bin(5), bin('101'); to_bin | bin --------+----- 101 | 5 (1 row)