Procedura pro generování kontingenční tabulky v PL/pgSQL

Z PostgreSQL
Skočit na navigaci Skočit na vyhledávání

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