PL/pgSQLはトリガプロシージャの定義に使用できます。 トリガプロシージャは、CREATE FUNCTIONコマンドを使って、triggerという戻り値の型を持った引数のない関数として作成されます。 その関数は、たとえ、CREATE TRIGGERにて引数をとるものとしていたとしても、引数を持たないものと宣言しなければなりません。 トリガの引数は、後述する通り、TG_ARGV経由で渡されます。
PL/pgSQL関数がトリガとして呼び出された場合、いくつかの特殊な変数が自動的に最上位レベルのブロックで作成されます。 それらを以下に示します。
RECORDデータ型。 この変数は行レベルのトリガでのINSERT/UPDATE操作によって更新された、新しいデータベースの行を保持します。 この変数は文レベルのトリガではNULLです。
RECORDデータ型。 この変数は、行レベルのトリガでのUPDATE/DELETE操作によって更新される前のデータベースの行を保持します。 この変数は文レベルのトリガではNULLです。
nameデータ型。 実際に発行されたトリガの名前を持つ変数。
textデータ型。 トリガの定義に依存したBEFOREまたはAFTERという文字列。
textデータ型。 トリガの定義に依存したROWまたはSTATEMENTという文字列。
textデータ型。 トリガを発行した操作を示す、INSERT, UPDATEまたはDELETEという文字列。
oidデータ型。 このトリガの呼び出し元になるテーブルのオブジェクトID。
nameデータ型。 このトリガの呼び出し元になるテーブルの名前。
integer型。 CREATE TRIGGER文におけるトリガプロシージャに与えられる引数の数。
text型の配列型。 CREATE TRIGGER文での引数。 このインデックスは0から始まります。 無効なインデックス(0未満やtg_nargs以上)は、NULL値という結果になります。
トリガ関数はNULLまたは、トリガの発行元になったテーブルの構造を正確に持ったレコード/行を返さなければなりません。
BEFOREとして発行された行レベルトリガがNULLを返す場合には、トリガマネージャに実際の行への操作を取りやめるように通知します(つまり、その後にトリガが発行されず、そのINSERT/UPDATE/DELETEはその行に対して実行されません)。 非NULL値を返す場合には、その操作はその行値で処理されます。 元のNEWの値と異なる行値を返すことは、挿入、更新される値を変更することに注意してください。 (しかし、DELETEの場合は直接影響しません。) 格納すべき行を変更するために、NEWの個々の値を直接置き換え、そのNEWを返すことも、新しいレコード/行を完全に作成して返すことも可能です。
文レベルのBEFOREまたはAFTERトリガ、行レベルのAFTERトリガの戻り値は常に無視されます。 NULLとしても構いません。 しかし、これらの種類のトリガでも、エラーを発生させることで操作全体を中断させることが可能です。
例35-1にPL/pgSQLのトリガ関数の例を示します。
例 35-1. 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
-- empname とsalary が与えられていることをチェック
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;
-- 支払い時に問題が起こらないように
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- 誰がいつ変更したかを記録
NEW.last_date := 'now';
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 PROCEDURE emp_stamp();テーブルにおける変更のログを取る他の方法は、挿入、更新または削除の各々に対する行を保有する新テーブルを作成することです。 この方法はテーブルにおける変更の監査と考えることができます。
例35-2は PL/pgSQL による監査用トリガプロシージャの一例を示します。
例 35-2. PL/pgSQLによる 監査用のトリガプロシージャ
このトリガの例では、emp テーブルにおける行の挿入、更新または削除のどれもが emp_audit テーブルの中へ確実に記録 (すなわち監査) されます。 現在時刻とユーザ名は、行った操作の種類と共に 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
--
-- empで行った操作を反映する行をemp_auditに作成
-- 操作の種類を決定するために、特殊な変数TG_OPを活用
--
IF (TG_OP = 'DELETE') THEN
INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
RETURN NEW;
END IF;
RETURN NULL; -- AFTERトリガですので、結果は無視されます
END;
$emp_audit$ language plpgsql;
CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
FOR EACH ROW EXECUTE PROCEDURE process_emp_audit();トリガの使用目的の 1つは、あるテーブルのサマリテーブル (データを抜粋したテーブル) を維持することです。 サマリテーブルは元のテーブルに代わって、ある種の問い合わせに対して使用でき、しばしば実行時間を大幅に縮小します。 通常この手法はデータウェアハウスに使用され、計測または観測データ (ファクトテーブルといいます) を非常に大きくすることができます。 データウェアハウス内のファクトテーブルに対してサマリテーブルを維持するPL/pgSQLのトリガプロシージャの例を例35-3に示します。
例 35-3. サマリテーブルを維持するための PL/pgSQL トリガプロシージャ
ここに述べるスキーマの一部は Ralph Kimball による The Data Warehouse Toolkit の Grocery Store の例に基づいています。
--
-- time dimension と 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);
--
-- 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);
--
-- 更新、挿入および削除によりサマリテーブルの列を修正する関数とトリガ
--
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
-- 増加または減少量を算出
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
-- time_key を変更する更新を禁止します
-- (削除 + 挿入の方法により大部分の変更を行うから
-- それほど厄介ではありません)。
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;
-- サマリテーブルの行を新しい値で更新します。
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;
-- この time_key (すなわち新データ) に対応する行が存在しないことがあります。
IF (NOT FOUND) THEN
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
);
EXCEPTION
--
-- 新しい time_key に対する 2つのトランザクションが
-- データを追加する競合状態を捕捉します。
--
WHEN UNIQUE_VIOLATION THEN
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;
END;
END IF;
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 PROCEDURE maint_sales_summary_bytime();