Как установить значение составного поля переменных с помощью динамического SQL
Для этого типа:
-- Just for testing purposes:
CREATE TYPE testType as (name text)
Я могу получить значение поля динамически с помощью этой функции:
CREATE OR REPLACE FUNCTION get_field(object anyelement, field text) RETURNS text as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE 'SELECT $1."' || field || '"'
USING object
INTO value;
return value;
END;
$BODY$
LANGUAGE plpgsql
Вызов get_field('(david)'::testType, 'name')
работает так, как ожидается, возвращается "david".
Но как установить значение поля в составном типе? Я пробовал эти функции:
CREATE OR REPLACE FUNCTION set_field_try1(object anyelement, field text, value text)
RETURNS anyelement
as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE '$1."' || field || '" := $2'
USING object, value;
return object;
END;
$BODY$
LANGUAGE plpgsql
CREATE OR REPLACE FUNCTION set_field_try2(object anyelement, field text, value text)
RETURNS anyelement
as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE 'SELECT $1 INTO $2."' || field || '"'
USING value, object;
return object;
END;
$BODY$
LANGUAGE plpgsql
CREATE OR REPLACE FUNCTION set_field_try3(object anyelement, field text, value text)
RETURNS anyelement
as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE 'BEGIN $1."' || field || '" := $2; SELECT $1; END;'
INTO object
USING value, object;
return object;
END;
$BODY$
LANGUAGE plpgsql
и некоторые варианты.
Вызов set_field_tryX
не работает. Я всегда получаю ошибку "ОШИБКА: синтаксис в точке или рядом...".
Как я могу это сделать?
Примечания:
- Параметр
anyelement
, и поле может быть любым полем в составном типе. Я не могу просто использовать object.name.
- Меня беспокоит SQL-инъекция. Любые советы в этом были бы оценены, но это не мой вопрос.
Ответы
Ответ 1
Быстрее с hstore
Так как Postgres 9.0, с дополнительный модуль hstore
установлен в в вашей базе данных есть очень простое и быстрое решение с #=
оператор, который...
замените [s] поля в record
соответствующими значениями из hstore
.
Чтобы установить модуль:
CREATE EXTENSION hstore;
Примеры:
SELECT my_record #= '"field"=>"value"'::hstore; -- with string literal
SELECT my_record #= hstore(field, value); -- with values
Значения должны быть отброшены на text
и обратно, очевидно.
Примеры функций plpgsql с более подробной информацией:
Почти так же быстро с json
Аналогичные, но в настоящее время недокументированные (начиная с pg 9.5) решения с json
(pg 9.3+) или jsonb
(pg 9.4+), встроенные в Postgres, поэтому вам не нужен дополнительный модуль.
Подробнее см. @Geir для ответа.
Без hstore
и json
Если вы используете более старую версию или не можете установить дополнительный модуль hstore
или не можете считать, что он установлен, вот улучшенная версия того, что я опубликовал ранее. Тем не менее, медленнее, чем оператор hstore
:
CREATE OR REPLACE FUNCTION f_setfield(INOUT _comp_val anyelement
, _field text, _val text)
RETURNS anyelement AS
$func$
BEGIN
EXECUTE 'SELECT ' || array_to_string(ARRAY(
SELECT CASE WHEN attname = _field
THEN '$2'
ELSE '($1).' || quote_ident(attname)
END AS fld
FROM pg_catalog.pg_attribute
WHERE attrelid = pg_typeof(_comp_val)::text::regclass
AND attnum > 0
AND attisdropped = FALSE
ORDER BY attnum
), ',')
USING _comp_val, _val
INTO _comp_val;
END
$func$ LANGUAGE plpgsql STABLE;
Вызов:
CREATE TEMP TABLE t( a int, b text); -- Composite type for testing
SELECT f_setfield(NULL::t, 'a', '1');
Примечания
-
Явный приведение значения _val
к целевому типу данных не является необходимым, строковый литерал в динамическом запросе будет принудительно принудительно заимствован для подзапроса на pg_type
. Но я сделал еще один шаг:
-
Замените quote_literal(_val)
прямой вставкой значений через предложение USING
. Сохраняет один вызов функции и два нажатия, и в любом случае безопаснее. text
автоматически применяется к целевому типу в современном PostgreSQL. (Не тестировалось с версиями до 9.1.)
-
array_to_string(ARRAY())
быстрее, чем string_agg()
.
-
Никаких переменных не требуется, нет DECLARE
. Меньше заданий.
-
В динамическом SQL нет подзапроса. ($1).field
выполняется быстрее.
-
pg_typeof(_comp_val)::text::regclass
делает то же самое, что и
(SELECT typrelid FROM pg_catalog.pg_type WHERE oid = pg_typeof($1)::oid)
для действительных составных типов, только быстрее.
Эта последняя модификация построена на предположении, что pg_type.typname
всегда совпадает с ассоциированным pg_class.relname
для зарегистрированных композитных типов, а двойной литой может заменить подзапрос. Я проверил этот тест в большой базе данных, чтобы проверить, и он выглядел пустым, как ожидалось:
SELECT *
FROM pg_catalog.pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE t.typrelid > 0 -- exclude non-composite types
AND t.typrelid IS DISTINCT FROM
(quote_ident(n.nspname ) || '.' || quote_ident(typname))::regclass
-
Использование параметра INOUT
устраняет необходимость явного RETURN
. Это всего лишь условный ярлык. Павлу это не понравится, он предпочитает явное выражение RETURN
...
Все, что собрано, это почти в два раза быстрее, чем предыдущая версия.
Ответ на оригинал (устаревший):
В результате получается версия ~ 2,25 раза быстрее. Но я, вероятно, не мог этого сделать, не построив вторую версию Павла.
Кроме того, эта версия позволяет избежать большей части кастинга для текста и обратно, делая все в рамках одного запроса, поэтому он должен быть гораздо менее подвержен ошибкам.
Протестировано с помощью PostgreSQL 9.0 и 9.1.
CREATE FUNCTION f_setfield(_comp_val anyelement, _field text, _val text)
RETURNS anyelement AS
$func$
DECLARE
_list text;
BEGIN
_list := (
SELECT string_agg(x.fld, ',')
FROM (
SELECT CASE WHEN a.attname = $2
THEN quote_literal($3) || '::'|| (SELECT quote_ident(typname)
FROM pg_catalog.pg_type
WHERE oid = a.atttypid)
ELSE quote_ident(a.attname)
END AS fld
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_catalog.pg_type
WHERE oid = pg_typeof($1)::oid)
AND a.attnum > 0
AND a.attisdropped = false
ORDER BY a.attnum
) x
);
EXECUTE 'SELECT ' || _list || ' FROM (SELECT $1.*) x'
USING $1
INTO $1;
RETURN $1;
END
$func$ LANGUAGE plpgsql STABLE;
Ответ 2
Я написал вторую версию функции setfield. Он работает в postgres 9.1. Я не тестировал его в более старых версиях. Это не чудо (из представления производительности), но оно более надежное и примерно в 8 раз быстрее предыдущего.
CREATE OR REPLACE FUNCTION public.setfield2(anyelement, text, text)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
DECLARE
_name text;
_values text[];
_value text;
_attnum int;
BEGIN
FOR _name, _attnum
IN SELECT a.attname, a.attnum
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_type
WHERE oid = pg_typeof($1)::oid)
AND a.attnum > 0
LOOP
IF _name = $2 THEN
_value := $3;
ELSE
EXECUTE 'SELECT (($1).' || quote_ident(_name) || ')::text' INTO _value USING $1;
END IF;
_values[_attnum] := COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', '');
END LOOP;
EXECUTE 'SELECT (' || quote_ident(pg_typeof($1)::text) || ' ''(' || array_to_string(_values,',') || ')'').*' INTO $1;
RETURN $1;
END;
$function$;
Ответ 3
ОБНОВЛЕНИЕ/предостережение: Эрвин указывает, что это в настоящее время недокументировано, а manual указывает, что это невозможно.
Используйте hstore или решение Pavel.
Это простое решение на основе json почти так же быстро, как и hstore, и требует только Postgres 9.3 или новее. Это должно быть хорошим вариантом, если вы не можете использовать расширение hstore, а разница в производительности должна быть незначительной. Контрольные показатели: fooobar.com/questions/31493/...
a) Мы можем либо сделать это inline путем cast/concat. Функция Json требует Postgres 9.3:
SELECT json_populate_record(
record
, ('{"'||'key'||'":"'||'new-value'||'"}')::json
);
b) или встроенный с помощью функций из Postgres 9.4.
SELECT json_populate_record (
record
,json_object(ARRAY['key', 'new-value'])
);
Примечание. Я выбрал json_object (ARRAY [ключ, значение]), поскольку он был немного быстрее, чем json_build_object (ключ, значение):
Чтобы скрыть данные о литье, вы можете использовать a) в функции с небольшими накладными расходами.
CREATE FUNCTION x.setfield_json(in_element anyelement, key text, value text)
RETURNS anyelement AS
$BODY$
SELECT json_populate_record( in_element, ('{"'||key||'":"'||value||'"}')::json);
$BODY$ LANGUAGE sql;
Ответ 4
"SELECT INTO" вне plpgsql (в динамическом контексте SQL) имеет другой смысл, чем вы ожидаете, - он сохраняет результат запроса к таблице.
Модификация любого поля возможна, но не простая
CREATE OR REPLACE FUNCTION public.setfield(a anyelement, text, text)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
begin
create temp table aux as select $1.*;
execute 'update aux set ' || quote_ident($2) || ' = ' || quote_literal($3);
select into $1 * from aux;
drop table aux;
return $1;
end;
$function$
Но этот код не очень эффективен - это невозможно записать в plpgsql. Вы можете найти некоторую библиотеку C, которая должна делать.
Ответ 5
Обновление в марте 2015 года:
Сейчас сильно устарел. Рассмотрите новый тест @Geir с более быстрыми вариантами.
Настройка тестирования и контрольные показатели
Я принял три представленных решения (к 16 октября 2011 года) и провел тест на PostgreSQL 9.0.
Ниже вы найдете полную настройку. Только тестовые данные не включены, поскольку я использовал базу данных реальной жизни (а не синтетические данные). Все это инкапсулировано в свою схему для неинтрузивного использования.
Я хотел бы поощрять любого, кто хочет воспроизвести тест. Может быть, с postgres 9.1? И добавить свои результаты здесь?:)
-- DROP SCHEMA x CASCADE;
CREATE SCHEMA x;
-- Pavel 1
CREATE OR REPLACE FUNCTION x.setfield(anyelement, text, text)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
begin
create temp table aux as select $1.*;
execute 'update aux set ' || quote_ident($2) || ' = ' || quote_literal($3);
select into $1 * from aux;
drop table aux;
return $1;
end;
$function$;
-- Pavel 2 (with patches)
CREATE OR REPLACE FUNCTION x.setfield2(anyelement, text, text)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
DECLARE
_name text;
_values text[];
_value text;
_attnum int;
BEGIN
FOR _name, _attnum
IN SELECT a.attname, a.attnum
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_type
WHERE oid = pg_typeof($1)::oid)
LOOP
IF _name = $2 THEN
_value := $3;
ELSE
EXECUTE 'SELECT (($1).' || quote_ident(_name) || ')::text' INTO _value USING $1;
END IF;
_values[_attnum] := COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', '');
END LOOP;
EXECUTE 'SELECT (' || pg_typeof($1)::text || '''(' || array_to_string(_values,',') || ')'').*' INTO $1;
RETURN $1;
END;
$function$;
-- Erwin 1
CREATE OR REPLACE FUNCTION x.setfield3(anyelement, text, text)
RETURNS anyelement
AS $body$
DECLARE
_list text;
BEGIN
_list := (
SELECT string_agg(x.fld, ',')
FROM (
SELECT CASE WHEN a.attname = $2
THEN quote_literal($3)
ELSE quote_ident(a.attname)
END AS fld
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_type
WHERE oid = pg_typeof($1)::oid)
ORDER BY a.attnum
) x
);
EXECUTE '
SELECT ' || _list || '
FROM (SELECT $1.*) x'
USING $1
INTO $1;
RETURN $1;
END;
$body$ LANGUAGE plpgsql;
-- composite type for tests.
CREATE TYPE x.t_f as (
id int
,company text
,sort text
,log_up timestamp
,log_upby smallint
);
-- temp table with real life test data
DROP TABLE IF EXISTS tmp_f;
CREATE TEMP table tmp_f AS
SELECT ROW(firma_id,firma,sort,log_up,log_upby)::x.t_f AS f
FROM ef.firma
WHERE firma !~~ '"%';
-- SELECT count(*) FROM tmp_f; -- 5183
-- Quick test: results are identical?
SELECT *,
x.setfield (f, 'company','test')
,x.setfield2(f, 'company','test')
,x.setfield3(f, 'company','test')
FROM tmp_f
LIMIT 10;
Бенчмарки
Я запускал запросы несколько раз, чтобы заполнить кеш. Представленные результаты являются лучшими из пяти общих периодов времени с EXPLAIN ANALYZE
.
Первый раунд с 1000 строк
Первый прототип Pavel максимизирует общую память с большим количеством строк.
Павел 1: 2445.112 мс
SELECT x.setfield (f, 'company','test') FROM tmp_f limit 1000;
Павел 2: 263.753 мс
SELECT x.setfield2(f, 'company','test') FROM tmp_f limit 1000;
Эрвин 1:120,671 мс
SELECT x.setfield3(f, 'company','test') FROM tmp_f limit 1000;
Другой тест с 5183 строками.
Павел 2: 1327.429 мс
SELECT x.setfield2(f, 'company','test') FROM tmp_f;
Erwin1: 588.691 мс
SELECT x.setfield3(f, 'company','test') FROM tmp_f;
Ответ 6
Настройка тестирования и тесты v2
Эрвин призвал воспроизвести свой тест в этой теме (fooobar.com/info/31493/...), поэтому я изменил его код на синтетические тестовые данные и добавил как решение hstore, так и json -решение из моего ответа (и решение json от Pavel найдено в другом потоке). Тест теперь запускается как один запрос, что облегчает захват результатов.
DROP SCHEMA IF EXISTS x CASCADE;
CREATE SCHEMA x;
-- Pavel 1:
--------------------------------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION x.setfield(anyelement, text, text)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
begin
create temp table aux as select $1.*;
execute 'update aux set ' || quote_ident($2) || ' = ' || quote_literal($3);
select into $1 * from aux;
drop table aux;
return $1;
end;
$function$;
-- Pavel 2 (with patches)
--------------------------------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION x.setfield2(anyelement, text, text)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
DECLARE
_name text;
_values text[];
_value text;
_attnum int;
BEGIN
FOR _name, _attnum
IN SELECT a.attname, a.attnum
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_type
WHERE oid = pg_typeof($1)::oid)
LOOP
IF _name = $2 THEN
_value := $3;
ELSE
EXECUTE 'SELECT (($1).' || quote_ident(_name) || ')::text' INTO _value USING $1;
END IF;
_values[_attnum] := COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', '');
END LOOP;
EXECUTE 'SELECT (' || pg_typeof($1)::text || '''(' || array_to_string(_values,',') || ')'').*' INTO $1;
RETURN $1;
END;
$function$;
-- Erwin 1
--------------------------------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION x.setfield3(anyelement, text, text)
RETURNS anyelement
AS $body$
DECLARE
_list text;
BEGIN
_list := (
SELECT string_agg(x.fld, ',')
FROM (
SELECT CASE WHEN a.attname = $2
THEN quote_literal($3)
ELSE quote_ident(a.attname)
END AS fld
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_type
WHERE oid = pg_typeof($1)::oid)
ORDER BY a.attnum
) x
);
EXECUTE '
SELECT ' || _list || '
FROM (SELECT $1.*) x'
USING $1
INTO $1;
RETURN $1;
END;
$body$ LANGUAGE plpgsql;
-- Erwin 2
--------------------------------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION x.setfield4(INOUT _comp_val anyelement
, _field text, _val text)
RETURNS anyelement AS
$func$
BEGIN
EXECUTE 'SELECT ' || array_to_string(ARRAY(
SELECT CASE WHEN attname = _field
THEN '$2'
ELSE '($1).' || quote_ident(attname)
END AS fld
FROM pg_catalog.pg_attribute
WHERE attrelid = pg_typeof(_comp_val)::text::regclass
AND attnum > 0
AND attisdropped = FALSE
ORDER BY attnum
), ',')
USING _comp_val, _val
INTO _comp_val;
END
$func$ LANGUAGE plpgsql;
-- Pavel 3: json. (Postgres 9.4)
-- Found here: https://stackoverflow.com/a/28284491/1914376
--------------------------------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION x.setfield5(r anyelement, fn text, val text,OUT result anyelement)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
declare jo json;
begin
jo := (select json_object(array_agg(key),
array_agg(case key when fn then val
else value end))
from json_each_text(row_to_json(r)));
result := json_populate_record(r, jo);
end;
$function$;
-- Json. Use built-in json functions (Postgres 9.3)
-- This is available from 9.3 since we create json by casting
-- instead of using json_object/json_build_object only available from 9.4
--------------------------------------------------------------------------------------------------
CREATE FUNCTION x.setfield_json(in_element anyelement, key text, value text)
RETURNS anyelement AS
$BODY$
SELECT json_populate_record( in_element, ('{"'||key||'":"'||value||'"}')::json);
$BODY$ LANGUAGE sql;
--------------------------------------------------------------------------------------------------
-- Test setup
--------------------------------------------------------------------------------------------------
-- composite type for tests.
CREATE TYPE x.t_f as (
id int
,company text
,sort text
,log_up timestamp
,log_upby smallint
);
-- Create temp table with synthetic test data
DROP TABLE IF EXISTS tmp_f;
CREATE TEMP table tmp_f AS
SELECT ROW(i, 'company'||i, NULL, NULL, NULL)::x.t_f AS f
FROM generate_series(1, 5000) S(i);
-- Run the benchmark
DO $$ DECLARE start_time timestamptz; test_count integer; test_description TEXT; BEGIN
test_count := 200;
test_description := 'setfield, Pavel 1: temptable';
start_time := clock_timestamp();
PERFORM x.setfield (f, 'company','new-value-'||md5(random()::text)) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
test_count := 5000;
test_description := 'setfield2, Pavel 2: reflection';
start_time := clock_timestamp();
PERFORM x.setfield2 (f, 'company','new-value-'||md5(random()::text)) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
test_count := 5000;
test_description := 'setfield3, Erwin 1: reflection';
start_time := clock_timestamp();
PERFORM x.setfield3 (f, 'company','new-value-'||md5(random()::text)) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
test_count := 5000;
test_description := 'setfield4, Erwin 2: reflection';
start_time := clock_timestamp();
PERFORM x.setfield4 (f, 'company','new-value-'||md5(random()::text)) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
test_count := 5000;
test_description := 'setfield5, Pavel 3: json (PG 9.4)';
start_time := clock_timestamp();
PERFORM x.setfield5 (f, 'company','new-value-'||md5(random()::text)) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
test_count := 5000;
test_description := 'setfield_json, Geir 1: casting (PG 9.3)';
start_time := clock_timestamp();
PERFORM x.setfield_json (f, 'company','new-value-'||md5(random()::text)) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
--json_object(ARRAY(key,value]) is actually faster than json_build_object(key, value)
test_count := 5000;
test_description := 'no function/inlined: json_object (PG 9.4)';
start_time := clock_timestamp();
PERFORM json_populate_record( f, json_object(ARRAY['company', 'new-value'||md5(random()::text)] )) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
test_count := 5000;
test_description := 'no function/inlined: hstore (PG 9.0)';
start_time := clock_timestamp();
PERFORM f #= hstore('company', 'new-value'||md5(random()::text)) FROM tmp_f LIMIT test_count;
RAISE NOTICE 'Test took: % ms (for % rows) Name: %', extract(MILLISECONDS FROM (clock_timestamp() - start_time))::INTEGER, test_count, test_description;
END; $$;
Результаты теста на 9.4.1, win32, i5-4300U
NOTICE: Test took: 1138 ms (for 200 rows) Name: setfield, Pavel 1: temptable
NOTICE: Test took: 652 ms (for 5000 rows) Name: setfield2, Pavel 2: reflection
NOTICE: Test took: 364 ms (for 5000 rows) Name: setfield3, Erwin 1: reflection
NOTICE: Test took: 275 ms (for 5000 rows) Name: setfield4, Erwin 2: reflection
NOTICE: Test took: 192 ms (for 5000 rows) Name: setfield5, Pavel 3: json (PG 9.4)
NOTICE: Test took: 23 ms (for 5000 rows) Name: setfield_json, Geir 1: casting (PG 9.3)
NOTICE: Test took: 25 ms (for 5000 rows) Name: no function/inlined: json_object (PG 9.4)
NOTICE: Test took: 14 ms (for 5000 rows) Name: no function/inlined: hstore (PG 9.0)