【问题标题】:How to set value of composite variable field using dynamic SQL如何使用动态 SQL 设置复合变量字段的值
【发布时间】:2011-10-10 10:25:40
【问题描述】:

鉴于这种类型:

-- 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 注入。对此的任何建议将不胜感激,但这不是我的问题。

【问题讨论】:

    标签: postgresql stored-procedures types composite plpgsql


    【解决方案1】:

    使用hstore 更快

    自 Postgres 9.0 以来,在您的数据库中安装了 additional module hstore 后,#= operator 有一个非常简单快速的解决方案...

    record 中的[s] 字段替换为hstore 中的匹配值。

    安装模块:

    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 一起使用,因此您不需要额外的模块。
    • 也适用于嵌套数组和复合类型。

    小缺点:有点慢。

    See @Geir's added answer for details.

    没有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 上的子查询。但我更进一步:

    • 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 的需要。这只是一个符号快捷方式。 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:谢谢,这值得你投 10 票。 :)
    • 谢谢@Erwin。我还在这个。我发现您的解决方案失败了:CREATE TYPE a as (a1 int); CREATE TYPE b as (b1 a); SELECT setfield3(null::b, 'b1', '(2)');。在这种情况下,来自@Pavel 的 setfield2 有效,但 setfield2 在某些情况下 setfield3 有效:-?
    • @DavidEG:我相信我已经为嵌套复合类型的特殊情况修复了它。事实上,我相信我已经为 any 复合类型修复了它。请尝试再次打破它。 :)
    • @DavidEG:实际上,还必须保护最后一点免受 SQLi 攻击。见修正版。
    • 很好的微优化。我对 INOUT 变量没有任何意见。只是我喜欢 RETURN 语句的显式用法(就像 ADA 要求的那样)。通过使用 RETURN 语句的显式返回值对人为错误更稳健一些,但在某个地方是不可能的(在 PL/pgSQL 中)。
    【解决方案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$;
    

    【讨论】:

    • 非常感谢。也许不是奇迹,但对我来说已经足够了。
    • 我修复了两个错误。希望你没问题? 1) 该类型没有 quote_ident() !它会在 pg 9.0 中使用模式中的类型(“myschema.mytype”)中断。 2)双引号,太!另外,将\' 替换为'' 以避免转义字符串。
    • 我现在意识到,该类型需要quote_ident(),否则它对 SQLi 不安全。所以我取消了那个编辑。但是,这将因 模式限定类型而失败。您必须分别使用 quote_ident() 架构和类型名称:"myschema"."mytype"
    • 我仍在努力。我发现了一个错误,因为类型 CREATE TYPE a as (a1 int); CREATE TYPE b as (b1 a); CREATE TYPE c as (c1 b[]); 运行 SELECT setfield2(null::c, 'c1', '{"(\"(2)\")"}'); 失败。
    • 我无法从触发器调用此函数。当我按照建议使用表调用它时,即NULL::table_name,调用函数时,我得到一个 column "tableoid" not found in data type "table_name"。任何的想法?我正在使用 postgresql v. 9.1。
    【解决方案3】:

    更新/警告: Erwin 指出这是currently undocumented,而manual 表示不可能以这种方式更改记录。

    请改用 hstore 或 Pavel's solution

    这个简单的基于 json 的解决方案几乎与 hstore 一样快,并且只需要 Postgres 9.3 或更高版本。如果您不能使用 hstore 扩展,这应该是一个不错的选择,并且性能差异应该可以忽略不计。基准测试:https://stackoverflow.com/a/28673542/1914376

    a) 我们可以通过 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[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;
    

    【讨论】:

    • 请注意,这是自 Postgres 13 以来的文档! :)
    【解决方案4】:

    plpgsql 之外的“SELECT INTO”(在动态 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 库,应该这样做。

    【讨论】:

    • 到您的代码 - 动态 SQL 必须只包含纯 SQL - 不可能使用任何 PL 语句 - 比如 :=
    • 谢谢@Pavel。虽然也许它不是最好的,但对我来说已经足够了。但是现在我遇到了另一个问题,该列并不总是text,当它尝试更新时,我得到了column "x" is of type real but expression is of type text。如何动态投射?
    • 它在我的 9.1 中工作 - 最简单的解决方案是重载 setfield 函数以实现双精度: CREATE OR REPLACE FUNCTION public.setfield(a anyelement, text, doble precision) RETURNS anyelement LANGUAGE plpgsql AS $function$开始创建临时表辅助选择 $1.*;执行'更新辅助集' ||报价标识($2) || '=' || 3美元;从 aux 中选择 $1 *;删除表辅助;返回 1 美元;结尾; $函数$
    • 在编写另一个版本的过程中,我在 postgres 9.0 上进行了广泛的测试。这个函数在几千行的测试中最大化了共享内存。服务器有不错的资源。所以这个不适合使用。警告:共享内存不足上下文:SQL 语句“create temp table aux as select $1.*”PL/pgSQL 函数“setfield”第 2 行在 SQL 语句
    【解决方案5】:

    测试设置和基准 v2

    Erwin 鼓励在此线程 (https://stackoverflow.com/a/7782839/1914376) 中重现他的基准测试,因此我使用综合测试数据修改了他的代码,并从我的答案中添加了 hstore 解决方案和 json-solution(以及 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://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)
    

    【讨论】:

    • 有趣!现在才看到。几点说明:1. hstore 解决方案自 9.0 like stated in my answer 起可用。 2. 使用 (anyelement, hstore) 的 hstore 函数应该更快,从而节省了多余的强制转换。您实际上根本不需要函数,只需要表达式my_record #= hsore(field, value)3. 您在我的回答中包含了我的第一个答案(我的基准已过时),但未包含很多改进的更高版本。
    • 我修正了 9.1/9.0 的错字,只是删除了 hstore 功能。哎呀!我不知何故错过了你在替补席上改进的答案,现在包括在内:)。我还添加了 json 作为内联测试。有趣的是 json_object(ARRAY[key,value]) 比 json_build_object(key,value) 快
    • 优秀的帖子。很有意思。还有一件事:function volatility。 Erwin 1、Erwin 2 和 Pavel 2 可以是 STABLE(也更新了我的旧答案),Pavel3 和 Geir 1 可以是 IMMUTABLE。这可能会有所不同 - 如果不是在这个测试中,那么在更复杂的查询的上下文中。
    • 是的,一些复杂的用例应该能够利用不可变(多次相同的输入?)在这个综合基准测试中,函数实际上比IMMUTABLE慢两倍。
    • 较慢IMMUTABLE,这很奇怪。哪一个?这是最近的一个问题,IMMUTABLE 发挥了重要作用(以一种不那么明显的方式):stackoverflow.com/questions/28899042/…
    【解决方案6】:

    2015 年 3 月更新:
    现在基本上已经过时了。考虑具有更快变体的new benchmark by @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 的第一个原型通过更多行来最大化共享内存。

    帕维尔 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;
    

    【讨论】:

    • @Geir:您的编辑已被拒绝,但您可以发布自己的答案。看起来很有趣......
    • 谢谢,我发布了一个新的解决方案以及基于您的代码的可重现基准。我们是否应该以某种方式“合并”基准帖子以使新读者更容易理解?任何建议表示赞赏=)
    猜你喜欢
    • 2019-04-08
    • 2012-07-22
    • 2011-10-28
    • 2015-03-25
    • 2021-12-02
    • 1970-01-01
    • 2019-05-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多