仅使用SQL返回pre-UPDATE列值 - PostgreSQL版本

pyt*_*rry 42 sql postgresql subquery sql-update

我有一个相关的问题,但这是我的谜题的另一部分.

我想从UPDATEd的行中获取列的OLD VALUE - 不使用触发器(也不使用存储过程,也不使用任何其他额外的非SQL /查询实体).

我的查询是这样的:

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this worker
    WHERE trans_nbr IN (
                        SELECT trans_nbr
                          FROM my_table
                         GROUP BY trans_nbr
                        HAVING COUNT(trans_nbr) > 1
                         LIMIT our_limit_to_have_single_process_grab
                       )
RETURNING row_id;
Run Code Online (Sandbox Code Playgroud)

如果我可以在子查询结束时执行"FOR UPDATE ON my_table",那将是devine(并修复我的其他问题/问题).但是,这不起作用:不能有这个AND"GROUP BY"(这对于计算trans_nbr的COUNT是必要的).然后我可以直接使用那些trans_nbr并进行查询以获得(即将成为)之前的processing_by值.

我尝试过这样做:

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this worker
     FROM my_table old_my_table
     JOIN (
             SELECT trans_nbr
               FROM my_table
           GROUP BY trans_nbr
             HAVING COUNT(trans_nbr) > 1
              LIMIT our_limit_to_have_single_process_grab
          ) sub_my_table
       ON old_my_table.trans_nbr = sub_my_table.trans_nbr
    WHERE     my_table.trans_nbr = sub_my_table.trans_nbr
      AND my_table.processing_by = old_my_table.processing_by
RETURNING my_table.row_id, my_table.processing_by, old_my_table.processing_by
Run Code Online (Sandbox Code Playgroud)

但这不起作用; old_my_table在联接之外是不可见的; 该RETURNING条款对此视而不见.

我早就忘记了我所做的所有尝试; 我一直在研究这个问题.

如果我能找到一种防弹的方法来锁定子查询中的行 - 而且只有那些行,并且当子查询发生时 - 我试图避免的所有并发问题都会消失......


更新: [WIPES EGG OFF FACE]好的,所以我在上面的非通用代码中写了一个拼写错误,我写的"不起作用"; 它确实......感谢下面的Erwin Brandstetter,他说过会这样做,我重新做了这件事(经过一夜的睡眠,精神焕发的眼睛,以及香槟的bfast).既然很长时间/很难找到这种解决方案,也许我的尴尬值得吗?至少现在这是为了子孙后代......:>

我现在拥有的(有效)是这样的:

   UPDATE my_table
      SET processing_by = our_id_info -- unique to this worker
     FROM my_table AS old_my_table
    WHERE trans_nbr IN (
                          SELECT trans_nbr
                            FROM my_table
                        GROUP BY trans_nbr
                          HAVING COUNT(*) > 1
                           LIMIT our_limit_to_have_single_process_grab
                       )
      AND my_table.row_id = old_my_table.row_id
RETURNING my_table.row_id, my_table.processing_by, old_my_table.processing_by AS old_processing_by
Run Code Online (Sandbox Code Playgroud)

COUNT(*)是从每一个建议Flimzy在我的其他(以上链接)的问题中留言.(我比必要的更具体.[在这种情况下.])

请参阅我的另一个问题,以正确实现并发,甚至是非阻塞版本; 此查询仅显示如何从更新中获取旧值和新值,忽略错误/错误并发位.

Erw*_*ter 65

问题

手册解释说:

optional RETURNING子句导致UPDATE根据实际更新的每一行计算和返回值.FROM可以计算使用表的列和/或其中提到的其他表的列的任何表达式.使用表的列新(更新后)值.RETURNING列表的语法与输出列表的语法相同SELECT.

强调我的.无法访问RETURNING子句中的旧行.你可以做,在触发或者单独SELECT 之前UPDATE,包裹在一个事务中@Flimzy和@wildplasser评论,或包裹在CTE为@MattDiPasquale公布.

但是,如果您在子句中加入表的另一个实例,那么您尝试实现的功能完全正常FROM:

UPDATE tbl x
SET    tbl_id = 23
     , name = 'New Guy'
FROM   tbl y                -- using the FROM clause
WHERE  x.tbl_id = y.tbl_id  -- must be UNIQUE NOT NULL
AND    x.tbl_id = 3
RETURNING y.tbl_id AS old_id, y.name AS old_name
        , x.tbl_id          , x.name;
Run Code Online (Sandbox Code Playgroud)

返回:

 old_id | old_name | tbl_id |  name
--------+----------+--------+---------
  3     | Old Guy  | 23     | New Guy
Run Code Online (Sandbox Code Playgroud)

SQL小提琴.

