Skip to content

事件触发器概述 ​

普通触发器(trigger)监听的是表上的行级或语句级 DML,而事件触发器(Event Trigger)监听的是整个数据库里发生的 DDL。它是 PostgreSQL 提供的一个「数据库级钩子」:只要有人执行 CREATE TABLE、ALTER INDEX、DROP FUNCTION 这类命令,你注册的函数就会被调用。用它可以在数据库内核之外实现审计、权限管控、命名规范强制、DDL 拦截等治理能力。

本页讲清事件触发器的定位、五个事件的生命周期、触发函数能拿到什么信息,以及它和普通触发器、规则(rule)之间的边界。这是后续深入学习 DDL 事件与表重写事件的地基。

前置知识

阅读本节前,建议先了解:触发器函数编写、触发器概述

基础概念 ​

事件触发器是「数据库级对象」,不是表级对象 ​

普通触发器依附于某一张表,表被删掉它也跟着消失。事件触发器不属于任何表、任何 schema,它挂在数据库本身之上,作用范围是当前数据库内的全部 DDL。

text
普通触发器(Trigger)                     事件触发器(Event Trigger)
─────────────────────────────           ─────────────────────────────
作用域:某一张表                         作用域:整个数据库
监听:INSERT / UPDATE / DELETE /        监听:ddl_command_start /
     TRUNCATE / TRUNCATE DDL             ddl_command_end / sql_drop /
触发时机:行级(FOR EACH ROW)            table_rewrite / login
        语句级(FOR EACH STATEMENT)     触发时机:DDL 命令的前后
返回类型:trigger                       返回类型:event_trigger
权限要求:表的所有者                     权限要求:超级用户
存放在:pg_trigger                      存放在:pg_event_trigger

一句话概括:普通触发器管「数据怎么变」,事件触发器管「结构怎么变」。

五个触发事件 ​

PostgreSQL 18 支持的事件触发器事件一共 5 个。官方原文:Currently, the supported events are login, ddl_command_start, ddl_command_end, table_rewrite and sql_drop.

事件触发时机可否用 WHEN TAG专用信息函数
login新会话建立时(客户端连接成功、后端进程启动后)否无
ddl_command_startDDL 命令执行之前是无(命令尚未执行)
ddl_command_endDDL 命令执行之后是pg_event_trigger_ddl_commands()
sql_drop有对象被 DROP 时,在 ddl_command_end 之前触发是pg_event_trigger_dropped_objects()
table_rewrite表因 DDL 被物理重写之前是pg_event_trigger_table_rewrite_oid() / pg_event_trigger_table_rewrite_reason()

注意一个反直觉的顺序:sql_drop 虽然名字里带 drop,但它在 ddl_command_end 之前触发。官方原文:The sql_drop event occurs just before the ddl_command_end event trigger for any operation that drops database objects.

一次 DROP TABLE 的完整生命周期 ​

text
客户端:  DROP TABLE orders;
   │
   ├─► ① ddl_command_start 事件触发器触发
   │      └─ 此时命令尚未执行,表还在,可以 raise exception 拦截
   │
   ├─► ② PostgreSQL 执行 DROP TABLE
   │
   ├─► ③ sql_drop 事件触发器触发
   │      └─ pg_event_trigger_dropped_objects() 可列出被删对象(含级联删除的索引、约束)
   │
   └─► ④ ddl_command_end 事件触发器触发
          └─ pg_event_trigger_ddl_commands() 可列出本次执行的 DDL 命令
   │
   └─► 事务提交(或回滚)

这四个阶段全部发生在同一个事务里。也就是说:触发器函数里 RAISE EXCEPTION 会连带把那条 DDL 一起回滚——这正是「DDL 审计与拦截」能实现的原因。

与普通触发器、规则(rule)的对比 ​

能力普通触发器事件触发器规则(RULE)
监听 DML是否是
监听 DDL否是否
能修改即将执行的数据是(BEFORE 触发器中改 NEW)否是(INSTEAD)
能拦截并报错是是有限
能拿到「被删对象的完整列表」否是否
需要超级用户否是否
能在备库上工作取决于 session_replication_role取决于 evtenabled是

事件触发器存在哪:pg_event_trigger ​

sql
SELECT oid, evtname, evtevent, evtowner, evtfoid, evtenabled, evttags
FROM pg_event_trigger;
字段类型说明
oidoid事件触发器 OID
evtnamename事件触发器名称
evteventname监听的事件名,如 ddl_command_end
evtowneroid所有者(必然是超级用户),关联 pg_authid.oid
evtfoidoid被调用的函数,关联 pg_proc.oid
evtenabledchar启用状态:O / D / R / A
evttagstext[]WHEN TAG IN (...) 里列出的命令标签;为空表示不过滤

