在PostgreSQL中每行只执行一次延迟触发器

jja*_*mes 12 postgresql triggers database-design plpgsql

AFTER UPDATE在表上有一个延迟触发器,设置为在某个列更新时触发.这是我用作计数器的整数类型.

我不是100%肯定,但看起来如果我在事务期间将该特定列增加100次,则触发器在事务结束时排队并执行100次.

我希望触发器每行只安排一次,无论我增加了多少次该列.

我能以某种方式这样做吗?或者,如果触发的触发器必须排队,无论它们是否重复,我是否可以在首次运行触发器时清除此队列?

Postgres的版本是9.1.这是我得到的:

CREATE CONSTRAINT TRIGGER counter_change
    AFTER UPDATE OF "Counter" ON "table"
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW
    EXECUTE PROCEDURE counter_change();

CREATE OR REPLACE FUNCTION counter_change()
    RETURNS trigger
    LANGUAGE plpgsql
    AS $$
DECLARE
BEGIN

PERFORM some_expensive_procedure(NEW."id");

RETURN NEW;

END;$$;
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 14

这是一个棘手的问题.但它可以通过PostgreSQL 9.0中引入的每列触发器和条件触发器执行来完成.

对于此解决方案,每行需要"更新"标记.boolean为简单起见,请在同一个表中使用列.但它可能在每个事务的另一个表或甚至临时表中.

每行执行一次昂贵的有效载荷,其中计数器被更新(一次或多次).

这也应该表现良好,因为......

  • ...它避免了在根处多次调用触发器(很好地扩展)
  • ...不会更改其他行(最小化表格膨胀)
  • ......不需要昂贵的异常处理.

考虑以下

演示

在PostgreSQL 9.1中测试,使用单独的模式x作为测试环境.

表和虚拟行

-- DROP SCHEMA x;
CREATE SCHEMA x;

CREATE TABLE x.tbl (
 id int
,counter int
,trig_exec_count integer  -- for monitoring payload execution.
,updated bool);
Run Code Online (Sandbox Code Playgroud)

插入两行以演示它适用于多行:

INSERT INTO x.tbl VALUES
 (1, 0, 0, NULL)
,(2, 0, 0, NULL);
Run Code Online (Sandbox Code Playgroud)

触发功能和触发器

1.)执行昂贵的有效载荷

CREATE OR REPLACE FUNCTION x.trg_upaft_counter_change_1()
    RETURNS trigger AS
$BODY$
BEGIN

 -- PERFORM some_expensive_procedure(NEW.id);
 -- Update trig_exec_count to count execution of expensive payload.
 -- Could be in another table, for simplicity, I use the same:

UPDATE x.tbl t
SET    trig_exec_count = trig_exec_count + 1
WHERE  t.id = NEW.id;

RETURN NULL;  -- RETURN value of AFTER trigger is ignored anyway

END;
$BODY$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

2.)标记行已更新.

CREATE OR REPLACE FUNCTION x.trg_upaft_counter_change_2()
    RETURNS trigger AS
$BODY$
BEGIN

UPDATE x.tbl
SET    updated = TRUE
WHERE  id = NEW.id;
RETURN NULL;

END;
$BODY$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

3.)重置"更新"标志.

CREATE OR REPLACE FUNCTION x.trg_upaft_counter_change_3()
    RETURNS trigger AS
$BODY$
BEGIN

UPDATE x.tbl
SET    updated = NULL
WHERE  id = NEW.id;
RETURN NULL;

END;
$BODY$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

触发器名称是相关的!为同一事件调用,它们按字母顺序执行.

1.)有效负载,仅在尚未"更新"的情况下:

CREATE CONSTRAINT TRIGGER upaft_counter_change_1
    AFTER UPDATE OF counter ON x.tbl
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW
    WHEN (NEW.updated IS NULL)
    EXECUTE PROCEDURE x.trg_upaft_counter_change_1();
Run Code Online (Sandbox Code Playgroud)

2.)标记行已更新,仅在尚未"更新"时:

CREATE TRIGGER upaft_counter_change_2   -- not deferred!
    AFTER UPDATE OF counter ON x.tbl
    FOR EACH ROW
    WHEN (NEW.updated IS NULL)
    EXECUTE PROCEDURE x.trg_upaft_counter_change_2();
Run Code Online (Sandbox Code Playgroud)

3.)重置标志.由于触发条件没有无限循环.

CREATE CONSTRAINT TRIGGER upaft_counter_change_3
    AFTER UPDATE OF updated ON x.tbl
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW
    WHEN (NEW.updated)                 --
    EXECUTE PROCEDURE x.trg_upaft_counter_change_3();
Run Code Online (Sandbox Code Playgroud)

测试

单独运行UPDATESELECT查看延迟效果.如果一起执行(在一个事务中),SELECT将显示新的tbl.counter但旧的tbl2.trig_exec_count.

UPDATE x.tbl SET counter = counter + 1;

SELECT * FROM x.tbl;
Run Code Online (Sandbox Code Playgroud)

现在,多次更新计数器(在一个事务中).有效负载只会执行一次.瞧!

UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;

SELECT * FROM x.tbl;
Run Code Online (Sandbox Code Playgroud)

  • 我知道的派对有点晚了,但是有可能只执行一次有效载荷函数和*last*(仅用于最后的UPDATE stmt)? (2认同)

pil*_*row 8

我不知道将触发器执行折叠为每个事务的每个(更新的)行一次的方法,但您可以使用TEMPORARY ON COMMIT DROP表来模拟这一点,该表跟踪那些已修改的行并且每行每行执行一次昂贵的操作:

CREATE OR REPLACE FUNCTION counter_change() RETURNS TRIGGER
AS $$
BEGIN
  -- If we're the first invocation of this trigger in this tx,
  -- make our scratch table.  Create unique index separately to
  -- suppress avoid NOTICEs without fiddling with log_min_messages
  BEGIN
    CREATE LOCAL TEMPORARY TABLE tbl_counter_tx_once
      ("id" AS_APPROPRIATE NOT NULL)
      ON COMMIT DROP;
    CREATE UNIQUE INDEX ON tbl_counter_tx_once AS ("id");
  EXCEPTION WHEN duplicate_table THEN
    NULL;
  END;

  -- If we're the first invocation in this tx *for this row*,
  -- then do our expensive operation.
  BEGIN
    INSERT INTO tbl_counter_tx_once ("id") VALUES (NEW."id");
    PERFORM SOME_EXPENSIVE_OPERATION_HERE(NEW."id");
  EXCEPTION WHEN unique_violation THEN
    NULL;
  END;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

当然存在与该临时表名称冲突的风险,因此请谨慎选择.