Procedura pro generování kontingenční tabulky v PL/pgSQL
Donedávna jsem žil v omylu, že v PostgreSQL nelze napsat uloženou proceduru, která by vracela předem nedefinovanou tabulku. To proto, že v fakticky lze procedury v Pg implementovat pouze jako funkce - ty lze volat pouze z příkazu SELECT a u příkazu SELECT musí být již v době plánování jasné, jak bude vypadat výstupní tabulka (v podstatě jazyk SQL je v PostgreSQL přísně typový a statický). Mýlil jsem se - určitá možnost tu je, a tou je použití kurzoru. Příkaz FETCH slouží pouze k přesunu tabulky ze serveru na klienta, k ničemu jinému. A lze jej použít k zobrazení dynamicky vytvořeného obsahu. Jako příklad použiji generování kontingenční tabulky převzatý od Rolanda Baumana použitý v článku Jemný úvod do uložených procedur - generování křížové sestavy.
CREATE TABLE employees (
id serial PRIMARY KEY,
shop_id int,
gender char(1),
name varchar(32),
salary int
);
CREATE TABLE shops (
id serial PRIMARY KEY,
shop varchar(32)
);
INSERT INTO shops(shop) VALUES('Zurich'),('New York'),('London');
INSERT INTO employees (shop_id, gender, name, salary)
VALUES
(1, 'm', 'Jon Simpson', 4500),
(1, 'f', 'Barbara Breitenmoser',4700),
(2, 'f', 'KirstenRuegg',5600),
(3, 'm', 'Ralp Teller',5100),
(3, 'm', 'Peter Jonson',4700);
Jádro procedury je v generování SELECTu podle vzoru:
SELECT shop, SUM(CASE gender WHEN 'f' THEN salary ELSE 0 END) AS f, SUM(CASE gender WHEN 'm' THEN salary ELSE 0 END) AS m, SUM(salary) AS total FROM employees INNER JOIN shops USING (shop_id) GROUP BY shop
Vlastní funkce je zde - vrací pojmenovaný kurzor:
CREATE OR REPLACE FUNCTION do_cross_cursor(dimx_name varchar, dimx_source varchar,
dimy_name varchar, dimy_source varchar,
expr varchar)
RETURNS refcursor AS $$
DECLARE
col_list text[] := '{}';
query text;
r RECORD;
result refcursor := 'result';
BEGIN
FOR r IN EXECUTE 'SELECT DISTINCT ' || dimx_name || '::text AS val ' || dimx_source
LOOP
col_list := array_append(col_list, 'SUM(CASE ' || dimx_name || ' WHEN '
|| quote_literal(r.val) || ' THEN ' || expr || ' ELSE 0 END) AS '
|| quote_ident(r.val) || '');
END LOOP;
query := 'SELECT ' || dimy_name || ', ' || array_to_string(col_list, ',')
|| ', SUM(' || expr || ') AS Total ' || dimy_source || ' GROUP BY ' || dimy_name;
OPEN result NO SCROLL FOR EXECUTE query;
RETURN result;
END;
$$ LANGUAGE plpgsql STRICT;
Použití kurzorů je omezeno transakcí, musím tedy zapouzdřit funkci do explicitní transakce:
BEGIN;
SELECT do_cross_cursor('gender', 'FROM employees','shop','FROM employees e JOIN shops s ON s.id = e.shop_id', 'salary');
FETCH ALL FROM result;
END;
BEGIN;
SELECT do_cross_cursor('shop', 'FROM shops','gender','FROM employees e JOIN shops s ON s.id = e.shop_id', 'salary');
FETCH ALL FROM result;
END;
Výsledky:
postgres=# BEGIN;
BEGIN
postgres=# SELECT do_cross_cursor('gender', 'FROM employees','shop','FROM employees e JOIN shops s ON s.id = e.shop_id', 'salary');
do_cross_cursor
-----------------
result
(1 row)
postgres=# FETCH ALL FROM result;
shop | m | f | total
----------+------+------+-------
New York | 0 | 5600 | 5600
Zurich | 4500 | 4700 | 9200
London | 9800 | 0 | 9800
(3 rows)
postgres=# END;BEGIN;
COMMIT
BEGIN
postgres=# SELECT do_cross_cursor('shop', 'FROM shops','gender','FROM employees e JOIN shops s ON s.id = e.shop_id', 'salary');
do_cross_cursor
-----------------
result
(1 row)
postgres=# FETCH ALL FROM result;
gender | New York | Zurich | London | total
--------+----------+--------+--------+-------
m | 0 | 4500 | 9800 | 14300
f | 5600 | 4700 | 0 | 10300
(2 rows)
postgres=# END;
COMMIT