web-dev-qa-db-ja.com

PostgreSQLのトリガーから他の挿入/更新された行にアクセスする方法は?

MS SQLのソリューション

MS SQLトリガー関数には、操作の影響を受けるすべての行が格納されるdeletedおよびinsertedシステムテーブルがあります。更新された行をカウントできます。

_set @updatedCount = (select count(*) from deleted)
_

または最小値を見つける:

_set @updatedMinimumCol1 = (select min(col1) from deleted)
_

PostgreSQLの問題

_FOR EACH ROW_トリガーの場合、OLDおよびNEWシステムレコードを使用できますが、トリガーの呼び出しごとに1行しか格納されません。トリガーの呼び出しは分離されているため、ユーザーが10行を更新すると、トリガーは10回呼び出されますが、毎回、10行すべてについてではなく、現在の行約1つしか知ることができません。

_FOR EACH STATEMENT_の場合、更新された行にアクセスするメカニズムがまったくわかりません。 PostgreSQL v9.6を使用しています、_OLD TABLE_および_NEW TABLE_はv10で導入されました。

PostgreSQLでは、ステートメントレベルのトリガーで古いテーブルと新しいテーブルを参照することはできません。つまり、SQL標準のOLD TABLE句とNEW TABLE句によって参照される古い行と新しい行の両方またはそのいずれかを含むテーブルです。


transaction_timestamp()追加列で試してください

DEFAULT transaction_timestamp()を使用して特別な列をメインテーブルに追加し、それを使用して、更新されたばかりの行を他の行と区別することができますが、1つのトランザクションに複数の_INSERTs/UPDATEs_を含めることができるため、これは解決策ではありませんトランザクションのタイムスタンプは同じになります。この問題を回避するために、各ステートメントの後にトリガーでこのタイムスタンプ列をクリアすることができますが、そのようなクリアが更新トリガーを再度発行する場合の実行方法-無限更新トリガーの呼び出しになります。

したがって、この試行は失敗しました。


PostgreSQLの悪い解決策

私が知る唯一の方法は、

最初に _FOR EACH ROW_トリガーを使用してcollect集計関数のような現在の統計(最小およびカウント)。一時テーブルを使用して呼び出し間で格納します(このトリガーは行ごとに1回呼び出されます)。しかし、どの行が最後であるかはわかりません(いつseこの統計になるか)。

_CREATE TEMP TABLE IF NOT EXISTS _stats (
  _current_min   int,
  _current_count int
) ON COMMIT DROP;

IF EXISTS(SELECT 1 FROM _stats LIMIT 1) THEN
  --Current row is not first, there is statistics for previous rows.
  UPDATE _stats
  SET _current_min   = (CASE WHEN NEW.col1 < _current_min THEN NEW.col1
                        ELSE _current_min END)
    , _current_count = _current_count + 1;
ELSE
  --There is no stats because current row is first for this INSERT/UPDATE
  INSERT INTO _stats (_current_min, _current_count)
  VALUES (NEW.col1, 1);
END IF;
_

2番目 _FOR EACH STATEMENT_トリガーを使用してse収集された統計。 忘れないでください一時テーブルをクリアします(ユーザーが1つのトランザクションで複数のINSERT/UPDATEを実行すると、古い統計が一時テーブルに残り、次のすべての計算が破損します!)。

より複雑なタスクの場合 __stats_と同じ方法で一時テーブルinsertedおよびdeletedを作成できます。


回避策

PostgreSQLでは、INSERT/UPDATE/DELETEにRETURNING句を使用して、操作の影響を受けるすべての行の新しい値を取得できます。その後、それらを操作できますが、INSERT/UPDATEを使用する各関数は、このテクノロジーを実装する必要があります===> 1.そのようなINSERT/UPDATEを使用する関数の追加コード-RETURNINGの複製。 2.そのような技術を新しい機能に実装することを忘れることができます。 3.(トリガーのように)必要な操作が自動的に呼び出されないため、データが破損します。


質問

おそらく、INSERT/UPDATEの影響を受けるすべての行にアクセスするためのより良い方法を知っていますか?

2
Evgeny Nozdrev

the docs を参照してください。ステートメントトリガーから新旧のレコードにアクセスできるはずです。

CREATE TRIGGER some_table_update_trigger
  AFTER UPDATE ON some_table
  REFERENCING NEW TABLE AS newtab OLD TABLE AS oldtab
  FOR EACH STATEMENT
  EXECUTE PROCEDURE do_something_with_newtab_and_oldtab();
4
ewramner

PostgreSQL 10以降については、 @ ewramnerの回答 を参照してください。

以前のバージョンでは、2つのソリューションが見つかりました。両方は、insertedトリガーでdeletedおよびAFTERテーブルを使用する場合にのみ機能します。


解決策1.一時テーブル_insertedおよび_deleted。

最初に、_BEFORE FOR EACH ROW_トリガーで一時テーブルを作成し、それらに入力します:

_CREATE TRIGGER trigger_fill_sys_tables
  BEFORE INSERT OR UPDATE OR DELETE
  ON public.ttest2
  FOR EACH ROW
EXECUTE PROCEDURE public.tr_fill_sys_tables();


