Skip to content

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 上的相对顺序 ​

先建立时间轴,后面所有结论都由此展开:

text
客户端:  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 执行完命令后会打印出来:

text
postgres=# CREATE TABLE demo(id int);
CREATE TABLE          ← 这就是命令标签

命令标签与事件的关系:

命令命令标签ddl_command_startddl_command_end是否可能触发 sql_drop
CREATE TABLECREATE TABLE是是否
CREATE TABLE ASCREATE TABLE AS是是否
SELECT ... INTOSELECT INTO是是否
ALTER TABLEALTER TABLE是是可能(如 DROP COLUMN)
DROP TABLEDROP TABLE是是是
DROP INDEXDROP INDEX是是是
COMMENT ONCOMMENT是是否
GRANTGRANT是是否
REVOKEREVOKE是是否
REINDEXREINDEX是是否
REFRESH MATERIALIZED VIEWREFRESH MATERIALIZED VIEW是是否
SECURITY LABELSECURITY LABEL是是否
IMPORT FOREIGN SCHEMAIMPORT 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 报错时命令已经执行,靠事务回滚把效果撤销。

触发顺序:按名字的字母顺序 ​

当同一个事件上挂了多个事件触发器时,触发顺序不是创建顺序,而是按触发器名字的字母顺序(字节序):

text
同一事件上的多个触发器:  按 evtname 的字母序依次触发
  trg_a_audit   ─► 先
  trg_b_guard   ─► 后
  trg_z_notify  ─► 最后

这带来一个实用技巧与一个陷阱:

  • 技巧:想让「审计」先于「管控」执行,可以靠前缀控制,例如 aaa_audit、zzz_guard。
  • 陷阱:若前一个触发器 RAISE EXCEPTION,后面的根本不会执行。名字顺序因此决定了「谁能拦住谁」。

语法与用法 ​

信息函数的调用方式 ​

两个信息函数都是集合返回函数(SRF),必须放在 FROM 子句里当作表使用:

text
-- 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(); 这种「当标量函数用」的形式——它们返回的是多行。

标准触发函数骨架 ​

sql
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_EVENTtext事件名,如 ddl_command_end
TG_TAGtext命令标签,如 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.

字段类型说明
classidoid包含该对象的系统目录 OID(即 pg_class.oid 之类的目录表自身 OID)
objidoid对象在系统目录中的 OID
objsubidinteger对象子 ID(例如列号);无子对象时为 0
command_tagtext命令标签,如 CREATE TABLE、ALTER TABLE
object_typetext对象类型的可读文本,如 table、index、column、view
schema_nametext对象所属 schema 名;若不适用则为 NULL
object_identitytext带 schema 限定的对象标识,如 public.orders
in_extensionboolean该对象是否属于某个扩展(扩展的 DDL 通常应被跳过)
commandpg_ddl_command该命令的内部表示

两点必须留意:

  1. command 列不能直接输出。它的类型 pg_ddl_command 是内核内部格式,虽然可以选定、可以存进变量,但输出到客户端会报错。想要可读文本,用 command_tag,或者把 command 强转成 text(得到的是内部表示的字符串,可读性有限)。
  2. object_identity 是「显示用」标识,不是稳定主键。要精确定位对象应使用 classid + objid + objsubid 三元组。
sql
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;
$$;
text
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.

字段类型说明
classidoid系统目录 OID
objidoid对象 OID
objsubidinteger对象子 ID(列号等)
object_typetext对象类型的可读文本
schema_nametext所属 schema;不适用时为 NULL
object_nametext对象名(不含 schema)
object_identitytext带 schema 限定的标识
address_namestext[]见下
address_argstext[]与 address_names 配套
originalboolean见下
normalboolean见下
is_temporaryboolean对象是否属于临时命名空间

四个「含义不直观」的字段需要专门解释:

original:被删的对象是原始对象还是被删除时连带消失的对象。

text
DROP TABLE orders;            -- orders 是 original
                            -- orders_pkey、pg_toast_xxx 是「连带」的,original = false

normal:该对象是否属于「正常」删除路径。被 DROP CASCADE 连带删掉的依赖对象是 normal = true,但与「内部生成的依赖」(如 TOAST、索引)区分开来。

address_names / address_args:这两个数组用于重建对象地址,主要服务于逻辑复制/DDL 复制这类场景。它们描述「对象在树形命名空间中的路径」。例如被删的对象是某个表上的一个索引时,address_names 会给出到达该索引的路径名,address_args 给出路径中需要的参数。对绝大多数审计用例,这两个字段用不上。

is_temporary:临时表在会话级 schema(pg_temp_N)里,审计时通常要排除掉。

