许多初学者在某个时候都会陷入触发器递归的陷阱。通常,解决方案是完全避免递归。但对于某些用例,您可能必须处理触发器递归。本文将告诉您有关该主题需要了解的内容。如果您曾经被错误消息“超出堆栈深度限制”所困扰,那么这里就是解决方案。
01 初学者的错误导致触发递归
触发器是自动更改数据的唯一好方法。约束是确保规则不被违反的“警察”,而触发器是让数据保持一致的工人。理解这一点的初学者可能(非常正确)希望使用触发器来设置updated_at下表中的列:
CREATE TABLE data (id bigintGENERATED ALWAYS AS IDENTITYPRIMARY KEY,value text NOT NULL,updated_at timestamp with time zoneDEFAULT current_timestampNOT NULL
);
插入行时将设置列默认值updated_at,但更新行时不会更改该值。为此,我们的初学者编写了一个触发器:
CREATE FUNCTION set_updated_at() RETURNS triggerLANGUAGE plpgsql AS
$$BEGINUPDATE dataSET updated_at = current_timestampWHERE data.id = NEW.id;RETURN NEW;
END;$$;CREATE TRIGGER set_updated_atAFTER UPDATE ON data FOR EACH ROWEXECUTE FUNCTION set_updated_at();
但这不会按预期发挥作用:
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 dataSET updated_at = current_timestampWHERE data.id = NEW.id"
PL/pgSQL function set_updated_at() line 2 at SQL statement
SQL statement "UPDATE dataSET updated_at = current_timestampWHERE data.id = NEW.id"
PL/pgSQL function set_updated_at() line 2 at SQL statement
...
错误上下文的最后四行不断重复,并表明存在递归问题。
02BEFORE使用触发器避免触发器递归
触发器的问题在于,它更新了最初调用触发器的更新的同一张表。这会再次触发相同的触发器,依此类推,直到堆栈上的递归函数调用过多而超出限制。与大多数其他情况不同,PostgreSQL 的提示在这里毫无用处。由于递归是无限的,因此增加堆栈深度限制只会增加错误消息的时间和错误上下文的长度。
即使不会导致无限递归,上述触发器也不是理想的。由于 PostgreSQL 的多版本实现,每次更新都会产生一个“死元组”,VACUUM稍后必须清理。如果触发器对您刚刚更新的表行执行第二次更新,则会生成第二个死元组。这是低效的,您可能需要调整自动清理以应对额外的工作负载。
PostgreSQL 中避免第二次更新和无限递归的正确解决方案是在BEFORE将新行添加到表之前对其进行修改的触发器:
CREATE FUNCTION set_updated_at() RETURNS triggerLANGUAGE plpgsql AS
$$BEGINNEW.updated_at := current_timestamp;RETURN NEW;
END;$$;CREATE TRIGGER set_updated_atbefore UPDATE ON data FOR EACH ROWEXECUTE FUNCTION set_updated_at();
03一个更严重的触发器递归示例
上述初学者的错误很容易修复,而且在大多数情况下,只要稍加思考就可以轻松避免这种递归。但有时,有些触发器用例很难避免递归。想象一下工作场所和工人的公共卫生数据库:
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使用容易出现无限递归的触发器来执行数据规则
想象一下,法律规定,如果一名工人被隔离,则在同一地址工作的所有人也将被隔离。最好使用触发器来实现这样的数据完整性规则。否则,在应用程序之外执行的数据修改可能会破坏数据的完整性。这样的触发器可能如下所示:
CREATE FUNCTION quarantine_coworkers() RETURNS triggerLANGUAGE plpgsql AS
$$BEGINIF NEW.quarantined IS TRUE THENUPDATE workerSET quarantined = TRUEWHERE worker.address_id = NEW.address_idAND worker.id <> NEW.id;END IF;RETURN NEW;
END;$$;CREATE TRIGGER quarantine_coworkersAFTER UPDATE ON worker FOR EACH ROWEXECUTE FUNCTION quarantine_coworkers();
这看起来基本上是正确的,但是只要一个地址上有更多工作线程,就会出现触发器递归。第一次触发器调用将更新同一地址的其他工作线程,这将再次调用触发器,第二次更新原始工作线程,依此类推,直到无穷:
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 workerSET quarantined = TRUEWHERE worker.address_id = NEW.address_idAND worker.id <> NEW.id"
PL/pgSQL function quarantine_coworkers() line 3 at SQL statement
SQL statement "UPDATE workerSET quarantined = TRUEWHERE worker.address_id = NEW.address_idAND worker.id <> NEW.id"
PL/pgSQL function quarantine_coworkers() line 3 at SQL statement
...
05WHERE使用条件避免无限触发器递归
对于上述情况,您可以通过添加另一个WHERE避免第二次更新行的条件来修复无限递归:
CREATE OR REPLACE FUNCTION quarantine_coworkers() RETURNS triggerLANGUAGE plpgsql AS
$$BEGINIF NEW.quarantined IS TRUE THENUPDATE workerSET quarantined = TRUEWHERE worker.address_id = NEW.address_idAND worker.id <> NEW.idAND NOT worker.quarantined;END IF;RETURN NEW;
END;$$;
现在,如果我用 更新工作器id = 1,触发器将用 更新工作器id = 2。这将第二次调用触发器,但该地址的所有工作器都已被隔离,因此触发器不会更新任何行,并且递归停止。
06使用函数避免无限触发递归pg_trigger_depth()
在我们的示例中,使用条件来避免无限递归并不困难WHERE。但事情并不总是那么容易。还有另一种方法可以停止递归:函数pg_trigger_depth()。此函数用于触发函数并返回递归级别。我们可以使用它作为保护措施来在第一级之后停止递归:
CREATE OR REPLACE FUNCTION quarantine_coworkers() RETURNS triggerLANGUAGE plpgsql AS
$$BEGINIF NEW.quarantined IS TRUE AND pg_trigger_depth() < 2 THENUPDATE workerSET quarantined = TRUEWHERE worker.address_id = NEW.address_idAND worker.id <> NEW.idAND NOT worker.quarantined;END IF;RETURN NEW;
END;$$;
07使用触发WHEN子句来获得更好的性能
WHEN使用上述代码,触发器仍将被调用两次。第二次,触发器函数将返回而不执行任何操作,但我们仍需付出第二次函数调用的代价。中鲜为人知的子句CREATE TRIGGER可以使触发器调用有条件并避免这种开销:
DROP TRIGGER quarantine_coworkers ON worker;CREATE OR REPLACE FUNCTION quarantine_coworkers() RETURNS triggerLANGUAGE plpgsql AS
$$BEGINUPDATE workerSET quarantined = TRUEWHERE worker.address_id = NEW.address_id;AND worker.id <> NEW.idAND NOT worker.quarantined;RETURN NEW;
END;$$;CREATE TRIGGER quarantine_coworkers AFTER UPDATE ON worker FOR EACH ROWWHEN (NEW.quarantined AND pg_trigger_depth() < 2)EXECUTE FUNCTION quarantine_coworkers();
通过此定义,在触发函数被第二次调用之前,递归就会停止,这将显著提高性能。
结论
我们已经了解了如何通过完全避免递归来避免初学者容易犯的错误,即导致无限触发器递归。在无法避免触发器递归的情况下,我们已经了解了如何使用pg_trigger_depth()或精心设计的附加条件在适当的时刻停止递归。我们还了解了可以简化代码并提高性能WHEN的子句。CREATE TRIGGER
#PG证书#PG考试#postgresql培训#postgresql考试#postgresql认证