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认证

相关推荐
一 乐5 小时前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
美林数据Tempodata6 小时前
大模型驱动数据分析革新:美林数据智能问数解决方案破局传统 BI 痛点
数据库·人工智能·数据分析·大模型·智能问数
野槐6 小时前
node.js连接mysql写接口(一)
数据库·mysql
Zzzone6837 小时前
PostgreSQL日常维护
数据库·postgresql
chxii7 小时前
1.13使用 Node.js 操作 SQLite
数据库·sqlite·node.js
冰刀画的圈7 小时前
修改Oracle编码
数据库·oracle
这个胖子不太裤7 小时前
Django(自用)
数据库·django·sqlite
麻辣清汤7 小时前
MySQL 索引类型及其必要性与优点
数据库·mysql
2501_915374358 小时前
Neo4j 图数据库安装教程(2024最新版)—— Windows / Linux / macOS 全平台指南
数据库·windows·neo4j
it-搬运工9 小时前
3.图数据Neo4j - CQL的使用
数据库·neo4j