sql
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;
$$;
text
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
-- 错误!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 不会触发):

不触发的场景说明
共享对象上的 DDLdatabases、roles、tablespaces 等集群级对象,不属于单个数据库
参数级权限命令ALTER ... SET 类的全局参数变更
ALTER SYSTEM变更配置文件,不是数据库内对象 DDL
针对事件触发器本身的命令CREATE/ALTER/DROP EVENT TRIGGER 不触发事件触发器

最后一条是安全网:如果事件触发器能拦截 ALTER EVENT TRIGGER ... DISABLE,那就没人能关掉它了。官方设计上明确把「对事件触发器自身的命令」排除在外。这也意味着你不能通过事件触发器阻止别人删除事件触发器。

事务性:报错发生在哪一阶段 ​

text
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 的信息函数返回的是每个被操作的对象一行,而不是「每个命令一行」。这造成一些反直觉的输出:

text
ALTER TABLE t ADD COLUMN a int, ADD COLUMN b int;
  → 返回 3 行(表 1 行 + 列 2 行)

因此触发器函数里不能假设「一次调用 → 一条记录」。若要做「每条 DDL 一条日志」的审计,要么在函数里聚合成一行,要么在日志表上接受「一条 DDL 多条日志」。

sql
-- 聚合写法:一次调用只写一行,把对象清单拼成数组
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。

sql
-- 审计时通常忽略扩展内部对象,否则日志会被 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 审计(命令 + 对象) ​

sql
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();
text
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)

示例二:捕获级联删除的完整清单 ​

sql
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();
text
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,忽略扩展与临时对象 ​

sql
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 之后,日志里剩下的基本就是真正需要关注的结构变更。

示例四:在同一事件上控制触发顺序 ​

sql
-- 字母序: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();
text
postgres=# DROP TABLE app.orders;
-- 先执行 trg_a_allowlist(记录意图),再执行 trg_z_guard(策略拦截)
ERROR:  策略拒绝:禁止在 app schema 中 DROP 对象 app.orders

如果你希望拦截器先跑(拦截后就不再记录),把管控触发器命名为字母序更小的名字即可。

示例五:临时屏蔽审计(用于扩展安装、数据导入) ​

官方文档给出的惯用法是用事务包住「禁用 → 执行 → 启用」,因为对事件触发器自身的 ALTER EVENT TRIGGER 不会触发事件触发器:

sql
BEGIN;
ALTER EVENT TRIGGER trg_ddl_audit DISABLE;
CREATE EXTENSION pg_stat_statements;
ALTER EVENT TRIGGER trg_ddl_audit ENABLE;
COMMIT;
text
postgres=# SELECT count(*) FROM audit.ddl_audit;
 count
-------
     0
(1 row)

注意必须用显式事务包起来,否则一旦中间失败,触发器会停留在 DISABLE 状态——审计静默失效比审计噪音危险得多。

示例六:巡检三个事件上分别挂了什么 ​

sql
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;
text
     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 中调用,其他事件里调用会直接报错。

    text
    ERROR:  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,职责分清。

    sql
    CREATE 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 (...) 把监听范围收到真正关心的命令上。

    sql
    CREATE 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」。

    sql
    FOR 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。 审计表不给业务角色写权限,同时防止被搜索路径劫持。

    sql
    CREATE 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 写出几十行日志。

    sql
    SELECT array_agg(object_identity ORDER BY object_identity) FROM pg_event_trigger_ddl_commands();
  • 审计表按时间分区并定期归档。 DDL 日志量小但持续增长。

  • 拦截型触发器用 ddl_command_start,并给出可操作的 HINT。

    sql
    RAISE EXCEPTION '禁止 DROP 对象:%', TG_TAG
          USING HINT = '请提交变更申请,由 DBA 在维护窗口执行';
  • 扩展安装、批量迁移、pg_restore 前用事务临时禁用审计。

    sql
    BEGIN;
    ALTER EVENT TRIGGER trg_audit_ddl DISABLE;
    \i migration.sql
    ALTER EVENT TRIGGER trg_audit_ddl ENABLE;
    COMMIT;
  • 把三个事件上的触发器清单纳入日常巡检,一旦发现「悄悄多出来的报错型触发器」,能第一时间定位。

    sql
    SELECT evtevent, evtname, evtenabled FROM pg_event_trigger ORDER BY 1, 2;
  • 部署前在影子库演练 pg_dump / pg_restore / 迁移框架,确认审计与管控触发器不会误伤恢复流程。

下一节

继续学习:表重写事件——讲清 table_rewrite 在什么情况下触发、pg_event_trigger_table_rewrite_reason() 的原因码含义,以及如何用它守住大表重写的维护窗口。

参考链接 ​