evtenabled 的取值直接决定哪些 session_replication_role 下会真的触发:

值含义何时触发
OOrigin(默认)仅在 session_replication_role = origin(默认)时触发
DDisabled永不触发
RReplica仅在 session_replication_role = replica 时触发
AAlways无论 session_replication_role 是什么都触发

三个层次的「谁能拦住你」 ​

应用层:GRANT / REVOKE、schema 权限        ← 拦住普通用户
事件触发器:RAISE EXCEPTION                ← 拦住有 DDL 权限的角色
目录层:pg_catalog 与系统对象              ← 拦住所有人(包括超级用户)

事件触发器是最后一道可编程防线,但它拦不住超级用户——超级用户可以直接 ALTER EVENT TRIGGER ... DISABLE。

语法与用法 ​

text
-- 创建
CREATE EVENT TRIGGER name
    ON event
    [ WHEN TAG IN (filter_value [, ... ]) [ AND ... ] ]
    EXECUTE FUNCTION function_name();

-- 改名
ALTER EVENT TRIGGER name RENAME TO new_name;

-- 启用 / 禁用(四态)
ALTER EVENT TRIGGER name ENABLE [ REPLICA | ALWAYS ];
ALTER EVENT TRIGGER name DISABLE;

-- 换函数
ALTER EVENT TRIGGER name ... -- 需 DROP 后重建,无法直接改函数

-- 改所有者
ALTER EVENT TRIGGER name OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER };

-- 删除
DROP EVENT TRIGGER name;

最小可运行示例:

sql
-- 1. 先写一个返回 event_trigger 的函数
CREATE FUNCTION log_ddl_event() RETURNS event_trigger
LANGUAGE plpgsql AS $$
BEGIN
    RAISE NOTICE 'event = %, tag = %', TG_EVENT, TG_TAG;
END;
$$;

-- 2. 把它挂到 ddl_command_end 上
CREATE EVENT TRIGGER trg_log_ddl
    ON ddl_command_end
    EXECUTE FUNCTION log_ddl_event();
text
postgres=# CREATE TABLE demo(id int);
NOTICE:  event = ddl_command_end, tag = CREATE TABLE
CREATE TABLE

管理用的常用查询:

sql
-- 列出所有事件触发器及其函数、状态
SELECT e.evtname,
       e.evtevent,
       p.proname,
       e.evtenabled,
       e.evttags
FROM pg_event_trigger e
JOIN pg_proc p ON p.oid = e.evtfoid
ORDER BY e.evtname;
text
     evtname     |     evtevent      |     proname      | evtenabled | evttags
-----------------+-------------------+------------------+------------+---------
 trg_log_ddl     | ddl_command_end   | log_ddl_event    | O          |
(1 row)

在 psql 里也可以用元命令快速查看:

text
postgres=# \dy
                 List of event triggers
    Name     |       Event       |  Owner   | Enabled | Function  | Tags
-------------+-------------------+----------+---------+-----------+------
 trg_log_ddl | ddl_command_end   | postgres | enabled | log_ddl_event |
(1 row)

详细说明 ​

事件触发器是数据库级对象 ​

事件触发器没有 schema 限定,也不归属某个 schema。你在 db_a 里建的事件触发器不会影响 db_b——事件触发器的作用范围恰好是「创建它的那个数据库」。

这带来两个直接结论:

  1. 每建一个新数据库,都要重新注册一遍。 事件触发器不会被 CREATE DATABASE 继承(只有 template1 里的会被继承,因为新库是 template1 的拷贝)。
  2. 跨库治理必须逐库部署。 想全局统一,通常的做法是写一个幂等脚本,在 postgres 库里对每个数据库执行。
sql
-- 在 template1 里注册 = 让未来所有新库自动带上
\c template1
CREATE EVENT TRIGGER trg_audit_ddl ON ddl_command_end EXECUTE FUNCTION audit.record_ddl();

同样地,pg_dump 不会导出事件触发器。官方文档在备份章节明确指出事件触发器不包含在 pg_dump 输出中,需要单独用脚本管理。

只有超级用户可以创建 ​

CREATE EVENT TRIGGER 的权限要求非常硬:必须拥有超级用户权限。这与普通触发器「表的所有者就能建」完全不同。

