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


13

在PostgreSQL 9.5中,给出了一个使用以下命令创建的简单表:

create table tbl (
    id serial primary key,
    val integer
);

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

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

结果是UPDATE被忽略:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

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

Answers:


15

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

UPDATE看到基础表作为的相同的状态INSERT,这意味着行与val = 1不存在,尚未。该手册在此处进行了说明:

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

每个语句都可以RETURNING子句中看到另一个CTE返回的内容。但是基础表在他们看来都是一样的。

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


15

这是一个实施决策。它在Postgres文档“ WITH查询(公用表表达式)”中进行了描述。有两个与该问题有关的段落。

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

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

在我向pgsql-docs发布建议之后,Marko Tiikkaja解释了(这与Erwin的回答是一致的):

insert-update和insert-delete情况不起作用,因为UPDATE和DELETE无法看到INSERTed行,因为在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;

我们分为两部分,CTE(newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

和主要查询:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

执行流程如下所示:

           initial data: tbl
                id  val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id  val           \
            1    1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

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

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


还有其他类似的情况,例如,如果语句在同一行上有一个INSERT然后有一个DELETE。删除将由于完全相同的原因而失败。

在同一文档页面的下一段中,将介绍其他一些带有update-update和update-delete及其行为的情况。

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

在Marko Tiikkaja的回复中:

显然,update-update和update-delete案例不是由相同的基础实现细节引起的(如insert-update和insert-delete案例)。
update-update案例不起作用,因为它内部看起来像万圣节问题,而Postgres无法知道哪些元组可以更新两次,哪些元组可以重新引入万圣节问题。

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

在insert-update(视您的情况)和类似的insert-delete中,结果是可预测的。只有插入发生,因为第二项操作(更新或删除)无法查看和影响新插入的行。


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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.