支援的版本:目前 (17) / 16 / 15 / 14 / 13
開發版本:devel
不支援的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2 / 7.1

41.10. 觸發函數 #

PL/pgSQL 可以用來定義資料變更或資料庫事件的觸發函數。觸發函數使用 CREATE FUNCTION 命令建立,將其宣告為沒有參數且傳回類型為 trigger (用於資料變更觸發器) 或 event_trigger (用於資料庫事件觸發器) 的函數。名為 TG_something 的特殊區域變數會自動定義,以描述觸發呼叫的條件。

41.10.1. 關於資料變更的觸發器 #

資料變更觸發器宣告為沒有參數且傳回類型為 trigger 的函數。請注意,即使函數預期接收在 CREATE TRIGGER 中指定的一些參數,也必須宣告為沒有參數 — 這些參數會透過 TG_ARGV 傳遞,如下所述。

PL/pgSQL 函數作為觸發器呼叫時,會在最上層區塊中自動建立幾個特殊變數。它們是

NEW record #

在列層級觸發器中用於 INSERT/UPDATE 操作的新資料庫列。此變數在語句層級觸發器和 DELETE 操作中為 null。

OLD record #

在列層級觸發器中用於 UPDATE/DELETE 操作的舊資料庫列。此變數在語句層級觸發器和 INSERT 操作中為 null。

TG_NAME name #

觸發的觸發器名稱。

TG_WHEN text #

取決於觸發器的定義,為 BEFOREAFTERINSTEAD OF

TG_LEVEL text #

取決於觸發器的定義,為 ROWSTATEMENT

TG_OP text #

觸發觸發器的操作:INSERTUPDATEDELETETRUNCATE

TG_RELID oid (參考 pg_class.oid) #

造成觸發器調用的資料表的物件 ID。

TG_RELNAME name #

造成觸發器調用的資料表。此現在已棄用,並可能在未來的版本中消失。請改用 TG_TABLE_NAME

TG_TABLE_NAME name #

造成觸發器調用的資料表。

TG_TABLE_SCHEMA name #

造成觸發器調用的資料表的綱要。

TG_NARGS integer #

CREATE TRIGGER 陳述式中給定觸發函數的引數數量。

TG_ARGV text[] #

來自 CREATE TRIGGER 陳述式的引數。索引從 0 開始計算。無效索引(小於 0 或大於或等於 tg_nargs)會產生 null 值。

觸發函數必須傳回 NULL 或具有觸發器觸發的資料表結構的記錄/列值。

觸發器以列為單位觸發,在 BEFORE 時機觸發的觸發器可以回傳 null 值,以通知觸發器管理器跳過此列的剩餘操作(也就是說,後續的觸發器不會被觸發,且此列的 INSERT/UPDATE/DELETE 動作不會發生)。如果回傳非 null 值,則操作會使用該列的值繼續進行。回傳與原始值 NEW 不同的列值會改變將被插入或更新的列。因此,如果觸發函數希望觸發動作正常成功而不改變列值,則必須回傳 NEW(或與之相等的值)。若要變更要儲存的列,可以直接在 NEW 中替換單個值,然後回傳修改後的 NEW,或者建立一個完整的新紀錄/列來回傳。在 DELETE 上的 before-trigger 的情況下,回傳值沒有直接的影響,但它必須是非 null 值才能允許觸發動作繼續進行。請注意,NEWDELETE 觸發器中為 null,因此回傳它通常不明智。在 DELETE 觸發器中,常見的做法是回傳 OLD

INSTEAD OF 觸發器(始終是列級觸發器,且只能用於檢視表)可以回傳 null 值,以表示它們沒有執行任何更新,並且此列的剩餘操作應被跳過(也就是說,後續的觸發器不會被觸發,且該列不會計入周圍的 INSERT/UPDATE/DELETE 的受影響列狀態中)。否則,應回傳非 null 值,以表示觸發器已執行所請求的操作。對於 INSERTUPDATE 操作,回傳值應為 NEW,觸發函數可以修改它以支援 INSERT RETURNINGUPDATE RETURNING(這也會影響傳遞給任何後續觸發器的列值,或傳遞給具有 ON CONFLICT DO UPDATE 子句的 INSERT 語句中的特殊 EXCLUDED 別名參考)。對於 DELETE 操作,回傳值應為 OLD

