带有嵌套 CTE 的条件插入?

Yog*_*sch 6 postgresql insert cte postgresql-10

我想弄清楚是否有一种方法可以使嵌套 CTE 适用于这种特殊情况。

考虑以下基于实际应用程序的(高度人为设计的)场景:有一个员工 ID 的单列表。然后是包含所有详细信息的员工属性表。(单列表背后的主要原因通常是理所当然地需要在知道实际员工的任何详细信息之前批量创建和分配新员工 ID。)

现在到手头的任务,我们要为新员工插入详细信息(即姓名),但首先我们需要检查是否已经存在具有该姓名的员工。如果是,我们将简单地返回 id,如果不是,我们将创建一个新的员工记录,然后插入详细信息,最后返回新创建的 id。

要重新创建此测试场景:

CREATE TABLE public.employee (
    id text DEFAULT gen_random_uuid(),
    PRIMARY KEY (id)
);

CREATE TABLE public.employee_details (
    employee_id text,
    name text,
    PRIMARY KEY (employee_id),
    FOREIGN KEY (employee_id) REFERENCES public.employee(id)
);
Run Code Online (Sandbox Code Playgroud)

我试图敲定的查询如下所示。

with 
e as 
    (select name, employee_id from employee_details where name = 'jack bauer'), 

i as (insert into employee_details (name, employee_id) 
    select 'jack bauer', 
        (with a as (insert into employee values(default) RETURNING id) select a.id from a)
    where not exists (select 1 from e) returning name, employee_id) 

select employee_id, name from e
union all 
select employee_id, name from i; 
Run Code Online (Sandbox Code Playgroud)

如果我用一个已经创建的 id 替换嵌套的 CTE(单独执行嵌套的 CTE),它就可以工作(但可能会导致创建一个多余的 id)。也可以简单地将嵌套的 CTE 移动到顶层(所以整个事情看起来像with e as (..), i as (..), a as (..) select .. where not exists...,但这也意味着在不需要插入细节的地方创建了一个多余的员工 ID。我想找出一种方法执行“内联” - 因此只有在not exists子句返回 true 时才会创建新的 id 。

我不断收到错误:

包含数据修改语句的 WITH 子句必须位于顶层。

我想问题在于嵌套的 CTE 返回一个“列”,而如果它得到一个“值”,则整个查询将起作用(它确实如此,当一个人简单地复制文本值而不是 CTE 时)。我确实在这个问题上遇到了一个有点相关的讨论,提到了一个自 9.3 以来已修复的明显错误。我不知道这是否与我在这里遇到的麻烦有关。引用链接的讨论:

解析分析代码似乎认为WITH只能附加到集合操作树中的顶层或叶级SELECT;但语法遵循 SQL 标准,没有说这样的事情

我正在使用 Postgres 10.3。

Erw*_*ter 9

出于这个问题的目的,我将假设employee_details.name为 defined UNIQUE。否则,整个操作将毫无意义。

您不能像尝试的那样嵌套数据修改 CTE(因为您已经发现了困难的方法) - 而且您不需要。此查询将实现您的目标:

WITH e AS (
   SELECT name, employee_id
   FROM   employee_details
   WHERE  name = 'jack bauer'
   )
 , i1 AS (
   INSERT INTO employee             -- no target columns!
   SELECT                           -- empty SELECT list!
   WHERE NOT EXISTS (SELECT FROM e)
   RETURNING id
   )
 , i2 AS (
   INSERT INTO employee_details (name, employee_id) 
   SELECT 'jack bauer', id
   FROM   i1
   RETURNING name, employee_id
   )
SELECT employee_id, name FROM e
UNION ALL 
SELECT employee_id, name FROM i2;
Run Code Online (Sandbox Code Playgroud)

核心功能是INSERT没有目标列和空的SELECT. PostgresSELECT使用默认值填充所有未在 中列出的列。这样我们就可以无条件更换VALUES (default)条件INSERTi1如果未找到给定名称,CTE仅插入一行。

手册:

如果根本没有给出 [target] 列名列表,则默认值是表中所有列的声明顺序;[...]

显式或隐式列列表中不存在的每一列都将填充一个默认值,如果没有,则为其声明的默认值或 null。

这是标准的 Postgres 特定扩展:

此外,标准不允许省略列名称列表但并非所有列都从VALUES子句 or填充query的情况。

最后的 CTEi2仅在i1返回一行时插入一行。瞧。

对相同表的并发写入负载下,这会受到竞争条件的影响。如果你需要排除这种情况,你需要做更多的事情。有关的:

如果没有第二个表中条件 INSERT 的并发症,这将归结为SELECT 或 INSERT的常见情况:

在旁边

"id" text DEFAULT gen_random_uuid()
Run Code Online (Sandbox Code Playgroud)

我强烈建议使用数据类型uuid来存储 UUID。

  • @Yogesch:是的,我的意思是在相同的两个表上并发写入负载。 (2认同)