sql >> データベース >  >> RDS >> PostgreSQL

一連の更新の最後にトリガーを起動するにはどうすればよいですか?

    report_subscriberでフラグを使用するのではなく それ自体、保留中の変更の別のキューを使用したほうがよいと思います。これにはいくつかの利点があります:

    • トリガーの再帰なし
    • 内部では、UPDATE ただのDELETE +再-INSERT 、したがって、キューに挿入する方が、フラグを反転するよりも実際に安価になります
    • 個別のreport_idをキューに入れるだけでよいため、おそらくかなり安価です。 s report_subscriber全体を複製するのではなく レコードを記録し、一時テーブルで実行できるため、ストレージは連続しており、ディスクに同期する必要はありません。
    • キューは現在のトランザクションに対してローカルであるため、フラグを反転するときに競合状態を心配する必要はありません(実装では、UPDATE report_subscriberの影響を受けるレコード SELECTで取得したレコードと必ずしも同じではありません ...)

    したがって、キューテーブルを初期化します。

    CREATE FUNCTION create_queue_table() RETURNS TRIGGER LANGUAGE plpgsql AS $$
    BEGIN
      CREATE TEMP TABLE pending_subscriber_changes(report_id INT UNIQUE) ON COMMIT DROP;
      RETURN NULL;
    END
    $$;
    
    CREATE TRIGGER create_queue_table_if_not_exists
      BEFORE INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
      ON report_subscriber
      FOR EACH STATEMENT
      WHEN (to_regclass('pending_subscriber_changes') IS NULL)
      EXECUTE PROCEDURE create_queue_table();
    

    ...変更が到着したらキューに入れ、すでにキューに入れられているものはすべて無視します:

    CREATE FUNCTION queue_subscriber_change() RETURNS TRIGGER LANGUAGE plpgsql AS $$
    BEGIN
      IF TG_OP IN ('DELETE', 'UPDATE') THEN
        INSERT INTO pending_subscriber_changes (report_id) VALUES (old.report_id)
        ON CONFLICT DO NOTHING;
      END IF;
    
      IF TG_OP IN ('INSERT', 'UPDATE') THEN
        INSERT INTO pending_subscriber_changes (report_id) VALUES (new.report_id)
        ON CONFLICT DO NOTHING;
      END IF;
      RETURN NULL;
    END
    $$;
    
    CREATE TRIGGER queue_subscriber_change
      AFTER INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
      ON report_subscriber
      FOR EACH ROW
      EXECUTE PROCEDURE queue_subscriber_change();
    

    ...そしてステートメントの最後でキューを処理します:

    CREATE FUNCTION process_pending_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
    BEGIN
      UPDATE report
      SET report_subscribers = ARRAY(
        SELECT DISTINCT subscriber_name
        FROM report_subscriber s
        WHERE s.report_id = report.report_id
        ORDER BY subscriber_name
      )
      FROM pending_subscriber_changes c
      WHERE report.report_id = c.report_id;
    
      DROP TABLE pending_subscriber_changes;
      RETURN NULL;
    END
    $$;
    
    CREATE TRIGGER process_pending_changes
      AFTER INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
      ON report_subscriber
      FOR EACH STATEMENT
      EXECUTE PROCEDURE process_pending_changes();
    

    これにはわずかな問題があります:UPDATE 更新順序についての保証はありません。これは、これら2つのステートメントが同時に実行された場合:

    INSERT INTO report_subscriber (report_id, subscriber_name) VALUES (1, 'a'), (2, 'b');
    INSERT INTO report_subscriber (report_id, subscriber_name) VALUES (2, 'x'), (1, 'y');
    

    ...その後、reportを更新しようとすると、デッドロックが発生する可能性があります。 反対の順序で記録します。すべての更新に一貫した順序を適用することでこれを回避できますが、残念ながらORDER BYを添付する方法はありません。 UPDATEに 声明;カーソルに頼る必要があると思います:

    CREATE FUNCTION process_pending_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
    DECLARE
      target_report CURSOR FOR
        SELECT report_id
        FROM report
        WHERE report_id IN (TABLE pending_subscriber_changes)
        ORDER BY report_id
        FOR NO KEY UPDATE;
    BEGIN
      FOR target_record IN target_report LOOP
        UPDATE report
        SET report_subscribers = ARRAY(
            SELECT DISTINCT subscriber_name
            FROM report_subscriber
            WHERE report_id = target_record.report_id
            ORDER BY subscriber_name
          )
        WHERE CURRENT OF target_report;
      END LOOP;
    
      DROP TABLE pending_subscriber_changes;
      RETURN NULL;
    END
    $$;
    

    クライアントが同じトランザクション内で複数のステートメントを実行しようとすると、これはデッドロックする可能性があります(更新順序は各ステートメント内でのみ適用されますが、更新ロックはコミットされるまで保持されます)。 process_pending_changes()を起動することで、これ(一種)を回避できます。 トランザクションの最後に1回だけ(欠点は、そのトランザクション内で、自分の変更がreport_subscribersに反映されないことです。 配列)。

    「コミット時」トリガーの一般的な概要は、入力するのに苦労する価値があると思われる場合です。

    CREATE FUNCTION run_on_commit() RETURNS TRIGGER LANGUAGE plpgsql AS $$
    BEGIN
      <your code goes here>
      RETURN NULL;
    END
    $$;
    
    CREATE FUNCTION trigger_already_fired() RETURNS BOOLEAN LANGUAGE plpgsql VOLATILE AS $$
    DECLARE
      already_fired BOOLEAN;
    BEGIN
      already_fired := NULLIF(current_setting('my_vars.trigger_already_fired', TRUE), '');
      IF already_fired IS TRUE THEN
        RETURN TRUE;
      ELSE
        SET LOCAL my_vars.trigger_already_fired = TRUE;
        RETURN FALSE;
      END IF;
    END
    $$;
    
    CREATE CONSTRAINT TRIGGER my_trigger
      AFTER INSERT OR UPDATE OR DELETE ON my_table
      DEFERRABLE INITIALLY DEFERRED
      FOR EACH ROW
      WHEN (NOT trigger_already_fired())
      EXECUTE PROCEDURE run_on_commit();
    



    1. AccessでCompactandRepairコマンドを使用する方法

    2. SQLServer2008にうるう秒を保存する

    3. ダウンロード後に空のファイル

    4. 行の複数のSUM