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の影響を受けるすべての行にアクセスするためのより良い方法を知っていますか?
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();
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';
_