循环通过 PL/PGSQL 中的 CURSOR 而不锁定表

Posted

技术标签:

【中文标题】循环通过 PL/PGSQL 中的 CURSOR 而不锁定表【英文标题】:Looping through a CURSOR in PL/PGSQL without locking the table 【发布时间】:2019-10-15 15:14:52 【问题描述】:

我有一个简单的 PL/PGSQL 块 Postgres 9.5,它循环遍历表中的记录并有条件地更新一些记录。

这是一个简化的例子:

DO $$
  DECLARE

    -- Define a cursor to loop through records
    my_cool_cursor CURSOR FOR
    SELECT
      u.id          AS user_id,
      u.name        AS user_name,
      u.email       AS user_email
    FROM users u
    ;

  BEGIN

    FOR record IN my_cool_cursor LOOP

      -- Simplified example: 
      -- If user's first name is 'Anjali', set email to NULL
      IF record.user_name = 'Anjali' THEN
        BEGIN
          UPDATE users SET email = NULL WHERE id = record.user_id;
        END;
      END IF;

    END LOOP;
  END;

$$ LANGUAGE plpgsql;

我想直接对我的数据库执行这个块(从我的应用程序,通过控制台等...)。我不想想创建一个FUNCTION() 或存储过程来执行此操作。

问题

问题在于CURSORLOOP 在我的users 表上创建了表级锁,因为外部BEGIN...END 之间的所有内容都在事务中运行。这会阻止针对它的任何其他未决查询。如果users 足够大,这会将其锁定几秒钟甚至几分钟。

我尝试了什么

我尝试在每个UPDATE 之后COMMIT,以便它定期清除事务和锁定。看到这个错误信息我很惊讶:

ERROR:  cannot begin/end transactions in PL/pgSQL
HINT:  Use a BEGIN block with an EXCEPTION clause instead.

我不太确定这是怎么做到的。是否要求我提出EXCEPTION 来强制COMMIT?我尝试阅读the documentation on Trapping Errors,但它只提到ROLLBACK,所以我看不到COMMIT 的任何方法。

    如何在上面的LOOP 中定期COMMIT 一个事务? 更一般地说,我的方法是否正确?有没有更好的方法来循环记录而不锁定表?

【问题讨论】:

游标不会阻塞其他(只读)查询。但我不认为需要一个(缓慢且低效的)光标开始 【参考方案1】:

1.

您不能在 PostgreSQL functionDO 命令中使用 COMMIT根本(plpgsql 或任何其他 PL)。您报告的错误消息很中肯(就 Postgres 9.5 而言):

ERROR:  cannot begin/end transactions in PL/pgSQL

procedure 可以在 Postgres 11 或更高版本中做到这一点。见:

PostgreSQL cannot begin/end transactions in PL/pgSQL In PostgreSQL, what is the difference between a “Stored Procedure” and other types of functions?

在旧版本中实现“自主事务”的解决方法有限:

How do I do large non-blocking updates in PostgreSQL? Does Postgres support nested or autonomous transactions? Do stored procedures run in database transaction in Postgres?

但是对于本案例,您不需要任何这些。

2.

改用简单的UPDATE

UPDATE users
SET    email = NULL
WHERE  user_name = 'Anjali'
AND    email IS DISTINCT FROM NULL;  -- optional improvement

仅锁定实际更新的行(极端情况例外)。而且由于这比整个表上的CURSOR很多,因此锁定也非常简短。

添加的AND email IS DISTINCT FROM NULL 避免了空更新。相关:

Update a column of a table with a column of another table in PostgreSQL How do I (or can I) SELECT DISTINCT on multiple columns?

显式游标很少在 plpgsql 函数中有用。

【讨论】:

谢谢。我给出的UPDATE 声明只是一个简化的例子。我的真实逻辑要复杂得多,所以我认为我不能在单个 UPDATE 中做到这一点作为解决方法。 @user2490003:你仍然可以。也许值得另一个关于细节的问题......【参考方案2】:

如果您想避免长时间锁定行,还可以定义游标WITH HOLD,例如使用DECLARE SQL 语句。

此类游标可以跨事务边界使用,因此您可以在一定数量的更新后COMMIT。您付出的代价是游标必须在数据库服务器上实现。

由于您不能在函数中使用事务语句,您将不得不在应用程序代码中使用过程或提交。

【讨论】:

以上是关于循环通过 PL/PGSQL 中的 CURSOR 而不锁定表的主要内容,如果未能解决你的问题,请参考以下文章

PL/pgSQL“for循环”+选择基本示例(“hello world”)

使用 PostgreSQL PL/pgSQL 在 For 循环中添加月份

Postgres 9.2 PL/pgSQL 循环简单更新

PL/pgSQL 函数 - 遍历特定列并在循环中执行第二个查询

在 Pl/pgSQL 中使用 FOR 循环时,它在 Postgres 11.8 中不起作用

PL/pgSQL 代码的问题