觸發器以列為單位觸發,在 AFTER 時機觸發或觸發器以語句為單位觸發,在 BEFOREAFTER 時機觸發,其回傳值始終會被忽略;它本來就可以是 null。但是,任何這些類型的觸發器仍然可能因引發錯誤而中止整個操作。

範例 41.3 顯示了 PL/pgSQL 中的觸發函數範例。

範例 41.3. 一個 PL/pgSQL 觸發函數

這個範例觸發器確保每次在表中插入或更新列時,當前的使用者名稱和時間都會被印記到列中。並且它檢查是否給定了員工姓名,以及薪水是否為正值。

CREATE TABLE emp (
    empname           text,
    salary            integer,
    last_date         timestamp,
    last_user         text
);

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Check that empname and salary are given
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname cannot be null';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% cannot have null salary', NEW.empname;
        END IF;

        -- Who works for us when they must pay for it?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
        END IF;

        -- Remember who changed the payroll when
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
    FOR EACH ROW EXECUTE FUNCTION emp_stamp();

記錄表格變更的另一種方法是建立一個新表格,該表格為每次發生的插入、更新或刪除保留一列。這種方法可以被認為是稽核表格的變更。範例 41.4 顯示了 PL/pgSQL 中的稽核觸發函數範例。

範例 41.4. 一個用於稽核的 PL/pgSQL 觸發函數

這個範例觸發器確保 emp 表格中任何列的插入、更新或刪除都會被記錄(即稽核)在 emp_audit 表格中。當前的時間和使用者名稱會被印記到列中,以及對其執行的操作類型。

CREATE TABLE emp (
    empname           text NOT NULL,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    stamp             timestamp NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer
);

CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- Create a row in emp_audit to reflect the operation performed on emp,
        -- making use of the special variable TG_OP to work out the operation.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit SELECT 'D', now(), current_user, OLD.*;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit SELECT 'U', now(), current_user, NEW.*;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit SELECT 'I', now(), current_user, NEW.*;
        END IF;
        RETURN NULL; -- result is ignored since this is an AFTER trigger
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
    FOR EACH ROW EXECUTE FUNCTION process_emp_audit();

先前範例的一個變體使用檢視表將主表格連接到稽核表格,以顯示每個條目上次修改的時間。這種方法仍然記錄了表格變更的完整稽核追蹤,但也提供了一個簡化的稽核追蹤檢視,僅顯示每個條目的稽核追蹤中導出的上次修改時間。範例 41.5 顯示了 PL/pgSQL 中檢視表上的稽核觸發器範例。

範例 41.5. 一個用於稽核的 PL/pgSQL 檢視表觸發函數

這個範例使用檢視表上的觸發器使其可更新,並確保檢視表中任何列的插入、更新或刪除都會被記錄(即稽核)在 emp_audit 表格中。記錄當前的時間和使用者名稱,以及執行的操作類型,並且檢視表顯示每列的上次修改時間。

CREATE TABLE emp (
    empname           text PRIMARY KEY,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer,
    stamp             timestamp NOT NULL
);

CREATE VIEW emp_view AS
    SELECT e.empname,
           e.salary,
           max(ea.stamp) AS last_updated
      FROM emp e
      LEFT JOIN emp_audit ea ON ea.empname = e.empname
     GROUP BY 1, 2;

CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
    BEGIN
        --
        -- Perform the required operation on emp, and create a row in emp_audit
        -- to reflect the change made to emp.
        --
        IF (TG_OP = 'DELETE') THEN
            DELETE FROM emp WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            OLD.last_updated = now();
            INSERT INTO emp_audit VALUES('D', current_user, OLD.*);
            RETURN OLD;
        ELSIF (TG_OP = 'UPDATE') THEN
            UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('U', current_user, NEW.*);
            RETURN NEW;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp VALUES(NEW.empname, NEW.salary);

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('I', current_user, NEW.*);
            RETURN NEW;
        END IF;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
    FOR EACH ROW EXECUTE FUNCTION update_emp_view();

