如何使用动态SQL设置复合变量字段的值

14

给定这个类型:

-- 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无效。我总是得到“ERROR:在或附近语法错误...”。

如何完成这个任务?

注意:

  • 该参数为anyelement,并且该字段可以是复合类型中的任何字段。我不能只使用object.name。
  • 我关注SQL注入问题。对此的任何建议将不胜感激,但这不是我的问题。
6个回答

19

使用hstore更快地进行操作

自从Postgres 9.0版本以后,只需在数据库中安装额外的模块hstore,就可以使用#=运算符进行非常简单和快速的操作,它可以...

用来自hstore的匹配值替换record中的字段。

要安装该模块:

CREATE EXTENSION hstore;

示例:

SELECT my_record #= '"field"=>"value"'::hstore;  -- with string literal
SELECT my_record #= hstore(field, value);        -- with values

明显地,值必须被转换为text并且再转回去。

下面是一些具体的plpgsql函数示例:

现在也支持json / jsonb

对于json(pg 9.3+)或jsonb(pg 9.4+)也有类似的解决方案。

SELECT json_populate_record (my_record, json_build_object('key', 'new-value');

这个功能以前没有文档,但从Postgres 13开始官方支持。手册中写到:

但是,如果base不为NULL,则使用其包含的值填充无法匹配的列。

因此,您可以获取任何现有行并填充任意字段(覆盖其中的内容)。

jsonhstore相比的主要优点:

  • 可以在原版Postgres上运行,无需安装其他模块。
  • 也适用于嵌套数组和复合类型。

小缺点:速度稍慢。

有关详细信息,请参见@Geir添加的答案。

没有hstorejson

如果您使用的是旧版本,无法安装附加模块hstore或不能假设已安装该模块,则可以使用我之前发布的改进版本。尽管比hstore操作符慢,但仍然有效:

CREATE OR REPLACE FUNCTION f_setfield(INOUT _comp_val anyelement
                                          , _field text, _val text)
  RETURNS anyelement
  LANGUAGE plpgsql STABLE 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$;

调用:

CREATE TEMP TABLE t( a int, b text);  -- Composite type for testing
SELECT f_setfield(NULL::t, 'a', '1');

注意事项

  • 不需要将值_val显式转换为目标数据类型,动态查询中的字符串字面量会自动强制转换,从而省略了在pg_type上的子查询。但我更进一步:

  • USING子句直接插入值来替换quote_literal(_val)。这样可以节省一个函数调用和两个转换,并且更安全。在现代PostgreSQL中,text会自动强制转换为目标类型。(没有在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语句,这只是一个简写。 Pavel 不喜欢它,他更喜欢显式的RETURN语句...

综合起来,这个版本比之前的版本快两倍


原始(过时的)答案:

结果是一个版本,速度约快了2.25倍。但我可能不能没有在Pavel的第二个版本的基础上构建它。

此外,这个版本避免了大部分文本转换的问题,通过在单个查询中完成所有操作,所以应该更少出错。
测试使用PostgreSQL 9.0 和 9.1

CREATE FUNCTION f_setfield(_comp_val anyelement, _field text, _val text)
  RETURNS anyelement
  LANGUAGE plpgsql STABLE 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$;

@Pavel:谢谢,这来自你的话就相当于十个赞了。 :) - Erwin Brandstetter
谢谢@Erwin。我还在处理这个问题。我发现你的解决方案在这种情况下失败了:CREATE TYPE a as (a1 int); CREATE TYPE b as (b1 a); SELECT setfield3(null::b, 'b1', '(2)');。在这种情况下,来自@Pavel的setfield2有效,但在某些情况下,setfield2会失败,而setfield3则有效 :-? - DavidEG
@DavidEG:a.atttypid <> 0 是什么意思? - Erwin Brandstetter
被删除的属性具有 atttypid = 0,因此基本上与 attisdropped = false 是多余的。 - DavidEG
1
良好的微观优化。我对INOUT变量没有任何意见。只是我喜欢显式使用RETURN语句(就像ADA要求的那样)。通过使用RETURN语句显式返回值可以更加抵御人为错误,但在某些情况下不可能(在PL/pgSQL中)。 - Pavel Stehule
显示剩余5条评论

8

我写了第二个版本的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$;

我还在处理这个问题。我发现了一个错误,给定类型 CREATE TYPE a as (a1 int); CREATE TYPE b as (b1 a); CREATE TYPE c as (c1 b[]); 运行 SELECT setfield2(null::c, 'c1', '{"(\"(2)\")"}'); 失败了。 - DavidEG
SELECT setfield2(NULL::"MY_TABLE", MY_FIELD, "new_value"); - neggenbe
@neggenbe - 啊哈 - 它在表类型上没起作用 - 我修复了代码。现在请检查一下。 - Pavel Stehule
好的,你的更改起作用了 - 函数现在可以工作了。但是,在我的触发器中,我执行 SELECT setfield2(NEW, MY_FIELD, "new_value") INTO _tmp(请注意 INTO _tmp 以避免错误 _query has no destination_),但是在调用之后,NEW.MY_FIELD 仍然具有初始值。如果我将结果放入 NEW 中,我会得到错误 _42804: returned row structure does not match the structure of the triggering table_。我在这里做错了什么? - neggenbe
你的表中是否有一些被删除的列?如果是这种情况,请尝试在该表上运行VACUUM FULL。 - Pavel Stehule
显示剩余5条评论

6
更新/注意: Erwin指出这是目前未记录的,而手册表明不应该通过这种方式改变记录。
使用hstore或Pavel的解决方案代替。
这个简单的基于json的解决方案几乎和hstore一样快,只需要Postgres 9.3或更新版本。如果您不能使用hstore扩展,那么这应该是一个很好的选择,性能差异应该可以忽略不计。基准测试:https://dev59.com/Imsz5IYBdhLWcg3wsaPN#28673542 a) 我们可以通过转换/连接来内联执行。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[key,value]),因为它比json_build_object(key,value)快一些。
为了隐藏转换的细节,您可以在函数中使用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;

1
请注意,自Postgres 13以来,此内容已有文档记录! :) - Erwin Brandstetter

