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

时间:2012-01-20 05:39:57

标签: postgresql database-design triggers 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;$$;

2 个答案:

答案 0 :(得分:12)

这是一个棘手的问题。但可以使用 PostgreSQL 9.0 中引入的per-column triggers and conditional trigger execution来完成。

此解决方案需要每行“更新”标记。为简单起见,请在同一个表中使用boolean列。但它可以在另一个表中,甚至可以在每个事务中使用临时表。

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

这也应该执行,因为......

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

考虑以下

演示

使用单独的模式x作为测试环境在PostgreSQL 9.1中进行测试。

表和虚拟行

-- DROP SCHEMA x;
CREATE SCHEMA x;

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

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

INSERT INTO x.tbl VALUES
 (1, 0, 0, NULL)
,(2, 0, 0, NULL);

触发器功能和触发器

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;

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;

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;

触发器名称是相关的!被称为同一事件的他们按字母顺序执行。

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();

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();

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();

测试

运行UPDATE& SELECT分别看到延迟效应。如果一起执行(在一个事务中),SELECT将显示新的tbl.counter,但旧的tbl2.trig_exec_count

UPDATE x.tbl SET counter = counter + 1;

SELECT * FROM x.tbl;

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

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;

答案 1 :(得分:7)

我不知道如何将触发器执行折叠为每个事务的每个(更新的)行一次,但您可以使用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;

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