Таблица как параметр в Postgresql
Часто видно жалобы на то, что параметры «не работают». Как же они не работают?
А вот так:
select * from $1 where ...;
И правда не работают — таблица должна быть известна серверу на момент подготовки запроса. Так что же — выходит, совсем никак невозможно передать таблицу как параметр? А если не как параметр? А если у меня в одной таблице значениями колонок являются другие таблицы — что делать? Не все так страшно — можно использовать функции. В самом деле, давайте создадим простую и незатейливую функцию, которая выполнит запрос и вернет нам результат:
create or replace function doSelect(query text) returns setof record as
$code$
begin
return query execute query;
end;
$code$
language plpgsql
В данном случае мы передаем не имя таблицы, а сразу запрос. И правда, нам могут понадобиться не все строки из нее, а только часть — так почему бы не заставить сервер сразу отобрать все нужные нам строки? Действительно, при выборке из функции сервер будет вынужден фильтровать все возвращаемые значения, отбирая только нужные, а в случае запроса он может воспользоваться, например, индексами, не говоря уже о том, что запрос может быть сложнее, чем просто выборка всего из таблицы.
Проверим:
work=# select * from doSelect('select relname::text from pg_class') as ds(name text) where name='aa';
name
------
aa
(1 строка)
Работает.
В принципе можно сделать еще и работу с параметрами:
create or replace function doSelect(query text, p1 text) returns setof record as
$code$
begin
return query execute query using p1;
end;
$code$
language plpgsql;
create or replace function doSelect(query text, p1 text, p2 text) returns setof record as
$code$
begin
return query execute query using p1, p2;
end;
$code$
language plpgsql;
… и так далее.
Несмотря на некоторую неуклюжесть (в execute…using нельзя передать массив параметров; массивы — это наборы элементов одного типа, а параметры, вообще говоря, могут иметь разный) это все прекрасно работает:
work=# select *
from doSelect(format('select table_catalog::text,
table_schema::text,
table_name::text
from %s
where table_schema=$1 limit $2::bigint',
'information_schema.tables'), 'public',1::text)
as i(table_catalog text, table_schema text, table_name text);
table_catalog | table_schema | table_name
---------------+--------------+------------
work | public | aa
(1 строка)
Чтобы не создавать множество однотипных функций можно просто создать функцию с переменным числом параметров:
create or replace function doSelect(query text, variadic param text[]) returns setof record as
$code$
begin
return query execute query using param;
end;
$code$
language plpgsql
Правда, при формировании строки запроса придется использовать не вполне удобный синтаксис — $1[N], где N — номер переданного параметра. Так, для запроса выше получается:
work=# select *
from doSelect(format('select table_catalog::text,
table_schema::text,
table_name::text
from %s
where table_schema=$1[1] limit $1[2]::bigint',
'information_schema.tables'), 'public',1::text)
as i(table_catalog text, table_schema text, table_name text);
table_catalog | table_schema | table_name
---------------+--------------+------------
work | public | aa
(1 строка)
Суть проблемы заключается в том, что в Postgres невозможно иметь массивы разнотипных элементов — и в функциях выше все приводится, как видно, к типу text, отчего в теле запроса требуются явные приведения к требуемым типам (любопытно, кстати, что limit требует тип bigint). Тем не менее это все вполне работоспособно. Что самое интересное, эти функции можно использовать не только с параметрами, передаваемыми из приложения — их можно использовать и с колонками из другой таблицы, например:
work=# select table_name, cnt.cnt
from information_schema.tables t,
doSelect(format('select count(*) from %s', t.table_name)) as cnt(cnt bigint)
where table_schema='public';
table_name | cnt
---------------+----------
aa | 10000000
ttq | 3
report | 12
colltest | 100000
t2 | 100000
tpair | 10000
call | 0
XXXXXXX_locks | 1
t | 2
sbr | 273370
stest | 954
house | 21000
ttn | 1000000
addrobj | 21000
tt1 | 1
tt2 | 1
ttt | 100000
tt | 1
t1 | 10000
(19 строк)
Хотелось бы обратить внимание на то, что мы ссылаемся в вызове функции на колонку из таблицы, расположенной левее в перечислении таблиц во from.
Так что таблицу как параметр использовать вполне можно; стоит, правда, обратить внимание на то, что во время выполнения запроса для каждого выполнения функции будет строиться отдельный план для динамического запроса, что, разумеется, требует определенных ресурсов, хотя, с другой стороны, часто может оказаться вполне желательным побочным эффектом.
Насколько просядет производительность при таком подоходе? Как ни странно, по крайней мере в простых случаях весьма незначительно:
create table tableasparameter as select n from generate_series(1,1000) as gs(n);
work=# explain analyze
select tt.* from generate_series(1,10000), tableasparameter tt;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.00..12526.50 rows=1000000 width=4) (actual time=1.919..1347.240 rows=10000000 loops=1)
-> Function Scan on generate_series (cost=0.00..10.00 rows=1000 width=0) (actual time=1.896..2.508 rows=10000 loops=1)
-> Materialize (cost=0.00..19.00 rows=1000 width=4) (actual time=0.000..0.042 rows=1000 loops=10000)
-> Seq Scan on tableasparameter tt (cost=0.00..14.00 rows=1000 width=4) (actual time=0.017..0.219 rows=1000 loops=1)
Planning time: 0.068 ms
Execution time: 1648.586 ms
(6 строк)
work=# explain analyze
work-# select tt.* from generate_series(1,10000), doSelect('select * from tableasparameter') as tt(val int);
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.25..20010.25 rows=1000000 width=4) (actual time=1.294..1401.768 rows=10000000 loops=1)
-> Function Scan on generate_series (cost=0.00..10.00 rows=1000 width=0) (actual time=1.033..1.590 rows=10000 loops=1)
-> Function Scan on doselect tt (cost=0.25..10.25 rows=1000 width=4) (actual time=0.000..0.047 rows=1000 loops=10000)
Planning time: 0.039 ms
Execution time: 1705.056 ms
(5 строк)
Как видно, потери в производительности есть, но, в общем, не слишком существенные.