Comment définir la valeur du champ variable composite en utilisant le SQL dynamique
étant donné ce type:
-- Just for testing purposes:
CREATE TYPE testType as (name text)
je peux obtenir la valeur d'un champ dynamiquement avec cette fonction:
CREATE OR REPLACE FUNCTION get_field(object anyelement, field text) RETURNS text as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE 'SELECT ."' || field || '"'
USING object
INTO value;
return value;
END;
$BODY$
LANGUAGE plpgsql
Appel get_field('(david)'::testType, 'name')
fonctionne comme prévu, le retour "david".
mais comment définir une valeur d'un champ dans un type composite? J'ai essayé ces fonctions:
CREATE OR REPLACE FUNCTION set_field_try1(object anyelement, field text, value text)
RETURNS anyelement
as
$BODY$
DECLARE
value text;
BEGIN
EXECUTE '."' || field || '" := '
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 INTO ."' || 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 ."' || field || '" := ; SELECT ; END;'
INTO object
USING value, object;
return object;
END;
$BODY$
LANGUAGE plpgsql
et quelques variantes.
Appeler set_field_tryX
ne marche pas. Je reçois toujours le message "ERREUR: erreur de syntaxe sur ou près...".
Comment puis-je accomplir?
Notes:
- le paramètre est
anyelement
et le champ peut être n'importe quel champ du type composite. Je ne peux pas juste utiliser un objet.nom. - Je m'inquiète de L'injection SQL. Tout conseil à ce sujet serait apprécié, mais ce n'est pas ma question.
6 réponses
plus rapide avec hstore
depuis Postgres 9.0 , avec le module additionnel hstore
installé dans votre base de données il ya une solution très simple et rapide avec le #=
opérateur ça ...
remplacer [s] les champs de
record
par les valeurs correspondantes dehstore
.
pour installer le module:
CREATE EXTENSION hstore;
exemples:
SELECT my_record #= '"field"=>"value"'::hstore; -- with string literal
SELECT my_record #= hstore(field, value); -- with values
Les valeurs doivent être moulées à text
et vice versa, évidemment.
exemple de fonctions plpgsql avec plus de détails:
- boucle sans fin dans la fonction de déclenchement
- attribuer à nouveau par clé dans un Postgres déclencheur
presque aussi rapide avec json
il existe des solutions similaires, mais actuellement non documentées (à partir de la page 9.5) avec json
(pg+ 9.3) ou jsonb
(pg+ 9.4), intégré dans les Postgres, donc vous n'avez pas besoin d'un module supplémentaire.
voir la réponse de @Geir pour plus de détails.
sans hstore
et json
si vous êtes sur une ancienne version ou si vous ne pouvez pas installer le module supplémentaire hstore
ou ne pouvez pas supposer qu'il est installé, voici une version améliorée de ce que j'ai posté précédemment. Encore plus lent que l'opérateur hstore
, cependant:
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 ''
ELSE '().' || 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;
appel:
CREATE TEMP TABLE t( a int, b text); -- Composite type for testing
SELECT f_setfield(NULL::t, 'a', '1');
Notes
-
une distribution explicite de la valeur
_val
au type de données cible n'est pas nécessaire, une chaîne littérale dans la requête dynamique serait forcée automatiquement, empêchant le sous-jeu surpg_type
. Mais j'ai fait un pas de plus: -
remplacer
quote_literal(_val)
par insertion directe de valeur via la clauseUSING
. Sauve un appel de fonction et deux plâtres, et est plus sûr de toute façon.text
est forcé au type cible automatiquement dans PostgreSQL moderne. (N'a pas testé les versions avant 9.1.) -
array_to_string(ARRAY())
est plus rapide questring_agg()
. -
pas de variables nécessaires, Pas de
DECLARE
. De moins en moins de devoirs. -
Pas de sous-requête dans le SQL dynamique.
().field
est plus rapide. -
pg_typeof(_comp_val)::text::regclass
fait la même chose que
(SELECT typrelid FROM pg_catalog.pg_type WHERE oid = pg_typeof()::oid)
pour les types composites valides, juste plus rapide.
Cette dernière modification est basée sur l'hypothèse quepg_type.typname
est toujours identique aupg_class.relname
associé pour les types composites enregistrés, et la double coulée peut remplacer le sous-vernis. J'ai lancé ce test dans une grande base de données pour vérifier, et il est venu vide comme prévu: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
-
l'utilisation d'un paramètre
INOUT
unRETURN
explicite . C'est juste un raccourci de notation. Pavel ne va pas aimer, il préfère une déclaration expliciteRETURN
...
tout mis ensemble c'est presque deux fois plus rapide que la version précédente.
réponse originale (périmée):
le résultat est une version qui est ~ 2,25 fois plus rapide . Mais Je n'aurais probablement pas pu le faire sans la deuxième version de Pavel.
en outre, cette version évite la plupart du casting au texte et en arrière en faisant tout dans une seule requête, de sorte qu'il devrait être beaucoup moins sujet à l'erreur.
Testé avec PostgreSQL 9.0 et 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 =
THEN quote_literal() || '::'|| (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()::oid)
AND a.attnum > 0
AND a.attisdropped = false
ORDER BY a.attnum
) x
);
EXECUTE 'SELECT ' || _list || ' FROM (SELECT .*) x'
USING
INTO ;
RETURN ;
END
$func$ LANGUAGE plpgsql STABLE;
j'ai écrit une deuxième version de la fonction setfield. Il fonctionne sur postgres 9.1 Je ne l'ai pas testé sur des versions plus anciennes. Ce n'est pas un miracle (vue performance), mais il est plus robuste et environ 8 fois plus rapide que le précédent.
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()::oid)
AND a.attnum > 0
LOOP
IF _name = THEN
_value := ;
ELSE
EXECUTE 'SELECT (().' || quote_ident(_name) || ')::text' INTO _value USING ;
END IF;
_values[_attnum] := COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', '');
END LOOP;
EXECUTE 'SELECT (' || quote_ident(pg_typeof()::text) || ' ''(' || array_to_string(_values,',') || ')'').*' INTO ;
RETURN ;
END;
$function$;
mise à jour/mise en garde: Erwin souligne qu'il s'agit de actuellement non documenté , et le manuel indique qu'il ne devrait pas être possible de modifier les enregistrements de cette façon.
utiliser hstore ou Pavel's solution à la place.
cette solution simple basée sur json est presque aussi rapide que hstore, et ne nécessite que Postgres 9.3 ou plus récent. Ce devrait être une bonne option si vous ne pouvez pas utiliser l'extension hstore, et la différence de performance devrait être négligeable. Benchmarks: https://stackoverflow.com/a/28673542/1914376
a) nous pouvons soit le faire en ligne par cast/concat. La fonction Json nécessite Postgres 9.3:
SELECT json_populate_record(
record
, ('{"'||'key'||'":"'||'new-value'||'"}')::json
);
b) ou en ligne en utilisant les fonctions de Postgres 9.4 .
SELECT json_populate_record (
record
,json_object(ARRAY['key', 'new-value'])
);
Note: I choisi json_object(TABLEAU[clé,valeur]), car il est un peu plus rapide que json_build_object(clé,valeur):
pour cacher les détails de la fonte, vous pouvez utiliser a) dans une fonction, avec peu de surimpression.
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;
"SELECT INTO" outside plpgsql (in dynamic SQL context) a un sens différent de celui que vous attendez - il stocke un résultat de requête à la table.
la Modification de n'importe quel champ est possible, mais pas simple
CREATE OR REPLACE FUNCTION public.setfield(a anyelement, text, text)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
begin
create temp table aux as select .*;
execute 'update aux set ' || quote_ident() || ' = ' || quote_literal();
select into * from aux;
drop table aux;
return ;
end;
$function$
Mais ce code n'est pas très efficace, n'est pas possible d'écrire ce bien en plpgsql. Tu peux trouver une bibliothèque C, ça devrait faire l'affaire.
installation d'Essai et les critères de référence v2
Erwin encouragé à reproduire son benchmark dans ce fil ( https://stackoverflow.com/a/7782839/1914376 ), donc j'ai modifié son code avec des données de test synthétiques et j'ai ajouté à la fois la solution hstore et la solution json de ma réponse (et une solution json par Pavel trouvé dans un autre thread) Le benchmark est maintenant exécuté comme une requête, ce qui rend plus facile de capturer les résultats.
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 .*;
execute 'update aux set ' || quote_ident() || ' = ' || quote_literal();
select into * from aux;
drop table aux;
return ;
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()::oid)
LOOP
IF _name = THEN
_value := ;
ELSE
EXECUTE 'SELECT (().' || quote_ident(_name) || ')::text' INTO _value USING ;
END IF;
_values[_attnum] := COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', '');
END LOOP;
EXECUTE 'SELECT (' || pg_typeof()::text || '''(' || array_to_string(_values,',') || ')'').*' INTO ;
RETURN ;
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 =
THEN quote_literal()
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()::oid)
ORDER BY a.attnum
) x
);
EXECUTE '
SELECT ' || _list || '
FROM (SELECT .*) x'
USING
INTO ;
RETURN ;
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 ''
ELSE '().' || 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; $$;
Résultats des tests sur 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)
Mise À Jour Mars 2015:
Largement dépassé maintenant. Considérons le nouveau benchmark de @Geir avec des variantes plus rapides.
installation d'Essai et les critères de référence
j'ai pris les trois solutions présentées (par Oct. 16th, 2011) et a effectué un test sur PostgreSQL 9.0. Vous trouverez la configuration complète ci-dessous. Seules les données de test ne sont pas incluses car j'ai utilisé une base de données de la vie réelle (données non synthétiques). Tout est encapsulé dans son propre schéma pour une utilisation non-intrusive.
je voudrais encourager quiconque veut reproduire le test. Peut-être avec postgres 9.1? Et ajouter vos résultats ici? :)
-- 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 .*;
execute 'update aux set ' || quote_ident() || ' = ' || quote_literal();
select into * from aux;
drop table aux;
return ;
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()::oid)
LOOP
IF _name = THEN
_value := ;
ELSE
EXECUTE 'SELECT (().' || quote_ident(_name) || ')::text' INTO _value USING ;
END IF;
_values[_attnum] := COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', '');
END LOOP;
EXECUTE 'SELECT (' || pg_typeof()::text || '''(' || array_to_string(_values,',') || ')'').*' INTO ;
RETURN ;
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 =
THEN quote_literal()
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()::oid)
ORDER BY a.attnum
) x
);
EXECUTE '
SELECT ' || _list || '
FROM (SELECT .*) x'
USING
INTO ;
RETURN ;
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;
Repères
j'ai lancé les requêtes quelques fois pour remplir le cache. Les résultats présentés sont les meilleurs de cinq durées d'exécution totales avec EXPLAIN ANALYZE
.
Rirst round avec 1000 lignes
le premier prototype de Pavel sort la mémoire partagée avec plus de lignes.
Pavel 1: 2445.112 ms
SELECT x.setfield (f, 'company','test') FROM tmp_f limit 1000;
Pavel 2: 263.753 ms
SELECT x.setfield2(f, 'company','test') FROM tmp_f limit 1000;
Erwin 1: 120.671 ms
SELECT x.setfield3(f, 'company','test') FROM tmp_f limit 1000;
un autre essai avec 5183 lignes.
Pavel 2: 1327.429 ms
SELECT x.setfield2(f, 'company','test') FROM tmp_f;
Erwin1: 588.691 ms
SELECT x.setfield3(f, 'company','test') FROM tmp_f;