sql
-- 错误!
-- 以普通角色执行会直接失败
SET ROLE app_user;
CREATE EVENT TRIGGER trg_x ON ddl_command_end EXECUTE FUNCTION f();
text
ERROR:  permission denied to create event trigger "trg_x"
HINT:  Must be superuser to create event triggers.

原因很简单:事件触发器能看到并干预全库的 DDL,如果普通用户能建,那就能借此窃取所有 schema 的结构变更信息,也能用 RAISE EXCEPTION 搞拒绝服务。因此 PostgreSQL 把它牢牢锁在超级用户手里。改所有者(ALTER EVENT TRIGGER ... OWNER TO)也要求新所有者是超级用户。

函数必须返回 event_trigger ​

事件触发器的处理函数有两条硬性要求:

  1. 返回类型必须是 event_trigger(一个伪类型,pg_ddl_command 的同族)。
  2. 不能声明任何参数(事件触发器调用时不会传参)。
sql
-- 错误!返回类型不对
CREATE FUNCTION bad_1() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$;
CREATE EVENT TRIGGER t1 ON ddl_command_end EXECUTE FUNCTION bad_1();
text
ERROR:  function bad_1 must return type event_trigger
sql
-- 错误!带了参数
CREATE FUNCTION bad_2(x int) RETURNS event_trigger LANGUAGE plpgsql AS $$ BEGIN END; $$;
text
ERROR:  function bad_2 must have no arguments

函数体内部的返回值会被忽略——RETURN NULL; 是标准写法。你不能通过返回值做任何「修改」,拦截只能靠 RAISE EXCEPTION。

五个触发事件各自的语义 ​

事件官方语义典型用途
login新会话建立时触发连接审计、连接数治理、会话级环境初始化
ddl_command_startDDL 命令执行前触发事前拦截(禁止建表、禁止 DROP)
ddl_command_endDDL 命令执行后触发事后审计、自动补 COMMENT、同步结构到外部系统
sql_drop有对象被删除时触发,先于 ddl_command_end删除审计、清理关联资源
table_rewrite表被重写前触发预警大表重写、限制重写窗口

login 事件是较新加入的成员,它与其余四个有本质区别:它不属于 DDL 生命周期,因此既不能用 WHEN TAG 过滤,也不能调用 pg_event_trigger_ddl_commands() 之类的信息函数。它的触发点非常靠前(会话初始化阶段),这意味着:

  • 在 login 触发器里做重活会拖慢每一次连接;
  • 一旦函数报错,连接会直接失败,等于把自己锁在门外;
  • 它可以用 GUC event_triggers = false 或 ALTER EVENT TRIGGER ... DISABLE 绕过。

WHEN TAG 的过滤规则 ​

WHEN TAG IN (...) 里写的是命令标签(command tag),也就是 psql 执行完命令后打印的那个大写字符串,例如 CREATE TABLE、ALTER TABLE、DROP INDEX。

sql
CREATE EVENT TRIGGER trg_audit_create
    ON ddl_command_end
    WHEN TAG IN ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
    EXECUTE FUNCTION audit_create();

要点:

  • 标签区分大小写,必须写成大写形式;写小写不会报错,但永远匹配不上。
  • 一个事件触发器的 WHEN 子句可以写多个 TAG IN (...) 组,组间是 AND(交集),组内是 OR(并集)。实际上多组 TAG IN 交集基本得不到结果,常用形式是单组。
  • login 事件不支持 WHEN TAG。
  • 注意 SELECT INTO 在内部会被当作 CREATE TABLE AS 处理,标签过滤时要留意官方文档列出的对应关系。
sql
-- 不推荐:多组 TAG IN 取交集,几乎永远不触发
CREATE EVENT TRIGGER trg_x ON ddl_command_end
    WHEN TAG IN ('CREATE TABLE') AND TAG IN ('DROP TABLE')
    EXECUTE FUNCTION f();

生命周期里能拿到什么信息 ​

不同的阶段能拿到的信息不同,这是最容易踩坑的地方:

事件可用的信息函数拿不到什么
login无没有任何 DDL 上下文
ddl_command_start可用 TG_TAG,可读系统目录的「旧状态」命令尚未执行,看不到新对象
ddl_command_endpg_event_trigger_ddl_commands()看不到「被删对象的级联清单」(用 sql_drop)
sql_droppg_event_trigger_dropped_objects()看不到「命令文本」
table_rewritepg_event_trigger_table_rewrite_oid() / ..._reason()没有通用的对象清单

