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

时间:2017-03-02 11:28:29

标签: postgresql exception insert plpgsql

我有一个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

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

3 个答案:

答案 0 :(得分:0)

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

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$;

答案 1 :(得分:0)

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$;

答案 2 :(得分:0)

如果您确实想重复使用结果 set ,那么通用解决方案将是一个临时表。例如:

然而,这相当昂贵。看起来您只需要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读取命令标记,这比运行其他计数查询要便宜得多。

相关:

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()仅计算非空值)。相关:

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

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

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

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

多个SQL语句之间肯定不是

即使函数本身已定义STABLEIMMUTABLE(示例中的jsonb_populate_recordset()STABLE),查询规划器也不知道输入参数的值是呼叫之间保持不变跟踪并确保它是很昂贵的 实际上,由于plpgsql将SQL语句视为预处理语句,因此这是不可能的,因为在参数值被提供给计划查询之前,计划了查询。