PL/pgSQL 可以用來定義資料變更或資料庫事件的觸發函數。觸發函數使用 CREATE FUNCTION
命令建立,將其宣告為沒有參數且傳回類型為 trigger
(用於資料變更觸發器) 或 event_trigger
(用於資料庫事件觸發器) 的函數。名為 TG_
的特殊區域變數會自動定義,以描述觸發呼叫的條件。something
資料變更觸發器宣告為沒有參數且傳回類型為 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
#取決於觸發器的定義,為 BEFORE
、AFTER
或 INSTEAD OF
。
TG_LEVEL
text
#取決於觸發器的定義,為 ROW
或 STATEMENT
。
TG_OP
text
#觸發觸發器的操作:INSERT
、UPDATE
、DELETE
或 TRUNCATE
。
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 值才能允許觸發動作繼續進行。請注意,NEW
在 DELETE
觸發器中為 null,因此回傳它通常不明智。在 DELETE
觸發器中,常見的做法是回傳 OLD
。
INSTEAD OF
觸發器(始終是列級觸發器,且只能用於檢視表)可以回傳 null 值,以表示它們沒有執行任何更新,並且此列的剩餘操作應被跳過(也就是說,後續的觸發器不會被觸發,且該列不會計入周圍的 INSERT
/UPDATE
/DELETE
的受影響列狀態中)。否則,應回傳非 null 值,以表示觸發器已執行所請求的操作。對於 INSERT
和 UPDATE
操作,回傳值應為 NEW
,觸發函數可以修改它以支援 INSERT RETURNING
和 UPDATE RETURNING
(這也會影響傳遞給任何後續觸發器的列值,或傳遞給具有 ON CONFLICT DO UPDATE
子句的 INSERT
語句中的特殊 EXCLUDED
別名參考)。對於 DELETE
操作,回傳值應為 OLD
。
觸發器以列為單位觸發,在 AFTER
時機觸發或觸發器以語句為單位觸發,在 BEFORE
或 AFTER
時機觸發,其回傳值始終會被忽略;它本來就可以是 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();
PL/pgSQL 可用於定義事件觸發器。PostgreSQL 要求要作為事件觸發器呼叫的函數必須宣告為沒有引數且回傳類型為 event_trigger
的函數。
當 PL/pgSQL 函數作為事件觸發器呼叫時,會在最上層區塊中自動建立幾個特殊變數。它們是
範例 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();
如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步澄清的地方,請使用此表單來回報文件問題。