在 plpgsql 里还有几个专用变量:

变量类型说明
TG_EVENTtext触发的事件名,如 ddl_command_end
TG_TAGtext触发本次调用的命令标签,如 CREATE TABLE
sql
CREATE FUNCTION audit_tag() RETURNS event_trigger LANGUAGE plpgsql AS $$
BEGIN
    RAISE NOTICE 'event=%, tag=%', TG_EVENT, TG_TAG;
END;
$$;

evtenabled 与 session_replication_role 的配合 ​

这是事件触发器最像普通触发器的地方。普通触发器的「点火」由 session_replication_role 决定,事件触发器也一样:O 只在 origin 下生效,R 只在 replica 下生效,A 永远生效。

sql
-- 让某个触发器在逻辑复制/恢复场景下也执行
ALTER EVENT TRIGGER trg_audit ENABLE ALWAYS;

这个设计的意义在于:pg_restore、逻辑复制订阅端常常会把 session_replication_role 设为 replica 以跳过触发器(避免重复触发副作用)。如果你的事件触发器是「审计」性质的、希望任何情况下都记录,就应该设为 ALWAYS;如果它是「管控」性质的(例如禁止建表),那么设成 ALWAYS 会让恢复流程也被拦住,需要谨慎。

sql
-- 会话级切换(需超级用户)
SET session_replication_role = replica;
-- 此后只有 evtenabled IN ('R','A') 的事件触发器会触发
RESET session_replication_role;

事务性与回滚行为 ​

事件触发器运行在触发它的那条 DDL 所在的事务里。因此:

  • 触发器中的 RAISE EXCEPTION → 整条 DDL 回滚,报错返回给客户端。
  • 触发器写入审计表 → 该写入与 DDL 同生共死:DDL 回滚时审计记录也一起回滚。
sql
CREATE FUNCTION deny_drop() RETURNS event_trigger LANGUAGE plpgsql AS $$
BEGIN
    RAISE EXCEPTION 'DROP 已被策略禁止 (tag=%)', TG_TAG;
END;
$$;
text
postgres=# DROP TABLE demo;
ERROR:  DROP 已被策略禁止 (tag=DROP TABLE)

第二条性质常被误用:有人希望「审计日志即使 DDL 失败也要留下记录」,但在同一个事务里做不到。要实现这种「失败也要留痕」的需求,只能走外部通道:写入另一台库、写文件、发消息队列,或者干脆让 login/ddl_command_start 阶段的 RAISE WARNING 由日志收集系统捕获。

事件触发器不能做什么 ​

  • 不能在单用户模式(single-user mode)下创建或执行。 官方原文:Event triggers are disabled in single-user mode (see postgres) as well as when event_triggers is set to false. 这提供了逃生通道:如果某条 DDL 被事件触发器锁死,可以通过 event_triggers=false 或单用户模式绕开。
  • 不能在已中止的事务中执行。 官方说明:Event triggers (like other functions) cannot be executed in an aborted transaction.
  • 不能修改即将执行的 DDL。 没有「改 NEW」这种机制,只能放行或报错。
  • 不能监听 DML。 INSERT/UPDATE/DELETE 必须用普通触发器。
  • pg_event_trigger_ddl_commands() 等函数有调用上下文限制,在错误的事件里调用会直接报错,见下一节 ddl-events。

实战示例 ​

示例一:建一张 DDL 审计表 + 全量审计触发器 ​

sql
-- 审计表单独放一个 schema,业务角色只读
CREATE SCHEMA IF NOT EXISTS audit;

CREATE TABLE audit.ddl_log (
    id          bigserial PRIMARY KEY,
    logged_at   timestamptz NOT NULL DEFAULT clock_timestamp(),
    db_name     text NOT NULL DEFAULT current_database(),
    session_user_name text NOT NULL DEFAULT session_user,
    current_role_name text NOT NULL DEFAULT current_user,
    event       text NOT NULL,
    tag         text NOT NULL,
    object_type text,
    object_id   text,
    command     text
);

CREATE OR REPLACE FUNCTION audit.record_ddl() RETURNS event_trigger
LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
    r record;
BEGIN
    FOR r IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
        INSERT INTO audit.ddl_log(event, tag, object_type, object_id, command)
        VALUES (TG_EVENT, r.command_tag, r.object_type,
                r.object_identity, r.command::text);
    END LOOP;
END;
$$;

CREATE EVENT TRIGGER trg_audit_ddl
    ON ddl_command_end
    EXECUTE FUNCTION audit.record_ddl();
