Выполнить отложенный триггер только один раз в строке в PostgreSQL - PullRequest
11 голосов
/ 20 января 2012

У меня есть отложенный триггер 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 ]

13 голосов
/ 21 января 2012

Это сложная проблема. Но это можно сделать с помощью триггеров на столбец и условного запуска триггера , представленных в PostgreSQL 9.0 .

Для этого решения вам необходим флаг «update» в строке . Для простоты используйте столбец 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();

Test

Запустите 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;
8 голосов
/ 21 января 2012

Я не знаю, как свести выполнение триггера к одному разу (обновленной) строке на транзакцию, но вы можете эмулировать это с таблицей 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;

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

...