PostgreSQL中触发器递归的处理 | 翻译

许多初学者在某个时候都会陷入触发器递归的陷阱。通常,解决方案是完全避免递归。但对于某些用例,您可能必须处理触发器递归。本文将告诉您有关该主题需要了解的内容。如果您曾经被错误消息"超出堆栈深度限制"所困扰,那么这里就是解决方案。

01 初学者的错误导致触发递归

触发器是自动更改数据的唯一好方法。约束是确保规则不被违反的"警察",而触发器是让数据保持一致的工人。理解这一点的初学者可能(非常正确)希望使用触发器来设置updated_at下表中的列:

sql 复制代码
CREATE TABLE data (
   id bigint
      GENERATED ALWAYS AS IDENTITY
      PRIMARY KEY,
   value text NOT NULL,
   updated_at timestamp with time zone
      DEFAULT current_timestamp
      NOT NULL
);

插入行时将设置列默认值updated_at,但更新行时不会更改该值。为此,我们的初学者编写了一个触发器:

sql 复制代码
CREATE FUNCTION set_updated_at() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   UPDATE data
   SET updated_at = current_timestamp
   WHERE data.id = NEW.id;

   RETURN NEW;
END;$$;

CREATE TRIGGER set_updated_at
   AFTER UPDATE ON data FOR EACH ROW
   EXECUTE FUNCTION set_updated_at();

但这不会按预期发挥作用:

sql 复制代码
INSERT INTO data (value) VALUES ('initial') RETURNING id;

 id 
════
  1
(1 row)

UPDATE data SET value = 'changed' WHERE id = 1;
ERROR:  stack depth limit exceeded
HINT:  Increase the configuration parameter "max_stack_depth" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.
CONTEXT:  SQL statement "UPDATE data
   SET updated_at = current_timestamp
   WHERE data.id = NEW.id"
PL/pgSQL function set_updated_at() line 2 at SQL statement
SQL statement "UPDATE data
   SET updated_at = current_timestamp
   WHERE data.id = NEW.id"
PL/pgSQL function set_updated_at() line 2 at SQL statement
...

错误上下文的最后四行不断重复,并表明存在递归问题。

02BEFORE使用触发器避免触发器递归

触发器的问题在于,它更新了最初调用触发器的更新的同一张表。这会再次触发相同的触发器,依此类推,直到堆栈上的递归函数调用过多而超出限制。与大多数其他情况不同,PostgreSQL 的提示在这里毫无用处。由于递归是无限的,因此增加堆栈深度限制只会增加错误消息的时间和错误上下文的长度。

即使不会导致无限递归,上述触发器也不是理想的。由于 PostgreSQL 的多版本实现,每次更新都会产生一个"死元组",VACUUM稍后必须清理。如果触发器对您刚刚更新的表行执行第二次更新,则会生成第二个死元组。这是低效的,您可能需要调整自动清理以应对额外的工作负载。

PostgreSQL 中避免第二次更新和无限递归的正确解决方案是在BEFORE将新行添加到表之前对其进行修改的触发器:

sql 复制代码
CREATE FUNCTION set_updated_at() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   NEW.updated_at := current_timestamp;

   RETURN NEW;
END;$$;

CREATE TRIGGER set_updated_at
   before UPDATE ON data FOR EACH ROW
   EXECUTE FUNCTION set_updated_at();

03一个更严重的触发器递归示例

上述初学者的错误很容易修复,而且在大多数情况下,只要稍加思考就可以轻松避免这种递归。但有时,有些触发器用例很难避免递归。想象一下工作场所和工人的公共卫生数据库:

sql 复制代码
CREATE TABLE address (
   id bigint PRIMARY KEY,
   street text,
   zip text NOT NULL,
   city text NOT NULL
);

CREATE TABLE worker (
   id bigint PRIMARY KEY,
   name text NOT NULL,
   quarantined boolean NOT NULL,
   address_id bigint REFERENCES address
);

INSERT INTO address VALUES
   (101, 'Römerstraße 19', '2752', 'Wöllersdorf'),
   (102, 'Heldenplatz', '1010', 'Wien');

INSERT INTO worker VALUES
   (1, 'Laurenz Albe', FALSE, 101),
   (2, 'Hans-Jürgen Schönig', FALSE, 101),
   (3, 'Alexander Van der Bellen', FALSE, 102);

每个工人都有一个状态" quarantined"(不,这篇文章不是在疫情期间写的)。

04使用容易出现无限递归的触发器来执行数据规则

想象一下,法律规定,如果一名工人被隔离,则在同一地址工作的所有人也将被隔离。最好使用触发器来实现这样的数据完整性规则。否则,在应用程序之外执行的数据修改可能会破坏数据的完整性。这样的触发器可能如下所示:

sql 复制代码
CREATE FUNCTION quarantine_coworkers() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   IF NEW.quarantined IS TRUE THEN
      UPDATE worker
      SET quarantined = TRUE
      WHERE worker.address_id = NEW.address_id
         AND worker.id <> NEW.id;
   END IF;

   RETURN NEW;
END;$$;

CREATE TRIGGER quarantine_coworkers
   AFTER UPDATE ON worker FOR EACH ROW
   EXECUTE FUNCTION quarantine_coworkers();

