Выполнить отложенный триггер только один раз в строке в PostgreSQL

у меня отложено AFTER UPDATE trigger в таблице, установите для запуска при обновлении определенного столбца. Это целочисленный тип, который я использую в качестве счетчика.

Я не уверен на 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 ответов


это сложная проблема. Но это можно сделать с триггеры для каждого столбца и выполнение условного триггера представил в 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);

вставьте две строки, чтобы продемонстрировать, что он работает с несколькими строками:

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;

Я не знаю, как свернуть выполнение триггера до одного раза за (обновленную) строку за транзакцию, но вы можете эмулировать это с помощью временной таблицы фиксации, которая отслеживает эти измененные строки и выполняет вашу дорогостоящую операцию только один раз за строку за tx:

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;

конечно, есть риск столкновения имен с этой временной таблицей, поэтому выбирайте разумно.