在 postgres plpgsql 函数中重用 json 解析的输入

Posted

技术标签:

【中文标题】在 postgres plpgsql 函数中重用 json 解析的输入【英文标题】:Reusing json parsed input in postgres plpgsql function 【发布时间】:2017-03-02 11:28:28 【问题描述】:

我有一个 plpgsql 函数,它接受 jsonb 输入,并使用它首先检查某些内容,然后再次在查询中获取结果。比如:

CREATE OR REPLACE FUNCTION public.my_func(
    a jsonb,
    OUT inserted integer)
    RETURNS integer
    LANGUAGE 'plpgsql'
    COST 100.0
    VOLATILE NOT LEAKPROOF
AS $function$
BEGIN
    -- fail if there's something already there 
    IF EXISTS(
    select t.x from jsonb_populate_recordset(null::my_type, a) f inner join some_table t
    on f.x = t.x and
       f.y = t.y
    ) THEN
    RAISE EXCEPTION 'concurrency violation... already present.';
    END IF;

    -- straight insert, and collect number of inserted
    WITH inserted_rows AS (
        INSERT INTO some_table (x, y, z)
        SELECT f.x, f.y, f.z
        FROM jsonb_populate_recordset(null::my_type, a) f
        RETURNING 1
    )
    SELECT count(*) from inserted_rows INTO inserted
    ;
END

在这里,我在IF 检查和实际插入中都使用了jsonb_populate_recordset(null::my_type, a)。有没有办法进行一次解析 - 也许通过某种变量?或者查询优化器是否会启动并确保解析操作只发生一次?

【问题讨论】:

服务器版本是多少?.. 服务器版本为9.6。 是否需要引发异常? 是的,我确实需要例外(示例已简化)。 【参考方案1】:

如果我理解正确,你会看到这样的东西:

CREATE OR REPLACE FUNCTION public.my_func(
    a jsonb,
    OUT inserted integer)
    RETURNS integer
    LANGUAGE 'plpgsql'
    COST 100.0
    VOLATILE NOT LEAKPROOF
AS $function$
BEGIN
    WITH checked_rows AS (
        SELECT f.x, f.y, f.z, t.x IS NOT NULL as present
        FROM jsonb_populate_recordset(null::my_type, a) f
        LEFT join some_table t
            on f.x = t.x and f.y = t.y
    ), vioalted_rows AS (
        SELECT count(*) AS violated FROM checked_rows AS c WHERE c.present
    ), inserted_rows AS (
        INSERT INTO some_table (x, y, z)
        SELECT c.x, c.y, c.z
        FROM checked_rows AS c
        WHERE (SELECT violated FROM vioalted_rows) = 0
        RETURNING 1
    )
    SELECT count(*) from inserted_rows INTO inserted
    ;

    IF inserted = 0 THEN 
        RAISE EXCEPTION 'concurrency violation... already present.';
    END IF;

END;
$function$;

【讨论】:

【参考方案2】:

JSONB 类型在赋值时不需要解析多次:

虽然 jsonb 数据以分解的二进制格式存储,但由于增加了转换开销,因此输入速度稍慢,但处理速度明显加快,因为不需要重新解析。

Link

jsonb_populate_recordset 函数声明为STABLE:

STABLE 表示该函数不能修改数据库,并且在单个表扫描中,对于相同的参数值,它将始终返回相同的结果,但其结果可能会在 SQL 语句中发生变化。

Link

我不确定。一方面 UDF 调用被视为单个语句,另一方面 UDF 可以包含多个语句。需要澄清。

最后,如果你想缓存这样的歌曲,那么你可以使用数组:

CREATE OR REPLACE FUNCTION public.my_func(
    a jsonb,
    OUT inserted integer)
    RETURNS integer
    LANGUAGE 'plpgsql'
    COST 100.0
    VOLATILE NOT LEAKPROOF
AS $function$
DECLARE
    d my_type[]; -- There is variable for caching 
BEGIN
    select array_agg(f) into d from jsonb_populate_recordset(null::my_type, a) as f;
    -- fail if there's something already there 
    IF EXISTS(
      select *
      from some_table t
      where (t.x, t.y) in (select x, y from unnest(d)))
    THEN
      RAISE EXCEPTION 'concurrency violation... already present.';
    END IF;

    -- straight insert, and collect number of inserted
    WITH inserted_rows AS (
        INSERT INTO some_table (x, y, z)
        SELECT f.x, f.y, f.z
        FROM unnest(d) f
        RETURNING 1
    )
    SELECT count(*) from inserted_rows INTO inserted;
