2025年9月25日: PostgreSQL 18 釋出!
支援的版本: 當前 (18) / 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 (references 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 觸發器,返回值沒有直接影響,但必須為非 null 才能允許觸發操作繼續。請注意,NEWDELETE 觸發器中為 null,因此返回它通常沒有意義。 DELETE 觸發器中的慣用做法是返回 OLD

INSTEAD OF 觸發器(總是行級觸發器,並且只能在檢視上使用)可以返回 null 以指示它們沒有執行任何更新,並且該行的其餘操作應被跳過(即,不觸發後續觸發器,並且在受影響行計數中不包括該行)。否則應返回非 null 值,以指示觸發器已執行請求的操作。對於 INSERTUPDATE 操作,返回值應為 NEW,觸發器函式可以修改它以支援 INSERT RETURNINGUPDATE RETURNING(這也會影響傳遞給任何後續觸發器的行值,或者傳遞給 INSERT 語句中帶有 ON CONFLICT DO UPDATE 子句的特殊 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();

提交更正

如果您在文件中看到任何不正確、與您對特定功能的體驗不符或需要進一步澄清的內容,請使用此表單來報告文件問題。