觸發器的一個用途是維護另一個表格的摘要表格。產生的摘要可以用來代替原始表格來進行某些查詢 — 通常可以大幅減少執行時間。這種技術通常用於資料倉儲中,在資料倉儲中,測量或觀察到的資料表格(稱為事實表格)可能非常龐大。範例 41.6 顯示了 PL/pgSQL 中的觸發函數範例,該觸發函數維護資料倉儲中事實表格的摘要表格。

範例 41.6. 一個用於維護摘要表格的 PL/pgSQL 觸發函數

這裡詳細介紹的架構部分基於 Ralph Kimball 在 The Data Warehouse Toolkit 中的 雜貨店 範例。

--
-- Main tables - time dimension and sales fact.
--
CREATE TABLE time_dimension (
    time_key                    integer NOT NULL,
    day_of_week                 integer NOT NULL,
    day_of_month                integer NOT NULL,
    month                       integer NOT NULL,
    quarter                     integer NOT NULL,
    year                        integer NOT NULL
);
CREATE UNIQUE INDEX time_dimension_key ON time_dimension(time_key);

CREATE TABLE sales_fact (
    time_key                    integer NOT NULL,
    product_key                 integer NOT NULL,
    store_key                   integer NOT NULL,
    amount_sold                 numeric(12,2) NOT NULL,
    units_sold                  integer NOT NULL,
    amount_cost                 numeric(12,2) NOT NULL
);
CREATE INDEX sales_fact_time ON sales_fact(time_key);

--
-- Summary table - sales by time.
--
CREATE TABLE sales_summary_bytime (
    time_key                    integer NOT NULL,
    amount_sold                 numeric(15,2) NOT NULL,
    units_sold                  numeric(12) NOT NULL,
    amount_cost                 numeric(15,2) NOT NULL
);
CREATE UNIQUE INDEX sales_summary_bytime_key ON sales_summary_bytime(time_key);

--
-- Function and trigger to amend summarized column(s) on UPDATE, INSERT, DELETE.
--
CREATE OR REPLACE FUNCTION maint_sales_summary_bytime() RETURNS TRIGGER
AS $maint_sales_summary_bytime$
    DECLARE
        delta_time_key          integer;
        delta_amount_sold       numeric(15,2);
        delta_units_sold        numeric(12);
        delta_amount_cost       numeric(15,2);
    BEGIN

        -- Work out the increment/decrement amount(s).
        IF (TG_OP = 'DELETE') THEN

            delta_time_key = OLD.time_key;
            delta_amount_sold = -1 * OLD.amount_sold;
            delta_units_sold = -1 * OLD.units_sold;
            delta_amount_cost = -1 * OLD.amount_cost;

        ELSIF (TG_OP = 'UPDATE') THEN

            -- forbid updates that change the time_key -
            -- (probably not too onerous, as DELETE + INSERT is how most
            -- changes will be made).
            IF ( OLD.time_key != NEW.time_key) THEN
                RAISE EXCEPTION 'Update of time_key : % -> % not allowed',
                                                      OLD.time_key, NEW.time_key;
            END IF;

            delta_time_key = OLD.time_key;
            delta_amount_sold = NEW.amount_sold - OLD.amount_sold;
            delta_units_sold = NEW.units_sold - OLD.units_sold;
            delta_amount_cost = NEW.amount_cost - OLD.amount_cost;

        ELSIF (TG_OP = 'INSERT') THEN

            delta_time_key = NEW.time_key;
            delta_amount_sold = NEW.amount_sold;
            delta_units_sold = NEW.units_sold;
            delta_amount_cost = NEW.amount_cost;

        END IF;


        -- Insert or update the summary row with the new values.
        <<insert_update>>
        LOOP
            UPDATE sales_summary_bytime
                SET amount_sold = amount_sold + delta_amount_sold,
                    units_sold = units_sold + delta_units_sold,
                    amount_cost = amount_cost + delta_amount_cost
                WHERE time_key = delta_time_key;

            EXIT insert_update WHEN found;

            BEGIN
                INSERT INTO sales_summary_bytime (
                            time_key,
                            amount_sold,
                            units_sold,
                            amount_cost)
                    VALUES (
                            delta_time_key,
                            delta_amount_sold,
                            delta_units_sold,
                            delta_amount_cost
                           );

                EXIT insert_update;

            EXCEPTION
                WHEN UNIQUE_VIOLATION THEN
                    -- do nothing
            END;
        END LOOP insert_update;

        RETURN NULL;

    END;
