为什么不能在同一语句中更新插入 CTE 中的行?

Jef*_*ner 17 postgresql cte

在 PostgreSQL 9.5 中,给定一个使用以下方法创建的简单表:

create table tbl (
    id serial primary key,
    val integer
);
Run Code Online (Sandbox Code Playgroud)

我运行 SQL 来插入一个值,然后在同一个语句中更新它:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;
Run Code Online (Sandbox Code Playgroud)

结果是 UPDATE 被忽略:

testdb=> select * from tbl;
????????????
? id ? val ?
????????????
?  1 ?   1 ?
????????????
Run Code Online (Sandbox Code Playgroud)

为什么是这样?这个限制是 SQL 标准的一部分(即存在于其他数据库中),还是特定于 PostgreSQL 可能在未来修复的部分?在使用查询文件说,多次更新不支持,但没有提到INSERT和UPDATE。

Erw*_*ter 20

具有 CTE 的查询的所有子语句几乎同时发生。即,它们基于数据库的相同快照。

TheUPDATE看到的基础表的状态与 相同INSERT,这意味着 with 的行val = 1尚不存在。手册在这里澄清:

所有语句都使用相同的快照执行(参见第 13 章),因此它们无法“看到”彼此对目标表的影响。

每个语句都可以看到RETURNING子句中另一个 CTE 返回的内容。但是底层的表对他们来说看起来都是一样的。

对于您要执行的操作,您需要两个语句(在单个事务中)。给定的示例实际上应该只是一个单一INSERT的开始,但这可能是由于简化的示例。


ype*_*eᵀᴹ 18

这是一个实施决定。它在 Postgres 文档WITHQueries (Common Table Expressions) 中有描述。有两段与该问题相关。

首先,观察到的行为的原因:

中的子语句WITH彼此并与主查询同时执行。因此,在 中使用数据修改语句时WITH,指定更新实际发生的顺序是不可预测的。所有语句都使用相同的快照执行(参见第 13 章),因此它们无法“看到”彼此对目标表的影响。这减轻了行更新实际顺序的不可预测性的影响,并且意味着RETURNING数据是在不同WITH子语句和主查询之间传达更改的唯一方式。这方面的一个例子是,在...

在我向pgsql-docs发布了一个建议后,Marko Tiikkaja 解释了(同意 Erwin 的回答):

insert-update 和 insert-delete 情况不起作用,因为 UPDATE 和 DELETE 无法看到 INSERT 行,因为它们的快照在 INSERT 发生之前已拍摄。这两种情况没有什么不可预测的。

所以你的语句没有更新的原因可以用上面的第一段(关于“快照”)来解释。当您修改 CTE 时发生的情况是,所有 CTE 和主查询都被执行并“看到”数据(表)的相同快照,就像它们在语句执行之前一样。CTE 可以通过使用RETURNING子句将有关它们插入/更新/删除的内容的信息传递给彼此和主查询,但它们无法直接看到表中的更改。因此,让我们看看您的语句中会发生什么:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;
Run Code Online (Sandbox Code Playgroud)

我们有 2 个部分,CTE ( newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id
Run Code Online (Sandbox Code Playgroud)

和主要查询:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id
Run Code Online (Sandbox Code Playgroud)

执行流程是这样的:

           initial data: tbl
                id ? val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id ? val           \
            1 ?   1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query
Run Code Online (Sandbox Code Playgroud)

结果,当主查询将tbl(如快照中所见)与newval表连接时,它会将一个空表与一个 1 行表连接起来。显然它更新了 0 行。所以该语句从未真正修改新插入的行,这就是您所看到的。

在您的情况下,解决方案是重写语句以首先插入正确的值或使用 2 个语句。一个插入,第二个更新。


还有其他类似的情况,例如如果语句在同一行上有 anINSERT和 a DELETE。删除将因完全相同的原因而失败。

其他一些情况,更新更新和更新删除及其行为在同一文档页面的下一段中进行了解释。

不支持尝试在单个语句中更新同一行两次。只发生了一个修改,但要可靠地预测哪一个并不容易(有时是不可能的)。这也适用于删除已在同一语句中更新的行:仅执行更新。因此,您通常应该避免尝试在单个语句中两次修改单个行。特别要避免编写可能影响由主语句或同级子语句更改的相同行的 WITH 子语句。这种声明的影响是不可预测的。

在 Marko Tiikkaja 的回复中:

update-update 和 update-delete 情况显然不是由相同的底层实现细节引起的(如 insert-update 和 insert-delete 情况)。
更新更新的情况不起作用,因为它在内部看起来像万圣节问题,并且 Postgres 无法知道哪些元组可以更新两次,哪些可能会重新引入万圣节问题。

所以原因是相同的(如何修改 CTE 以及每个 CTE 如何看到相同的快照)但是这两种情况的细节不同,因为它们更复杂,并且在更新更新的情况下结果可能无法预测。

在插入更新(根据您的情况)和类似的插入删除中,结果是可预测的。只有插入才会发生,因为第二个操作(更新或删除)无法查看和影响新插入的行。


对于尝试多次修改相同行的所有情况,建议的解决方案都是相同的:不要这样做。要么编写修改每一行一次的语句,要么使用单独的(2 个或更多)语句。