END $function$;

【讨论】:

【参考方案3】:

如果你真的想重复重复使用一个结果set,一般的解决方案是临时表。示例:

Using temp table in PL/pgSQL procedure for cleaning tables

但是,这相当昂贵。看起来您只需要一个 UNIQUE 约束或索引:

简单而安全的UNIQUE约束

ALTER TABLE some_table ADD CONSTRAINT some_table_x_y_uni UNIQUE (x,y);

与您的程序尝试相反,这也是并发安全的(没有竞争条件)。也快得多。

那么函数可以很简单:

CREATE OR REPLACE FUNCTION public.my_func(a jsonb, OUT inserted integer) AS
$func$
BEGIN
   INSERT INTO some_table (x, y, z)
   SELECT f.x, f.y, f.z
   FROM   jsonb_populate_recordset(null::my_type, a) f;

   GET DIAGNOSTICS inserted = ROW_COUNT;  -- OUT param, we're done here
END
$func$  LANGUAGE plpgsql;

如果(x,y) 已经存在于some_table 中,您将获得例外。为约束选择一个有指导意义的名称,在错误消息中报告。

我们可以用GET DIAGNOSTICS 读取命令标签,这比运行另一个计数查询便宜得多。

相关:

How does PostgreSQL enforce the UNIQUE constraint / what type of index does it use?

UNIQUE 约束不可能?

对于 UNIQUE 约束不可行的不太可能的情况,您仍然可以让它相当简单:

CREATE OR REPLACE FUNCTION public.my_func(a jsonb, OUT inserted integer) AS
$func$
BEGIN
   INSERT INTO some_table (x, y, z)
   SELECT f.x, f.y, f.z  -- empty result set if there are any violations
   FROM  (
      SELECT f.x, f.y, f.z, count(t.x) OVER () AS conflicts
      FROM   jsonb_populate_recordset(null::my_type, a) f
      LEFT   JOIN some_table t USING (x,y)
      ) f
   WHERE  f.conflicts = 0;

   GET DIAGNOSTICS inserted = ROW_COUNT;

   IF inserted = 0 THEN
      RAISE EXCEPTION 'concurrency violation... already present.';
   END IF;

END
$func$  LANGUAGE plpgsql;

计算同一查询中的违规次数。 (count() 只计算非空值)。相关:

Best way to get result count before LIMIT was applied

无论如何,您至少应该在some_table (x,y) 上有一个简单的索引。

重要的是要知道 plpgsql 在控制退出函数之前不会返回结果。异常取消返回,用户永远不会得到结果,只有错误消息。 We added a code example to the manual.

但是请注意,在并发写入负载下存在竞争条件。相关:

Is SELECT or INSERT in a function prone to race conditions?

查询规划器会避免重复评估吗?

肯定不是在多个 SQL 语句之间。

即使函数本身定义为STABLEIMMUTABLE(示例中的jsonb_populate_recordset()STABLE),查询规划器也不知道输入参数的值在调用之间没有变化。跟踪并确保它会很昂贵。 实际上,由于 plpgsql 将 SQL 语句视为准备好的语句,这显然是不可能的,因为查询是计划的 before 参数值被提供给计划的查询。

【讨论】:

这个例子被简化了。使用唯一约束无法实现实际用例(它是一个仅插入模型,每次成功更新都有一个新行)。 @ashic: it's an insert only model with a new row for each successful update。好的,那么什么不适用呢?无论如何,您都迫切地需要(x,y) 上的索引以提高性能。 在必要的列上已经有一个索引。这不是独一无二的。问题更多是关于参数的解析(或重新解析),以及是否有办法减少浪费的调用(如果优化器还没有这样做的话)。 @ashic:我为此添加了一个通用和一个特定的替代方案。 感谢您的澄清。我今天晚些时候正在尝试各种选项,看看哪些有效。知道查询计划器不会自动执行此操作肯定会有所帮助。

以上是关于在 postgres plpgsql 函数中重用 json 解析的输入的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Postgres/plpgsql 的视图定义中使用变量

无法执行 plpgsql/postgres 中的函数

将数组从 node-postgres 传递给 plpgsql 函数

如何在不创建函数的情况下运行 plpgsql?

创建语法处或附近的 Plpgsql 函数错误

plpgsql 专家:(记录集)函数的输入和输出