CREATE OR REPLACE FUNCTION public.tr_fill_sys_tables()
  RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  EXECUTE 'CREATE TEMP TABLE IF NOT EXISTS _deleted (LIKE ' || tg_table_schema || '.' || tg_relname || ');';
  IF tg_op <> 'INSERT' THEN
    INSERT INTO _deleted
      SELECT old.*;
  END IF;

  EXECUTE 'CREATE TEMP TABLE IF NOT EXISTS _inserted (LIKE ' || tg_table_schema || '.' || tg_relname || ');';
  IF tg_op <> 'DELETE' THEN
    INSERT INTO _inserted
      SELECT new.*;
  END IF;

  IF tg_op <> 'DELETE' THEN
    RETURN new;
  ELSE
    RETURN old;
  END IF;
END;
$$;
_

newおよびoldシステムレコードを使用して、トリガーが呼び出されるたびに現在のレコードにアクセスできます。しかし、1行目が呼び出されたとき、2行目はわかりません。さらに行が存在するかどうかはわかりません。それが理由です

秒。_AFTER EACH STATEMENT_で、すでに収集されているすべての行をトリガーします。次の表を使用できます。

_CREATE TRIGGER trigger_use_sys_tables
  AFTER INSERT OR UPDATE OR DELETE
  ON ttest2
  FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_use_sys_tables();


CREATE OR REPLACE FUNCTION public.tr_use_sys_tables()
  RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
  _row record;
BEGIN
  --If 0 rows was affected by statement, tr_fill_sys_tables() will NOT be called, _inserted will NOT be created. To avoid a crash, check it:
  IF NOT EXISTS(SELECT 1
                FROM pg_class
                WHERE relname = '_inserted') THEN
    RETURN NULL;
  END IF;

  --Work with sys tables.
  --Note: changing data in them will not affect to main table!
  --Note: changing data in main table can fire this trigger again and fall into infinity loop. CREATE TEMP TABLE _lock() before UPDATE and DROP it after one to check if trigger was called recursively.
  FOR _row IN
  SELECT
      COALESCE(n.id, o.id) AS id
    , o.data               AS old_data
    , n.data               AS new_data
  FROM _inserted n
    FULL OUTER JOIN _deleted o ON n.id = o.id
  LOOP
    RAISE NOTICE 'id = %, old data = %, new data = %', _row.id, _row.old_data, _row.new_data;
  END LOOP;

  --DO NOT FORGET to drop the tables!
  --Just clear is not a solution, since next INSERT/UPDATE/DELETE can work with another table with different structure
  DROP TABLE _deleted;
  DROP TABLE _inserted;

  RETURN NULL;
END;
$$;
_

ソリューション2.表の追加の列。

トリガーで挿入/更新された文字列を再度更新する場合に適しています。 DELETEトリガーでは機能しません。ソリューション1を参照してください。

最初に、列_trans_timest timestamp_をメインテーブルに追加します。

2番目、_BEFORE FOR EACH ROW_トリガーを介してtransaction_timestamp()を書き込みます:

_CREATE TRIGGER trigger_trans_mark
  BEFORE INSERT OR UPDATE
  ON public.ttest
  FOR EACH ROW
EXECUTE PROCEDURE public.tr_ttest_trans_mark();

CREATE OR REPLACE FUNCTION public.tr_ttest_trans_mark()
  RETURNS trigger AS $$
BEGIN
  IF tg_op = 'INSERT' THEN --to not crash when checking "old" record
    new.trans_timest = transaction_timestamp();
  ELSE
    IF old.trans_timest IS NULL THEN --if we are clearing marks, do not set them again
      new.trans_timest = transaction_timestamp();
    END IF;
  END IF;
  RETURN new;
END;
$$
LANGUAGE 'plpgsql';
_

3番目、_AFTER FOR EACH STATEMENT_では、このマークを使用して、この_INSERT/UPDATE_の影響を受ける行を他の行と区別できます。 このトリガーのマークをクリアすることを忘れないでください(ユーザーが1つのトランザクションで複数のINSERT/UPDATEを実行する場合、それらはすべて同じtrans_timestを持ち、混合されます)。ただし、このマークをクリアできるのは、まだクリアされていない場合のみです(UPDATEトリガーでUPDATEを呼び出すと、それ自体が呼び出されます。このチェックを行わないと、無限ループに陥ります)。

_CREATE TRIGGER trigger_use_mark
  AFTER INSERT OR UPDATE
  ON public.ttest
  FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_ttest_use_mark();

CREATE OR REPLACE FUNCTION public.tr_ttest_use_mark()
  RETURNS trigger AS $$
BEGIN
  IF NOT EXISTS(SELECT 1
            FROM public.ttest t
            WHERE t.trans_timest = transaction_timestamp()
            LIMIT 1) THEN --To avoid infinity loop
    RETURN NULL;
  END IF;

    --Work with marked rows.
    ...

  --DO NOT FORGET to clear marks!
  UPDATE public.ttest
  SET trans_timest = NULL --update this rows again only simultaniously with clearing of marks!
  WHERE trans_timest = transaction_timestamp();

  RETURN NULL;
END;
$$
LANGUAGE 'plpgsql';
_
1
Evgeny Nozdrev