text
postgres=# CREATE INDEX idx_demo_a ON demo(a);
CREATE INDEX
postgres=# SELECT tag, object_type, object_id FROM audit.ddl_log ORDER BY id;
     tag      | object_type | object_id
--------------+-------------+------------
 CREATE INDEX | index       | public.idx_demo_a
(1 row)

注意 pg_event_trigger_ddl_commands() 会被循环遍历:一条 DDL 可能产生多行(例如 ALTER TABLE 同时改了多个对象),所以触发器函数必须写成 FOR r IN SELECT ... LOOP。

示例二:禁止在业务 schema 里执行 DROP ​

sql
CREATE OR REPLACE FUNCTION guard.no_drop_in_app() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
    obj record;
BEGIN
    FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
               WHERE schema_name = 'app' LOOP
        RAISE EXCEPTION '禁止删除 app schema 中的对象:% (%)',
              obj.object_identity, obj.object_type;
    END LOOP;
END;
$$;

CREATE EVENT TRIGGER trg_no_drop_in_app
    ON sql_drop
    EXECUTE FUNCTION guard.no_drop_in_app();
text
app=# DROP TABLE app.customers;
ERROR:  禁止删除 app schema 中的对象:app.customers (table)

这里能生效的关键是 pg_event_trigger_dropped_objects() 提供了 schema_name 和 object_identity——它比 TG_TAG 精确得多,因为 TG_TAG 只能告诉你「这是个 DROP 命令」,不会告诉你删的是哪个 schema 的什么对象。

示例三:巡检当前库里的事件触发器 ​

sql
SELECT e.evtname,
       e.evtevent,
       e.evtenabled,
       COALESCE(array_to_string(e.evttags, ', '), '(all)') AS tags,
       n.nspname || '.' || p.proname AS func,
       p.prolang::regtype AS lang
FROM pg_event_trigger e
JOIN pg_proc p ON p.oid = e.evtfoid
JOIN pg_namespace n ON n.oid = p.pronamespace
ORDER BY e.evtevent, e.evtname;
text
     evtname      |     evtevent     | evtenabled |    tags     |           func           |   lang
------------------+------------------+------------+-------------+--------------------------+----------
 trg_audit_ddl    | ddl_command_end  | O          | (all)       | audit.record_ddl         | plpgsql
 trg_no_drop_in_app | sql_drop       | O          | (all)       | guard.no_drop_in_app     | plpgsql
(2 rows)

这个查询建议纳入日常巡检:事件触发器是「隐形」的全局行为,一旦有人偷偷建了一个报错型触发器,整个库的 DDL 都会莫名其妙失败,而报错信息往往只提示「函数返回了错误」,不告诉你是哪个事件触发器干的。

示例四:临时禁用排查问题 ​

sql
-- 先禁用可疑触发器再重试
ALTER EVENT TRIGGER trg_no_drop_in_app DISABLE;
DROP TABLE app.customers;
-- 排查完成后恢复
ALTER EVENT TRIGGER trg_no_drop_in_app ENABLE;

注意事项 ​

  • 不要以为普通用户能创建事件触发器。 官方要求超级用户,普通角色会得到 permission denied to create event trigger。

  • 不要忘了事件触发器是「每库一份」。 在 postgres 库里建的不会自动出现在业务库里,也不会被 pg_dump 带出去。

    sql
    -- 不推荐:以为在 postgres 库建完就全库生效了
    \c postgres
    CREATE EVENT TRIGGER trg_audit ON ddl_command_end EXECUTE FUNCTION audit.record_ddl();
    \c app_db          -- 这里没有触发器
    \dy
  • 不要在 ddl_command_start 里调用 pg_event_trigger_ddl_commands()。 该函数只允许在 ddl_command_end 中调用,其他地方一律报错。

    sql
    -- 错误!
    CREATE FUNCTION bad() RETURNS event_trigger LANGUAGE plpgsql AS $$
    DECLARE n int;
    BEGIN
        SELECT count(*) INTO n FROM pg_event_trigger_ddl_commands();
    END; $$;
    CREATE EVENT TRIGGER t_bad ON ddl_command_start EXECUTE FUNCTION bad();
    -- DROP TABLE demo;  → ERROR: pg_event_trigger_ddl_commands() can only be called in a ddl_command_end event trigger function
  • 不要用 pg_event_trigger_dropped_objects() 去 ddl_command_end 里找被删对象。 它是 sql_drop 专用的。

  • 不要假设 TG_TAG 是「命令原文」。 它是命令标签,不含对象名和参数。想看完整 SQL 文本,得从 ddl_command_end 的 command 列取。

  • 不要让触发器函数的返回值承担逻辑。 返回值被忽略,写 RETURN NEW 之类的代码只会让人误读。

  • 不要在事件触发器里做重活(尤其是 login)。 它在路径的极前端执行,任何延迟都被放大到每一次 DDL / 每一次连接。

    sql
    -- 不推荐:在 login 触发器里查询大表
    CREATE FUNCTION heavy_login() RETURNS event_trigger LANGUAGE plpgsql AS $$
    BEGIN
        PERFORM count(*) FROM audit.huge_events;   -- 拖慢每一个连接
    END; $$;
  • 不要把「审计」触发器设成 ALWAYS 之后又依赖它做管控。 两者目标相反,混用会让 pg_restore 与逻辑复制行为难以预测。

  • 不要指望在触发器中「修改」即将执行的 DDL。 没有可写的上下文对象,只能放行或中止。

  • 不要在触发器函数里依赖事务外的副作用来「留痕」。 同事务写入会随 DDL 一起回滚。

  • 不要忘记单用户模式与 event_triggers=false 是逃生通道。 被自己写的触发器锁死时,postgres --single 或设置 event_triggers = false 可以进去把它删掉。