这看起来基本上是正确的,但是只要一个地址上有更多工作线程,就会出现触发器递归。第一次触发器调用将更新同一地址的其他工作线程,这将再次调用触发器,第二次更新原始工作线程,依此类推,直到无穷:

sql 复制代码
UPDATE worker SET quarantined = TRUE WHERE id = 1;
ERROR:  stack depth limit exceeded
HINT:  Increase the configuration parameter "max_stack_depth" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.
CONTEXT:  SQL statement "SELECT 1 FROM ONLY "laurenz"."address" x WHERE "id" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x"
SQL statement "UPDATE worker
      SET quarantined = TRUE
      WHERE worker.address_id = NEW.address_id
         AND worker.id <> NEW.id"
PL/pgSQL function quarantine_coworkers() line 3 at SQL statement
SQL statement "UPDATE worker
      SET quarantined = TRUE
      WHERE worker.address_id = NEW.address_id
         AND worker.id <> NEW.id"
PL/pgSQL function quarantine_coworkers() line 3 at SQL statement
...

05WHERE使用条件避免无限触发器递归

对于上述情况,您可以通过添加另一个WHERE避免第二次更新行的条件来修复无限递归:

sql 复制代码
CREATE OR REPLACE FUNCTION quarantine_coworkers() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   IF NEW.quarantined IS TRUE THEN
      UPDATE worker
      SET quarantined = TRUE
      WHERE worker.address_id = NEW.address_id
         AND worker.id <> NEW.id
         AND NOT worker.quarantined;
   END IF;

   RETURN NEW;
END;$$;

现在,如果我用 更新工作器id = 1,触发器将用 更新工作器id = 2。这将第二次调用触发器,但该地址的所有工作器都已被隔离,因此触发器不会更新任何行,并且递归停止。

06使用函数避免无限触发递归pg_trigger_depth()

在我们的示例中,使用条件来避免无限递归并不困难WHERE。但事情并不总是那么容易。还有另一种方法可以停止递归:函数pg_trigger_depth()。此函数用于触发函数并返回递归级别。我们可以使用它作为保护措施来在第一级之后停止递归:

sql 复制代码
CREATE OR REPLACE FUNCTION quarantine_coworkers() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   IF NEW.quarantined IS TRUE AND pg_trigger_depth() < 2 THEN
      UPDATE worker
      SET quarantined = TRUE
      WHERE worker.address_id = NEW.address_id
         AND worker.id <> NEW.id
         AND NOT worker.quarantined;
   END IF;

   RETURN NEW;
END;$$;

07使用触发WHEN子句来获得更好的性能

WHEN使用上述代码,触发器仍将被调用两次。第二次,触发器函数将返回而不执行任何操作,但我们仍需付出第二次函数调用的代价。中鲜为人知的子句CREATE TRIGGER可以使触发器调用有条件并避免这种开销:

sql 复制代码
DROP TRIGGER quarantine_coworkers ON worker;

CREATE OR REPLACE FUNCTION quarantine_coworkers() RETURNS trigger
   LANGUAGE plpgsql AS
$$BEGIN
   UPDATE worker
   SET quarantined = TRUE
   WHERE worker.address_id = NEW.address_id;
      AND worker.id <> NEW.id
      AND NOT worker.quarantined;

   RETURN NEW;
END;$$;

CREATE TRIGGER quarantine_coworkers 
   AFTER UPDATE ON worker FOR EACH ROW
   WHEN (NEW.quarantined AND pg_trigger_depth() < 2)
   EXECUTE FUNCTION quarantine_coworkers();

通过此定义,在触发函数被第二次调用之前,递归就会停止,这将显著提高性能。

结论

我们已经了解了如何通过完全避免递归来避免初学者容易犯的错误,即导致无限触发器递归。在无法避免触发器递归的情况下,我们已经了解了如何使用pg_trigger_depth()或精心设计的附加条件在适当的时刻停止递归。我们还了解了可以简化代码并提高性能WHEN的子句。CREATE TRIGGER
#PG证书#PG考试#postgresql培训#postgresql考试#postgresql认证

相关推荐
双子座断点12 分钟前
QStringList 使用详解
数据库
bw87672068720 分钟前
金融工程--pine-script 入门
数据库·金融
三日看尽长安花1 小时前
【Redis:原理、架构与应用】
数据库·redis·架构
尘浮生3 小时前
Java项目实战II基于Spring Boot的美食烹饪互动平台的设计与实现(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·微信小程序·小程序·美食
forestqq3 小时前
构建后端为etcd的CoreDNS的容器集群(七)、编写适合阅读的域名管理脚本
运维·数据库·etcd
hongkid3 小时前
MongoDB常用操作
数据库·mongodb
岁岁岁平安3 小时前
mysql上课总结(1)(mysql中的常见的存储引擎)(面试)
数据库·mysql·innodb·存储引擎
幽兰的天空5 小时前
python实现excel数据导入数据库
数据库
尘佑不尘6 小时前
shodan5,参数使用,批量查找Mongodb未授权登录,jenkins批量挖掘
数据库·笔记·mongodb·web安全·jenkins·1024程序员节
传输能手6 小时前
从三方云服务器将数据迁移至本地,如何保障安全高效?
大数据·服务器·数据库