DDL 事件触发(CREATE / ALTER / DROP)
上一节说明了事件触发器有五个事件。其中 ddl_command_start、ddl_command_end、sql_drop 这三个构成了 DDL 的完整生命周期:命令执行前拦截、命令执行后审计、对象被删除时清算。它们不是三个独立的钩子,而是同一件事的三个切面,理解它们的关键在于「谁先谁后」与「每个阶段能看到什么」。
本页把这三个事件的触发顺序、适用范围、信息函数(尤其是 pg_event_trigger_ddl_commands() 与 pg_event_trigger_dropped_objects() 的完整字段)以及典型陷阱逐一讲清。所有字段定义与语义均以 PostgreSQL 18 官方文档为准。
前置知识
阅读本节前,建议先了解:事件触发器概述
基础概念
三个事件在一条 DDL 上的相对顺序
先建立时间轴,后面所有结论都由此展开:
客户端: ALTER TABLE orders ADD COLUMN note text;
│
├─► ① ddl_command_start
│ 命令尚未执行。系统目录还是「旧状态」。
│ 可以读旧状态、可以 RAISE EXCEPTION 拦截。
│ 不能调用 pg_event_trigger_ddl_commands()。
│
├─► ② 内核执行 ALTER TABLE
│ 系统目录被改写。若涉及物理重写,此处会插入 table_rewrite 事件。
│
├─► ③ sql_drop ← 仅当本次命令「删除了对象」时才触发
│ 对象已从系统目录消失。
│ pg_event_trigger_dropped_objects() 列出全部被删对象(含级联)。
│
└─► ④ ddl_command_end
命令已执行完成。
pg_event_trigger_ddl_commands() 列出本次命令涉及的对象。
↑ 注意:sql_drop 一定在它之前
│
└─► 事务提交 / 回滚有一条非常容易记反的规则,官方原文明确写过:The sql_drop event occurs just before the ddl_command_end event trigger for any operation that drops database objects. 也就是说 sql_drop 先于 ddl_command_end,而不是先有 ddl_command_end 再有 sql_drop。
「命令标签」不等于「命令原文」
三个事件都用 WHEN TAG IN (...) 过滤,而过滤依据是命令标签(command tag)。它在 psql 执行完命令后会打印出来:
postgres=# CREATE TABLE demo(id int);
CREATE TABLE ← 这就是命令标签命令标签与事件的关系:
| 命令 | 命令标签 | ddl_command_start | ddl_command_end | 是否可能触发 sql_drop |
|---|---|---|---|---|
CREATE TABLE | CREATE TABLE | 是 | 是 | 否 |
CREATE TABLE AS | CREATE TABLE AS | 是 | 是 | 否 |
SELECT ... INTO | SELECT INTO | 是 | 是 | 否 |
ALTER TABLE | ALTER TABLE | 是 | 是 | 可能(如 DROP COLUMN) |
DROP TABLE | DROP TABLE | 是 | 是 | 是 |
DROP INDEX | DROP INDEX | 是 | 是 | 是 |
COMMENT ON | COMMENT | 是 | 是 | 否 |
GRANT | GRANT | 是 | 是 | 否 |
REVOKE | REVOKE | 是 | 是 | 否 |
REINDEX | REINDEX | 是 | 是 | 否 |
REFRESH MATERIALIZED VIEW | REFRESH MATERIALIZED VIEW | 是 | 是 | 否 |
SECURITY LABEL | SECURITY LABEL | 是 | 是 | 否 |
IMPORT FOREIGN SCHEMA | IMPORT FOREIGN SCHEMA | 是 | 是 | 否 |
sql_drop 的产生条件比名字暗示的更宽:不只是 DROP 命令。任何导致对象从系统目录中消失的命令都可能触发它,因此对 ALTER TABLE ... DROP COLUMN 也可能触发。
三个事件的「身份」
| 事件 | 精确定义(官方语义) | 能否阻断 | 主要信息函数 |
|---|---|---|---|
ddl_command_start | 在 DDL 命令开始执行之前触发 | 能(命令还没跑) | 无(只能用 TG_TAG) |
ddl_command_end | 在 DDL 命令完成执行之后触发 | 能(回滚已执行命令) | pg_event_trigger_ddl_commands() |
sql_drop | 在 ddl_command_end 之前、对象已被删除后触发 | 能(回滚) | pg_event_trigger_dropped_objects() |
三个都能「阻断」,但阻断的机制与后果不同:ddl_command_start 报错时命令根本没执行;ddl_command_end / sql_drop 报错时命令已经执行,靠事务回滚把效果撤销。
触发顺序:按名字的字母顺序
当同一个事件上挂了多个事件触发器时,触发顺序不是创建顺序,而是按触发器名字的字母顺序(字节序):
同一事件上的多个触发器: 按 evtname 的字母序依次触发
trg_a_audit ─► 先
trg_b_guard ─► 后
trg_z_notify ─► 最后这带来一个实用技巧与一个陷阱:
- 技巧:想让「审计」先于「管控」执行,可以靠前缀控制,例如
aaa_audit、zzz_guard。 - 陷阱:若前一个触发器
RAISE EXCEPTION,后面的根本不会执行。名字顺序因此决定了「谁能拦住谁」。
语法与用法
信息函数的调用方式
两个信息函数都是集合返回函数(SRF),必须放在 FROM 子句里当作表使用:
-- ddl_command_end 专用
SELECT * FROM pg_event_trigger_ddl_commands();
-- sql_drop 专用
SELECT * FROM pg_event_trigger_dropped_objects();不能写成 SELECT pg_event_trigger_ddl_commands(); 或 SELECT count(*) INTO n FROM pg_event_trigger_ddl_commands(); 这种「当标量函数用」的形式——它们返回的是多行。
标准触发函数骨架
CREATE FUNCTION my_ddl_handler() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
r record;
BEGIN
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
-- 逐条处理
END LOOP;
END;
$$;FOR ... IN SELECT ... LOOP 是必须的写法:ALTER TABLE 这类命令可能一次改动多个对象,SRF 会返回多行。
在 plpgsql 里可用的专用变量
| 变量 | 类型 | 说明 |
|---|---|---|
TG_EVENT | text | 事件名,如 ddl_command_end |
TG_TAG | text | 命令标签,如 ALTER TABLE |
事件触发器没有 NEW / OLD(那不是行级上下文),也没有 TG_TABLE_NAME 之类的表级变量。
详细说明
pg_event_trigger_ddl_commands() 的完整字段
官方定义:Returns a list of each command executed by the current command, which could be many if the command is ALTER TABLE, etc. The function can only be called in a ddl_command_end event trigger function.
| 字段 | 类型 | 说明 |
|---|---|---|
classid | oid | 包含该对象的系统目录 OID(即 pg_class.oid 之类的目录表自身 OID) |
objid | oid | 对象在系统目录中的 OID |
objsubid | integer | 对象子 ID(例如列号);无子对象时为 0 |
command_tag | text | 命令标签,如 CREATE TABLE、ALTER TABLE |
object_type | text | 对象类型的可读文本,如 table、index、column、view |
schema_name | text | 对象所属 schema 名;若不适用则为 NULL |
object_identity | text | 带 schema 限定的对象标识,如 public.orders |
in_extension | boolean | 该对象是否属于某个扩展(扩展的 DDL 通常应被跳过) |
command | pg_ddl_command | 该命令的内部表示 |
两点必须留意:
command列不能直接输出。它的类型pg_ddl_command是内核内部格式,虽然可以选定、可以存进变量,但输出到客户端会报错。想要可读文本,用command_tag,或者把command强转成text(得到的是内部表示的字符串,可读性有限)。object_identity是「显示用」标识,不是稳定主键。要精确定位对象应使用classid+objid+objsubid三元组。
CREATE FUNCTION audit.dump_ddl() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
r record;
BEGIN
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
RAISE NOTICE 'tag=% type=% schema=% identity=% in_ext=%',
r.command_tag, r.object_type, r.schema_name,
r.object_identity, r.in_extension;
END LOOP;
END;
$$;postgres=# ALTER TABLE demo ADD COLUMN note text;
NOTICE: tag=ALTER TABLE type=table schema=public identity=public.demo in_ext=f
NOTICE: tag=ALTER TABLE type=column schema=public identity=public.demo.note in_ext=f
ALTER TABLE一条 ALTER TABLE 打印了两行:一行是表本身,一行是新增的列。这正是必须用 FOR ... LOOP 的原因。
pg_event_trigger_dropped_objects() 的完整字段
官方定义:Returns a list of all objects dropped by the current command. The function can only be called in a sql_drop event trigger function.
| 字段 | 类型 | 说明 |
|---|---|---|
classid | oid | 系统目录 OID |
objid | oid | 对象 OID |
objsubid | integer | 对象子 ID(列号等) |
object_type | text | 对象类型的可读文本 |
schema_name | text | 所属 schema;不适用时为 NULL |
object_name | text | 对象名(不含 schema) |
object_identity | text | 带 schema 限定的标识 |
address_names | text[] | 见下 |
address_args | text[] | 与 address_names 配套 |
original | boolean | 见下 |
normal | boolean | 见下 |
is_temporary | boolean | 对象是否属于临时命名空间 |
四个「含义不直观」的字段需要专门解释:
original:被删的对象是原始对象还是被删除时连带消失的对象。
DROP TABLE orders; -- orders 是 original
-- orders_pkey、pg_toast_xxx 是「连带」的,original = falsenormal:该对象是否属于「正常」删除路径。被 DROP CASCADE 连带删掉的依赖对象是 normal = true,但与「内部生成的依赖」(如 TOAST、索引)区分开来。
address_names / address_args:这两个数组用于重建对象地址,主要服务于逻辑复制/DDL 复制这类场景。它们描述「对象在树形命名空间中的路径」。例如被删的对象是某个表上的一个索引时,address_names 会给出到达该索引的路径名,address_args 给出路径中需要的参数。对绝大多数审计用例,这两个字段用不上。
is_temporary:临时表在会话级 schema(pg_temp_N)里,审计时通常要排除掉。
CREATE FUNCTION audit.dump_drop() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
o record;
BEGIN
FOR o IN SELECT * FROM pg_event_trigger_dropped_objects() LOOP
RAISE NOTICE 'type=% identity=% original=% normal=% temp=%',
o.object_type, o.object_identity,
o.original, o.normal, o.is_temporary;
END LOOP;
END;
$$;postgres=# DROP TABLE demo;
NOTICE: type=table identity=public.demo original=t normal=t temp=f
NOTICE: type=index identity=public.demo_pkey original=f normal=t temp=f
NOTICE: type=table identity=pg_toast.pg_toast_16405 original=f normal=f temp=f
DROP TABLE注意 object_identity 在这里包含了被级联删除的对象(主键索引、TOAST 表),这正是 sql_drop 相对 ddl_command_end 的独特价值:它能看到级联删除的全貌。
sql_drop 里「对象已经没了」
这是最容易写出 bug 的一处。sql_drop 触发时,对象已经从系统目录中被删除。因此:
-- 错误!sql_drop 里查不到对象
CREATE FUNCTION bad_drop() RETURNS event_trigger LANGUAGE plpgsql AS $$
DECLARE o record;
BEGIN
FOR o IN SELECT * FROM pg_event_trigger_dropped_objects() LOOP
-- 此时 public.demo 已经不存在,下面的查询返回 0 行或报错
PERFORM 1 FROM pg_class WHERE oid = o.objid; -- 找不到
END LOOP;
END;
$$;所以你不能在 sql_drop 里「读取被删对象的属性」(比如你想先查出这张表有多少行再记录日志——做不到)。要在删除前拿到对象信息,必须用 ddl_command_start:
| 需求 | 应该用哪个事件 |
|---|---|
| 删除前读取对象属性(行数、大小、所有者) | ddl_command_start(此时对象还在) |
| 删除后拿到「级联删了什么」的完整清单 | sql_drop |
| 拿到本次 DDL 命令涉及的完整对象列表 | ddl_command_end |
组合用法通常是:ddl_command_start 先抓取快照存到临时表/自定义 GUC,sql_drop 再结合两者写审计。
ddl_command_start 适用与不适用
ddl_command_start 覆盖的 DDL 命令包括:CREATE、ALTER、DROP、COMMENT、GRANT、REVOKE、IMPORT FOREIGN SCHEMA、REINDEX、REFRESH MATERIALIZED VIEW、SECURITY LABEL,以及会被当作 CREATE TABLE AS 处理的 SELECT INTO。
不适用于以下几类命令(在这些命令上挂 ddl_command_start 不会触发):
| 不触发的场景 | 说明 |
|---|---|
| 共享对象上的 DDL | databases、roles、tablespaces 等集群级对象,不属于单个数据库 |
| 参数级权限命令 | ALTER ... SET 类的全局参数变更 |
ALTER SYSTEM | 变更配置文件,不是数据库内对象 DDL |
| 针对事件触发器本身的命令 | CREATE/ALTER/DROP EVENT TRIGGER 不触发事件触发器 |
最后一条是安全网:如果事件触发器能拦截 ALTER EVENT TRIGGER ... DISABLE,那就没人能关掉它了。官方设计上明确把「对事件触发器自身的命令」排除在外。这也意味着你不能通过事件触发器阻止别人删除事件触发器。
事务性:报错发生在哪一阶段
ddl_command_start 里 RAISE EXCEPTION
└─► 命令未执行,客户端收到 ERROR,事务进入 aborted 状态
ddl_command_end 里 RAISE EXCEPTION
└─► 命令已执行,但整个子事务/事务回滚,效果被撤销
sql_drop 里 RAISE EXCEPTION
└─► 同上,删除被回滚三者都能阻断 DDL,但报错信息的「来源」对使用者是一样的(都表现为 DDL 失败)。官方文档在示例中大量使用 ddl_command_start + RAISE EXCEPTION 实现「禁止某类命令」的策略,因为它是唯一不需要付出执行代价(命令根本没跑)就能拦截的位置。
ALTER TABLE 的「一对多」特性
ddl_command_end 的信息函数返回的是每个被操作的对象一行,而不是「每个命令一行」。这造成一些反直觉的输出:
ALTER TABLE t ADD COLUMN a int, ADD COLUMN b int;
→ 返回 3 行(表 1 行 + 列 2 行)因此触发器函数里不能假设「一次调用 → 一条记录」。若要做「每条 DDL 一条日志」的审计,要么在函数里聚合成一行,要么在日志表上接受「一条 DDL 多条日志」。
-- 聚合写法:一次调用只写一行,把对象清单拼成数组
CREATE FUNCTION audit.one_row_per_ddl() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
objs text[];
BEGIN
SELECT array_agg(object_identity ORDER BY object_identity) INTO objs
FROM pg_event_trigger_ddl_commands();
INSERT INTO audit.ddl_log(logged_at, event, tag, objects)
VALUES (clock_timestamp(), TG_EVENT, TG_TAG, objs);
END;
$$;in_extension:扩展的 DDL 通常要跳过
CREATE EXTENSION 会执行扩展脚本里的大量 DDL。这些语句产生的对象都带 in_extension = true。
-- 审计时通常忽略扩展内部对象,否则日志会被 pg_stat_statements 之类灌满
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands()
WHERE NOT in_extension LOOP
...
END LOOP;sql_drop 侧没有同名字段,但可以用 object_identity 是否属于扩展 schema 来近似判断。更可靠的做法是在 CREATE EXTENSION 前后用 ALTER EVENT TRIGGER ... DISABLE 临时屏蔽(见下)。
实战示例
示例一:完整的 DDL 审计(命令 + 对象)
CREATE SCHEMA IF NOT EXISTS audit;
CREATE TABLE audit.ddl_audit (
id bigserial PRIMARY KEY,
logged_at timestamptz NOT NULL DEFAULT clock_timestamp(),
db_name text NOT NULL DEFAULT current_database(),
who text NOT NULL DEFAULT session_user,
event text NOT NULL,
tag text NOT NULL,
object_type text,
object_identity text,
in_extension boolean,
schema_name text
);
CREATE OR REPLACE FUNCTION audit.on_ddl_end() RETURNS event_trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, audit AS $$
DECLARE r record;
BEGIN
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
INSERT INTO audit.ddl_audit
(event, tag, object_type, object_identity, in_extension, schema_name)
VALUES (TG_EVENT, r.command_tag, r.object_type,
r.object_identity, r.in_extension, r.schema_name);
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_ddl_audit
ON ddl_command_end
EXECUTE FUNCTION audit.on_ddl_end();postgres=# CREATE UNIQUE INDEX idx_demo_code ON demo(code);
CREATE INDEX
postgres=# SELECT tag, object_type, object_identity FROM audit.ddl_audit ORDER BY id;
tag | object_type | object_identity
-------------+-------------+-----------------------
CREATE INDEX | index | public.idx_demo_code
(1 row)示例二:捕获级联删除的完整清单
CREATE TABLE audit.drop_audit (
id bigserial PRIMARY KEY,
logged_at timestamptz NOT NULL DEFAULT clock_timestamp(),
tag text NOT NULL,
object_type text,
object_identity text,
is_original boolean,
is_temp boolean
);
CREATE OR REPLACE FUNCTION audit.on_sql_drop() RETURNS event_trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, audit AS $$
DECLARE o record;
BEGIN
FOR o IN SELECT * FROM pg_event_trigger_dropped_objects() LOOP
INSERT INTO audit.drop_audit
(tag, object_type, object_identity, is_original, is_temp)
VALUES (TG_TAG, o.object_type, o.object_identity, o.original, o.is_temporary);
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_drop_audit
ON sql_drop
EXECUTE FUNCTION audit.on_sql_drop();postgres=# DROP TABLE demo CASCADE;
DROP TABLE
postgres=# SELECT object_type, object_identity, is_original
FROM audit.drop_audit ORDER BY id;
object_type | object_identity | is_original
-------------+----------------------------+-------------
table | public.demo | t
index | public.demo_pkey | f
table | pg_toast.pg_toast_16405 | f
(3 rows)DROP TABLE demo CASCADE 只写了一条 SQL,但审计表里留下了三行——主键索引和 TOAST 表都是被连带删除的。这是 ddl_command_end 做不到的。
示例三:只审计业务 schema,忽略扩展与临时对象
CREATE OR REPLACE FUNCTION audit.on_ddl_filtered() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE r record;
BEGIN
FOR r IN SELECT * FROM pg_event_trigger_ddl_commands()
WHERE NOT in_extension
AND schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
AND schema_name NOT LIKE 'pg_temp%'
LOOP
RAISE NOTICE '[DDL] % % -> %', r.command_tag, r.object_type, r.object_identity;
END LOOP;
END;
$$;
CREATE EVENT TRIGGER trg_ddl_filtered
ON ddl_command_end
EXECUTE FUNCTION audit.on_ddl_filtered();过滤掉 pg_catalog / information_schema / pg_toast / 临时 schema 之后,日志里剩下的基本就是真正需要关注的结构变更。
示例四:在同一事件上控制触发顺序
-- 字母序:trg_a_* 先于 trg_z_*
CREATE EVENT TRIGGER trg_a_allowlist ON ddl_command_start
EXECUTE FUNCTION guard.record_intent();
CREATE EVENT TRIGGER trg_z_guard ON ddl_command_start
EXECUTE FUNCTION guard.enforce_policy();postgres=# DROP TABLE app.orders;
-- 先执行 trg_a_allowlist(记录意图),再执行 trg_z_guard(策略拦截)
ERROR: 策略拒绝:禁止在 app schema 中 DROP 对象 app.orders如果你希望拦截器先跑(拦截后就不再记录),把管控触发器命名为字母序更小的名字即可。
示例五:临时屏蔽审计(用于扩展安装、数据导入)
官方文档给出的惯用法是用事务包住「禁用 → 执行 → 启用」,因为对事件触发器自身的 ALTER EVENT TRIGGER 不会触发事件触发器:
BEGIN;
ALTER EVENT TRIGGER trg_ddl_audit DISABLE;
CREATE EXTENSION pg_stat_statements;
ALTER EVENT TRIGGER trg_ddl_audit ENABLE;
COMMIT;postgres=# SELECT count(*) FROM audit.ddl_audit;
count
-------
0
(1 row)注意必须用显式事务包起来,否则一旦中间失败,触发器会停留在 DISABLE 状态——审计静默失效比审计噪音危险得多。
示例六:巡检三个事件上分别挂了什么
SELECT evtevent, evtname, evtenabled,
COALESCE(array_to_string(evttags, ', '), '(all)') AS tags
FROM pg_event_trigger
WHERE evtevent IN ('ddl_command_start', 'ddl_command_end', 'sql_drop')
ORDER BY evtevent, evtname; evtevent | evtname | evtenabled | tags
-------------------+------------------+------------+------
ddl_command_end | trg_ddl_audit | O | (all)
ddl_command_end | trg_ddl_filtered | O | (all)
sql_drop | trg_drop_audit | O | (all)
(3 rows)注意事项
不要在
ddl_command_start里调用pg_event_trigger_ddl_commands()。 该函数只允许在ddl_command_end中调用,其他事件里调用会直接报错。textERROR: pg_event_trigger_ddl_commands() can only be called in a ddl_command_end event trigger function不要在
ddl_command_end里调用pg_event_trigger_dropped_objects()。 同理,它只允许在sql_drop中调用。不要记错
sql_drop与ddl_command_end的先后。sql_drop在前。若你在ddl_command_end里等待sql_drop的副作用,永远等不到。不要试图在
sql_drop里读取被删对象的内容。 对象已经从系统目录消失,pg_class、pg_attribute都查不到了。需要删除前的信息必须用ddl_command_start。不要把
command列直接放进RAISE NOTICE或返回给客户端。pg_ddl_command是内部类型,输出会报错。要文本就用command_tag。不要假设「一次触发 = 一个对象」。
ALTER TABLE、DROP TABLE CASCADE都可能一次产生多行,必须用FOR ... LOOP遍历(要做单行日志就先array_agg聚合)。不要忽略
in_extension。 安装扩展时的几百条内部 DDL 会把审计表灌满,且绝大多数没有审计价值。不要给审计触发器设成
ALWAYS后又在其中做管控。 审计要宽(记录一切),管控要窄(只拦该拦的),混在一起会让pg_restore/ 逻辑复制端行为难以预测。不要在触发器里做重活。 每条 DDL 都会走一遍函数,函数里的每次查询都是真实开销,尤其是
ddl_command_start(它在命令执行的最前端)。不要忘记
sql_drop也会被ALTER TABLE ... DROP COLUMN触发。 只按TG_TAG = 'DROP TABLE'过滤会漏掉大量删除事件。sql-- 不推荐:只认 DROP 前缀,漏掉 ALTER ... DROP COLUMN IF TG_TAG LIKE 'DROP%' THEN ...不要依赖
object_identity做唯一键。 它可能被重命名、可能被搜索路径影响;精确定位用classid/objid/objsubid。不要在临时禁用审计后忘记启用。 用显式事务包住「禁用 → DDL → 启用」,避免异常中断导致
DISABLE残留。不要指望事件触发器能拦住对事件触发器自身的操作。
CREATE/ALTER/DROP EVENT TRIGGER不触发事件触发器,这是设计上的安全网。
最佳实践
审计用
ddl_command_end,拦截用ddl_command_start,删除清单用sql_drop,职责分清。sqlCREATE EVENT TRIGGER trg_audit ON ddl_command_end EXECUTE FUNCTION audit.on_ddl_end(); CREATE EVENT TRIGGER trg_guard ON ddl_command_start EXECUTE FUNCTION guard.enforce(); CREATE EVENT TRIGGER trg_drop ON sql_drop EXECUTE FUNCTION audit.on_drop();用
WHEN TAG IN (...)把监听范围收到真正关心的命令上。sqlCREATE EVENT TRIGGER trg_audit_ddl ON ddl_command_end WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE INDEX', 'DROP INDEX', 'ALTER INDEX') EXECUTE FUNCTION audit.on_ddl_end();在函数内再过滤一层 schema 与
in_extension。WHEN TAG只能过滤命令类型,过滤不了「哪个 schema」。sqlFOR r IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE NOT in_extension AND schema_name NOT IN ('pg_catalog', 'information_schema') LOOP ...审计写入用
SECURITY DEFINER+ 固定search_path。 审计表不给业务角色写权限,同时防止被搜索路径劫持。sqlCREATE FUNCTION audit.on_ddl_end() RETURNS event_trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, audit AS $$ ... $$;pg_event_trigger_ddl_commands()与pg_event_trigger_dropped_objects()的结果先聚合再落库,避免一条 DDL 写出几十行日志。sqlSELECT array_agg(object_identity ORDER BY object_identity) FROM pg_event_trigger_ddl_commands();审计表按时间分区并定期归档。 DDL 日志量小但持续增长。
拦截型触发器用
ddl_command_start,并给出可操作的HINT。sqlRAISE EXCEPTION '禁止 DROP 对象:%', TG_TAG USING HINT = '请提交变更申请,由 DBA 在维护窗口执行';扩展安装、批量迁移、
pg_restore前用事务临时禁用审计。sqlBEGIN; ALTER EVENT TRIGGER trg_audit_ddl DISABLE; \i migration.sql ALTER EVENT TRIGGER trg_audit_ddl ENABLE; COMMIT;把三个事件上的触发器清单纳入日常巡检,一旦发现「悄悄多出来的报错型触发器」,能第一时间定位。
sqlSELECT evtevent, evtname, evtenabled FROM pg_event_trigger ORDER BY 1, 2;部署前在影子库演练
pg_dump/pg_restore/ 迁移框架,确认审计与管控触发器不会误伤恢复流程。
下一节
继续学习:表重写事件——讲清 table_rewrite 在什么情况下触发、pg_event_trigger_table_rewrite_reason() 的原因码含义,以及如何用它守住大表重写的维护窗口。
参考链接
- PostgreSQL 18 官方文档:Overview of Event Trigger Behavior(五个事件与触发顺序)
- PostgreSQL 18 官方文档:Event Trigger Functions(
pg_event_trigger_ddl_commands/pg_event_trigger_dropped_objects) - PostgreSQL 18 官方文档:CREATE EVENT TRIGGER(
WHEN TAG IN与命令标签) - PostgreSQL 18 官方文档:Event Trigger Example(审计示例)
- PostgreSQL 18 官方文档:ALTER EVENT TRIGGER(禁用/启用)
- PostgreSQL 18 官方文档:pg_event_trigger(系统目录)
- PostgreSQL 18 官方文档:ALTER TABLE(会触发
sql_drop的子命令)