3

"SELECT INTO"在动态SQL上下文中与PL/pgSQL中不同 - 它将查询结果存储到表中。

可以修改任何字段,但并不简单。

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库来完成此任务。


在您的代码中,动态SQL必须仅包含普通SQL语句,不能使用任何PL语句,例如:=。 - Pavel Stehule
谢谢@Pavel。虽然可能不是最好的,但对我来说已经足够了。但现在我又有另一个问题,该列并不总是“text”,当它尝试更新时,我得到了“column 'x' is of type real but expression is of type text”的错误。如何动态转换类型? - DavidEG
1
它在我的9.1版本中运行 - 最简单的解决方案是重载双精度setfield函数: CREATE OR REPLACE FUNCTION public.setfield(a anyelement, text, doble precision) RETURNS anyelement LANGUAGE plpgsql AS $function$ begin create temp table aux as select $1.*; execute 'update aux set ' || quote_ident($2) || ' = ' || $3; select into $1 * from aux; drop table aux; return $1; end; $function$ - Pavel Stehule
在编写另一个版本的过程中,我对postgres 9.0进行了广泛的测试。这个函数在测试几千行数据时耗尽了共享内存。服务器资源还算不错。因此,这不适合使用。警告:共享内存不足 上下文:SQL语句“create temp table aux as select $1.*” PL/pgSQL函数“setfield”第2行的SQL语句。 - Erwin Brandstetter

2

测试设置和基准测试v2

Erwin鼓励在这个帖子中重新创建他的基准测试(https://dev59.com/Imsz5IYBdhLWcg3wsaPN#7782839),因此我使用合成测试数据修改了他的代码,并添加了来自我的答案的hstore解决方案和json解决方案(以及Pavel在另一个帖子中找到的json解决方案)。现在,基准测试作为一个查询运行,更容易捕获结果。

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://dev59.com/GIfca4cB1Zd3GeqPjHeP#28284491
--------------------------------------------------------------------------------------------------
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)

有趣!现在才看到。几个注意点:1. hstore解决方案自9.0以来就已经可用了,正如我在我的答案中所述。 2. hstore函数应该更快,采用**(anyelement, hstore)*,避免冗余转换。实际上你根本不需要一个函数,只需要表达式my_record #= hsore(field, value)3. 你包含了我的第一个答案(我的基准测试已经过时了),但没有包含我答案中大大改进的*后续版本。 - Erwin Brandstetter
我已经修复了9.1/9.0的错别字,并且删除了hstore函数。糟糕!我不知道怎么错过您在工作台上改进的答案,现在已经包含了:)。我还添加了json作为内联测试。有趣的是看到json_object(ARRAY[key,value])比json_build_object(key,value)快。 - Geir Bostad
1
很棒的帖子。非常有趣。还有一件事:**函数的不稳定性**。Erwin 1、Erwin 2和Pavel 2可以是STABLE(我也更新了我的旧答案),Pavel3和Geir 1可以是IMMUTABLE。这可能会产生差异 - 如果不是在这个测试中,那么就是在更复杂的查询上下文中。 - Erwin Brandstetter
使用IMMUTABLE会变慢,这很奇怪。哪一个?这里有一个最近的问题,其中IMMUTABLE产生了很大的差异(以一种不太明显的方式):https://dev59.com/kYjca4cB1Zd3GeqPxX_Y#28903806 - Erwin Brandstetter
确实有点慢!我得到了一致的结果,使用IMMUTABLE时为45毫秒,不使用时为25毫秒。这是针对json函数“setfield_json”的。你可以自己试试看。我之前在其他情况下也见过几次,但忽略了它,因为易变性有时感觉像黑魔法;) 看起来有一些开销? - Geir Bostad
显示剩余2条评论

1

2015年3月更新:
现在已经大部分过时了。请考虑使用@Geir的更快变体进行新基准测试。


测试设置和基准测试

我采用了2011年10月16日提出的三种解决方案,并在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 的第一个原型在更多的行数下达到共享内存的最大值。

Pavel 1:2445.112 毫秒

SELECT x.setfield (f, 'company','test') FROM tmp_f limit 1000;

Pavel 2: 263.753毫秒

SELECT x.setfield2(f, 'company','test') FROM tmp_f limit 1000;

Erwin 1: 120.671毫秒

SELECT x.setfield3(f, 'company','test') FROM tmp_f limit 1000;

另一个测试,共5183行。

Pavel 2: 1327.429毫秒

SELECT x.setfield2(f, 'company','test') FROM tmp_f;

Erwin1: 588.691毫秒

SELECT x.setfield3(f, 'company','test') FROM tmp_f;

@Geir:您的编辑已被拒绝,但您可以发布自己的答案。看起来足够有趣... - Erwin Brandstetter
谢谢,我根据你的代码发布了一个新的可重现基准测试解决方案。我们是否应该以某种方式“合并”基准测试帖子,以便新读者更容易跟进呢?欢迎提供任何建议=) - Geir Bostad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接