$maint_sales_summary_bytime$ LANGUAGE plpgsql;

CREATE TRIGGER maint_sales_summary_bytime
AFTER INSERT OR UPDATE OR DELETE ON sales_fact
    FOR EACH ROW EXECUTE FUNCTION maint_sales_summary_bytime();

INSERT INTO sales_fact VALUES(1,1,1,10,3,15);
INSERT INTO sales_fact VALUES(1,2,1,20,5,35);
INSERT INTO sales_fact VALUES(2,2,1,40,15,135);
INSERT INTO sales_fact VALUES(2,3,1,10,1,13);
SELECT * FROM sales_summary_bytime;
DELETE FROM sales_fact WHERE product_key = 1;
SELECT * FROM sales_summary_bytime;
UPDATE sales_fact SET units_sold = units_sold * 2;
SELECT * FROM sales_summary_bytime;

AFTER 觸發器也可以使用轉換表格來檢查觸發語句變更的整組列。CREATE TRIGGER 命令將名稱指定給一個或兩個轉換表格,然後該函數可以像引用唯讀暫存表格一樣引用這些名稱。範例 41.7 顯示了一個範例。

範例 41.7. 使用轉換表格進行稽核

這個範例產生與範例 41.4相同的結果,但它不是使用每個列觸發一次的觸發器,而是使用每個語句觸發一次的觸發器,在轉換表格中收集相關資訊後。當調用語句修改了許多列時,這可能比列觸發器方法快得多。請注意,我們必須為每種事件建立單獨的觸發器宣告,因為 REFERENCING 子句對於每種情況都必須不同。但這並不妨礙我們在選擇的情況下使用單個觸發函數。(在實踐中,最好使用三個單獨的函數,並避免對 TG_OP 進行執行時測試。)

CREATE TABLE emp (
    empname           text NOT NULL,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    stamp             timestamp NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer
);

CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- Create rows in emp_audit to reflect the operations performed on emp,
        -- making use of the special variable TG_OP to work out the operation.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit
                SELECT 'D', now(), current_user, o.* FROM old_table o;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit
                SELECT 'U', now(), current_user, n.* FROM new_table n;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit
                SELECT 'I', now(), current_user, n.* FROM new_table n;
        END IF;
        RETURN NULL; -- result is ignored since this is an AFTER trigger
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit_ins
    AFTER INSERT ON emp
    REFERENCING NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_upd
    AFTER UPDATE ON emp
    REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_del
    AFTER DELETE ON emp
    REFERENCING OLD TABLE AS old_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();

41.10.2. 事件觸發器 #

PL/pgSQL 可用於定義事件觸發器PostgreSQL 要求要作為事件觸發器呼叫的函數必須宣告為沒有引數且回傳類型為 event_trigger 的函數。

PL/pgSQL 函數作為事件觸發器呼叫時,會在最上層區塊中自動建立幾個特殊變數。它們是

TG_EVENT text #

觸發器觸發的事件。

TG_TAG text #

觸發器觸發的命令標記。

範例 41.8 顯示了 PL/pgSQL 中的事件觸發函數範例。

範例 41.8. 一個 PL/pgSQL 事件觸發函數

這個範例觸發器僅在每次執行支援的指令時,引發一個 NOTICE 訊息。

CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
    RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE FUNCTION snitch();

提交更正

如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步澄清的地方,請使用此表單來回報文件問題。