【问题标题】:Return pre-UPDATE column values using SQL only仅使用 SQL 返回 pre-UPDATE 列值
【发布时间】:2011-12-16 22:01:57
【问题描述】:

I posted a related question,但这是我谜题的另一部分。

我想从已更新的行中获取列的 OLD 值 - 不使用触发器(也不使用存储过程,也不使用任何其他额外的非 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;

如果我可以在子查询的末尾做FOR UPDATE ON my_table,那将是神圣的(并解决我的其他问题/问题)。但这行不通:不能将其与GROUP BY 结合使用(这是计算计数所必需的)。然后我可以只取那些 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

但这行不通; old_my_table 在连接之外不可见; RETURNING 子句对此视而不见。

我早已数不清自己做过的所有尝试;我已经研究了好几个小时了。

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


更新:我在上面的非通用代码中有错字。在 Erwin Brandstetter 建议它应该可以工作后,我重试了。既然我花了这么长时间才找到这种解决方案,也许我的尴尬是值得的?至少这是为了后代现在...... :>

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

   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

COUNT(*) 是根据 Flimzy 在对我的另一个(上面链接)问题的评论中提出的建议。

Please see my other question 用于正确实现并发甚至非阻塞版本;此查询仅显示如何从更新中获取旧值和新值,忽略错误/错误的并发位。

【问题讨论】:

  • 为什么不能使用规则或触发器?
  • 恕我直言,最好的方法是使旧行具有历史意义,无论是通过显式 SQL,还是通过重写规则或触发器。
  • @Flimzy: 1. 如果一个人无法访问这些东西(尽管 可以),如果它可以纯粹在 SQL/单个查询中完成...... 2. 规则/触发器是一个完整的“另一个调试辣酱玉米饼馅”。 3. 保持简单、直接的 SQL 并使用一个查询来完成所有操作很好。再次感谢 count(*) 提醒,寿! :>
  • @wildplasser:目标是找回 (1) 更改的内容和 (2) 更改之前的内容(至少)。这对于事先没有明确知道要更改的内容的作业很方便,但是程序应该知道旧的 val 用于处理什么。 (例如,在故障排除之前/之后输出。)历史行是不必要的混乱,规则和触发器不仅是杂乱无章的(对于这个用例),而且还需要“更多”(安全、访问等)。对于那些没有/想要/需要这些的人来说,这个解决方案是最好的。

标签: sql postgresql concurrency sql-update subquery


【解决方案1】:

问题

The manual explains:

可选的RETURNING 子句导致UPDATE 计算并返回 基于实际更新的每一行的值。任何表达式使用 表的列,和/或FROM 中提到的其他表的列,可以 被计算。表列的 新(更新后)值是 使用RETURNING 列表的语法与 SELECT的输出列表。

我的大胆强调。无法访问 RETURNING 子句中的旧行。您可以使用触发器或单独的SELECT 来解决此限制之前 UPDATE 包装在事务中或包装在 CTE 中,如评论。

但是,如果您在 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;

返回:

 old_id | old_name | tbl_id |  name
--------+----------+--------+---------
  3     | Old Guy  | 23     | New Guy

用于自联接的列必须是UNIQUE NOT NULL。在简单示例中,WHERE 条件位于同一列 tbl_id,但这只是巧合。适用于任何条件。

我使用 8.4 到 13 的 PostgreSQL 版本对此进行了测试。

INSERT 不同:

并发写入负载的解决方案

有多种方法可以避免在同一行上进行并发写入操作的竞争条件。 (注意,对不相关行的并发写操作完全没有问题。)简单、缓慢且确定(但代价高昂)的方法是使用SERIALIZABLE isolation level 运行事务:

BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE ... ;
COMMIT;

但这可能是矫枉过正。并且需要做好重复操作的准备,以防序列化失败。

更简单、更快(并且与并发写入负载一样可靠)是对要更新的一个行的显式锁定:

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;

注意WHERE 条件是如何移动到子查询的(同样,可以是任何东西),并且只有自联接(在UNIQUE NOT NULL 列上)保留在外部查询中.这保证了只处理被内部SELECT 锁定的行。稍后,WHERE 条件可能会解析为一组不同的行。

见:

db小提琴here
sqlfiddle

【讨论】:

  • 在 9.1 上也能正常工作(如果没有,我会非常感到惊讶)
  • 不要挑剔,但我可能会建议编辑您的答案,@ErwinBrandstetter,因为路人可能会停止阅读顶部(“无法完成 ") 并错过你的完全震撼的证明它可以!无论如何,再次感谢! :o)
  • 它运行良好,直到它不起作用。对我来说,以相当小的延迟(大约 10 毫秒)运行具有不同参数的相同请求会发生这种情况。在这种情况下,后续请求将具有来自后续请求之一的旧值(读作“具有不同键的对象”),而不是正在运行的请求。 所以在负载较重的情况下请谨慎使用!
  • @MattDiPasquale:我们正在以不同的别名加入同一个表的第二个实例。我们需要一个连接条件,否则它将是一个交叉连接(笛卡尔积)。
  • 如果目标表没有UNIQUE NOT NULL列,我们可以使用ctid系统列加入; WHERE x.ctid = y.ctid.
【解决方案2】:

您可以使用SELECT 子查询。

示例:Update 用户的电子邮件 RETURNING 旧值。

  1. RETURNING子查询

    UPDATE users SET email = 'new@gmail.com' WHERE id = 1
    RETURNING (SELECT email FROM users WHERE id = 1);
    
  2. PostgreSQL WITH Query (Common Table Expressions)

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

    这已经在我的本地数据库上运行了多次,但我不确定WITH 中的SELECT 是否能保证在UPDATE 之前始终如一地执行,因为“WITH 中的子语句被执行与主查询同时进行。”

【讨论】:

  • 两种解决方案在高负载下是否始终如一地工作?如果是,请证明。如果没有,如何修改它们以便它们这样做?
  • 听起来应该是一个问题,那么。您始终可以链接到此以获取上下文...
  • @2: "所有语句都使用同一个快照执行(参见第 13 章),因此它们无法“看到”彼此对目标表的影响。这减轻了无法预测的影响行更新的实际顺序,这意味着返回数据是在不同 WITH 子语句和主查询之间传达更改的唯一方式。” -- postgresql.org/docs/9.3/static/queries-with.html
【解决方案3】:

proposed by @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;

未经测试,我声称这是可行的:SELECTUPDATE 看到相同的数据库快照。 SELECT 一定会返回旧值(即使您将 CTE 放在带有 UPDATE 的 CTE 之后),而 UPDATE 根据定义返回新值。瞧。

但它会比我的第一个答案慢。

【讨论】:

  • 您是否发现使用子查询方法有任何直接的问题? (性能或其他)
  • @calebboyd:唯一直接的“问题”:它比 CTE 变体更快。还要考虑防御另一个答案中讨论的可能并发问题的版本。
  • 我想这可能是我想知道的——你知道子查询是否会出现类似的并发问题吗? -- 可能是一个单独的问题...
【解决方案4】:

当面临这种困境时,我将垃圾列添加到表中,然后在更新记录时将旧值复制到垃圾列中(然后我返回)。这会使表格有点膨胀,但避免了连接的需要。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-10
    • 1970-01-01
    • 2020-01-30
    相关资源
    最近更新 更多