最佳实践 ​

  • 事件触发器统一放在专门的 schema 下,函数名与触发器名保持一对一可追溯。

    sql
    CREATE SCHEMA IF NOT EXISTS audit;
    CREATE EVENT TRIGGER trg_audit_ddl ON ddl_command_end
        EXECUTE FUNCTION audit.record_ddl();
  • 用 SECURITY DEFINER + 严格 search_path 保护审计写入。 审计表不应给业务角色写权限。

    sql
    CREATE FUNCTION audit.record_ddl() RETURNS event_trigger
    LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, audit AS $$ ... $$;
  • 用 WHEN TAG IN (...) 缩小监听范围。 全量监听会为每条 DDL 付出一次函数调用开销。

    sql
    CREATE EVENT TRIGGER trg_audit_ddl ON ddl_command_end
        WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE INDEX', 'DROP INDEX')
        EXECUTE FUNCTION audit.record_ddl();
  • 把「审计」与「管控」写成两个独立的事件触发器。 审计要 ALWAYS(宽),管控要 O(窄),职责分开才好调。

  • 拦截型触发器给出可操作的错误提示。 不要只写「禁止操作」,把 object_identity、命令标签、正确的申请流程写进 HINT。

    sql
    RAISE EXCEPTION '禁止在 app schema 中 DROP 对象:%', obj.object_identity
          USING HINT = '请走 change request 流程,由 DBA 在维护窗口执行';
  • 用幂等脚本部署事件触发器,纳入版本管理。 CREATE EVENT TRIGGER 没有 IF NOT EXISTS,脚本里先判断再建。

    sql
    DO $$
    BEGIN
        IF NOT EXISTS (SELECT 1 FROM pg_event_trigger WHERE evtname = 'trg_audit_ddl') THEN
            CREATE EVENT TRIGGER trg_audit_ddl ON ddl_command_end
                EXECUTE FUNCTION audit.record_ddl();
        END IF;
    END $$;
  • 新库初始化时自动装载。 把注册逻辑放进 template1 或放进数据库初始化流水线。

  • 审计表按时间分区并定期归档。 DDL 日志增长慢但不会停,用 pg_partman 或原生分区按月切。

  • 给 login 事件触发器严格限速与限时。 它服务于连接路径,任何查询都必须走索引且代价可控。

  • 部署前先在测试库验证触发器不会误伤 pg_dump / pg_restore / 迁移工具。 很多迁移框架会执行大量 DDL,一个写错的管控触发器能让整个部署失败。

    bash
    # 在测试库演练恢复,确认没有事件触发器拦截
    pg_restore -d test_db -j 4 dump.custom
  • 把事件触发器清单纳入监控与变更审计。 没人记得的隐形全局钩子是生产事故的常见来源。

    sql
    SELECT evtname, evtevent, evtenabled FROM pg_event_trigger ORDER BY 1;

下一节

下一节:DDL 事件触发(CREATE / ALTER / DROP)——讲清 ddl_command_start / ddl_command_end / sql_drop 三个事件的细节,以及 pg_event_trigger_ddl_commands() 与 pg_event_trigger_dropped_objects() 的完整字段与用法。

参考链接 ​