我用PostgreSQL版本从8.4到9.6进行了测试.

它有所不同INSERT:

处理并发写入负载

有几种方法可以避免并发写操作的竞争条件.简单,缓慢且可靠(但价格昂贵)的方法是使用SERIALIZABLE隔离级别运行事务.

BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE ..;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

但这可能是矫枉过正的.如果序列化失败,您需要准备好重复操作.
简单和快速(和公正的并发写入负载可靠)是在明确的锁定一个行进行更新:

UPDATE tbl x
SET    tbl_id = 24
     , name = 'New Gal'
FROM  (SELECT tbl_id, name FROM tbl WHERE tbl_id = 4 FOR UPDATE) y 
WHERE  x.tbl_id = y.tbl_id
RETURNING y.tbl_id AS old_id, y.name AS old_name, x.tbl_id, x.name;
Run Code Online (Sandbox Code Playgroud)

此相关问题下的更多解释,示例和链接:

  • 每次我有 PG 问题时,我所需要做的就是寻找 @ErwinBrandstetter 的回复。这个家伙相当于《黑客帝国》中的 Neo 和 PostgreSQL。 (6认同)
  • 不要选择尼特,但我可能会建议编辑你的答案,@ ErwinBrandstetter,作为一个过路人可能会停止阅读顶部("_it not not done_")并错过你的**完全摇滚的证据,它可以**!无论如何,再次感谢!:O) (4认同)
  • 它运作良好,直到它没有.对我而言,运行具有不同参数的相同请求时会发生相当小的延迟(大约10毫秒).在这种情况下,后续请求将具有来自其中一个后续请求的旧值(读作"具有不同键的对象"),而不是正在运行的请求.*因此在负载较重的情况下请谨慎使用!* (4认同)
  • @JLarky:好点.我为此添加了一个解决方案. (3认同)
  • 同样适用于9.1(如果没有,我会非常惊讶**) (2认同)
  • 如果目标表没有`UNIQUE NOT NULL`列,我们可以使用`ctid`系统列进行join;`在哪里 x.ctid = y.ctid`。 (2认同)

ma1*_*w28 9

您可以使用SELECT子查询.

示例:更新用户的电子邮件RETURNING旧值.

  1. RETURNING 子查询

    UPDATE users SET email = 'new@gmail.com' WHERE id = 1
    RETURNING (SELECT email FROM users WHERE id = 1);
    
    Run Code Online (Sandbox Code Playgroud)
  2. PostgreSQL WITH Query(公用表表达式)

    WITH u AS (
        SELECT email FROM users WHERE id = 1
    )
    UPDATE users SET email = 'new@gmail.com' WHERE id = 1
    RETURNING (SELECT email FROM u);
    
    Run Code Online (Sandbox Code Playgroud)

    这对我没有失败的本地数据库工作了好几次,但我不知道,如果SELECTWITH保证前的一贯执行UPDATE,因为"与子语句彼此之间以及与主查询并发执行."

  • @2:“所有的语句都是用同一个快照执行的(见第十三章),所以它们不能“看到”彼此对目标表的影响。这减轻了行更新实际顺序的不可预测性的影响,意味着RETURNING 数据是在不同 WITH 子语句和主查询之间传达更改的唯一方法。” -- https://www.postgresql.org/docs/9.3/static/queries-with.html (3认同)
  • 两种解决方案在重负载下都能一致工作吗?如果是,请证明。如果不是,如何修改才能做到这一点? (2认同)
  • 那听起来应该是一个问题.你总是可以链接到这个上下文... (2认同)

Erw*_*ter 6

@MattDiPasquale提出的CTE变体也应该起作用.
有了CTE的舒适手段,我会更明确,但是:

WITH sel AS (
   SELECT tbl_id, name FROM tbl WHERE tbl_id = 3  -- assuming unique tbl_id
   )
, upd AS (
   UPDATE tbl SET name = 'New Guy' WHERE tbl_id = 3
   RETURNING tbl_id, name
   )
SELECT s.tbl_id AS old_id, s.name As old_name
     , u.tbl_id, u.name
FROM   sel s, upd u;
Run Code Online (Sandbox Code Playgroud)

在没有测试的情况下,我宣称这是有效的:SELECTUPDATE查看数据库的相同快照.该SELECT势必返回旧值(即使您将CTE的CTE与后UPDATE),而UPDATE收益率的定义的新值.瞧.

但它会慢于我的第一个答案.

  • 在 READ COMMITTED 中,当更新语句被另一个事务阻塞时,在另一个事务提交后,select 语句将不会重新运行。因此它将返回原始值(存在于两个事务之前),而不是实际被更新语句覆盖的值。我想这取决于用例是否重要。 (2认同)