表重写事件
有些 ALTER TABLE 看着只是改个属性,实际却会把整张表连同它的所有索引重新写一遍。在一个几亿行的表上,这不是「改个元数据」,而是一次持续数十分钟、占满 I/O、锁住 ACCESS EXCLUSIVE 的重量级操作。更糟的是,同一句 ALTER TABLE 在小表上是秒级、在大表上是灾难级,语法上完全看不出区别。
table_rewrite 事件触发器就是为这件事准备的:它在表即将被物理重写之前触发,让你有机会记录、告警,或者直接把它拦下来。本页讲清三件事:哪些操作真的会重写表、pg_event_trigger_table_rewrite_reason() 的原因码怎么解读、以及如何用它在维护窗口之外禁止重写。
前置知识
阅读本节前,建议先了解:事件触发器概述、DDL 事件触发(CREATE / ALTER / DROP)、修改表结构(ALTER TABLE)
基础概念
什么是「表重写」
PostgreSQL 的表数据存在一堆堆文件块(heap page)里,每行按固定布局存放。当某次 ALTER TABLE 改变了行的物理布局(列的数量与类型、列的默认值是否需要回填、表的持久性、访问方法)时,旧的行格式不再适用,数据库只能:
新建一张「空的新表」
│
├─► 逐行从旧表读出 → 按新格式转换 → 写入新表
├─► 索引全部重建(索引里存的是行指针 TID)
├─► TOAST 数据一并处理
└─► 写 WAL(生成量与表大小同量级)
完成后:把新表文件改名成旧表的 relfilenode,删掉旧文件这个过程就叫 表重写(table rewrite)。它的三个致命特征:
| 特征 | 后果 |
|---|---|
持有 ACCESS EXCLUSIVE 锁 | 重写期间读写全部阻塞,不是「慢」,是「不可用」 |
| 生成与表大小同量级的 WAL | 主从延迟、WAL 归档压力、磁盘空间被吃掉一倍 |
| 不可中断(除非取消整个命令) | 中途 Ctrl-C 会回滚整个重写,代价白付 |
官方文档对此有一句非常直接的说明:Adding a column with a volatile DEFAULT or changing the type of an existing column will require the entire table and its indexes to be rewritten.
table_rewrite 事件的精确定义
官方原文:The table_rewrite event occurs just before a table is rewritten by some actions of the commands ALTER TABLE and ALTER TYPE. While other control statements are available to rewrite a table, like CLUSTER and VACUUM, the table_rewrite event is not triggered by them.
拆开来读,有三条关键信息:
- 触发时机是重写之前(just before),所以还有机会阻止。
- 触发来源是
ALTER TABLE与ALTER TYPE的某些子命令——不是全部。 CLUSTER、VACUUM FULL这类「也会重写表」的命令不会触发table_rewrite。这是一个明确的盲区,后面会专门讲。
ALTER TABLE ...(会重写的子命令) ─► table_rewrite 触发 ✔
ALTER TYPE ...(类型变更连带重写) ─► table_rewrite 触发 ✔
CLUSTER / VACUUM FULL ─► 不触发 ✘
REINDEX ─► 不触发 ✘事件在时间轴上的位置
客户端: ALTER TABLE big_table ALTER COLUMN c TYPE bigint;
│
├─► ① ddl_command_start
│
├─► ② table_rewrite ← 表还没开始重写
│ pg_event_trigger_table_rewrite_oid() → 即将重写的表
│ pg_event_trigger_table_rewrite_reason() → 重写原因码
│ 此处 RAISE EXCEPTION 可以阻止重写
│
├─► ③ 内核执行重写(耗时最长的一步)
│
├─► ④ ddl_command_end
│
└─► 事务提交 / 回滚注意 table_rewrite 在 ddl_command_end 之前,也就是说它触发时 pg_event_trigger_ddl_commands() 还拿不到东西(那个函数只允许在 ddl_command_end 里调用)。
语法与用法
两个专用信息函数
官方定义(§9.30.3):
Table 9.111. Table Rewrite Information Functions
| 函数 | 返回类型 | 说明 |
|---|---|---|
pg_event_trigger_table_rewrite_oid() | oid | Returns the OID of the table about to be rewritten. |
pg_event_trigger_table_rewrite_reason() | integer | Returns a code explaining the reason(s) for rewriting. |
两者都只能在 table_rewrite 事件触发器的函数里调用,其他上下文调用会报错。
原因码是一个 bitmap,不是一个枚举
这是最容易写错的地方。官方原文:
Returns a code explaining the reason(s) for rewriting. The value is a bitmap built from the following values:
1(the table has changed its persistence),2(default value of a column has changed),4(a column has a new data type) and8(the table access method has changed).
翻译过来:
| 位值 | 含义 |
|---|---|
1 | 表的持久性发生变化(LOGGED ↔ UNLOGGED) |
2 | 某列的默认值发生变化 |
4 | 某列有了新的数据类型 |
8 | 表的访问方法发生变化 |
关键点:它返回的是按位组合(bitmap)的整数,而不是 1/2/4/8 这四个互斥的枚举值。因此:
- 只改类型 → 返回
4 - 同时改类型和持久性 → 返回
4 | 1 = 5 - 只有一步操作触发重写时通常是单值,但不要假设它一定是单值
官方只定义了数值,没有公开 AT_REWRITE_* 宏名
在某些资料里你会看到 AT_REWRITE_DEFAULT_VAL、AT_REWRITE_COLUMN_TYPE 之类的符号名,它们是 PostgreSQL 源码里的内部宏,官方 SQL 文档并没有把这些名字作为稳定接口公开。写文档、写脚本、做判断时,请以官方文档给出的数值 1 / 2 / 4 / 8 为准。跨版本升级时这些数值的语义以当版官方文档为准。
由于是 bitmap,解码必须用按位运算,不能用等值比较:
-- 错误!这是等值比较,reason = 5 时判断不出「类型变了」
IF pg_event_trigger_table_rewrite_reason() = 4 THEN ...
-- 正确:按位与
IF pg_event_trigger_table_rewrite_reason() & 4 <> 0 THEN ... -- 涉及类型变更最小可运行示例
官方文档给出的范例:
CREATE FUNCTION test_event_trigger_table_rewrite_oid()
RETURNS event_trigger
LANGUAGE plpgsql AS
$$
BEGIN
RAISE NOTICE 'rewriting table % for reason %',
pg_event_trigger_table_rewrite_oid()::regclass,
pg_event_trigger_table_rewrite_reason();
END;
$$;
CREATE EVENT TRIGGER test_table_rewrite_oid
ON table_rewrite
EXECUTE FUNCTION test_event_trigger_table_rewrite_oid();postgres=# ALTER TABLE demo ALTER COLUMN code TYPE varchar(64);
NOTICE: rewriting table demo for reason 4
ALTER TABLEpg_event_trigger_table_rewrite_oid() 返回 oid,用 ::regclass 转成可读的表名是标准写法。
原因码解码函数
CREATE OR REPLACE FUNCTION rewrite_reason_text(reason integer)
RETURNS text LANGUAGE sql IMMUTABLE AS $$
SELECT string_agg(part, ' + ' ORDER BY bit)
FROM (VALUES
(1, 'persistence changed'),
(2, 'column default changed'),
(4, 'column data type changed'),
(8, 'access method changed')
) AS t(bit, part)
WHERE reason & bit <> 0;
$$;SELECT rewrite_reason_text(4) AS r4, -- column data type changed
rewrite_reason_text(5) AS r5, -- persistence changed + column data type changed
rewrite_reason_text(15) AS r15; -- 全部四项 r4 | r5 | r15
---------------+-------------------------------------+------------------------------------------------------------------------
column data type changed | persistence changed + column data type changed | persistence changed + column default changed + column data type changed + access method changed详细说明
哪些 ALTER TABLE 操作真的会重写表
不是所有 ALTER TABLE 都会重写。改「元数据」的操作(比如 SET STATISTICS、RENAME、SET DEFAULT)几乎都是瞬时的;只有改变行物理布局或需要回填数据的操作才会重写。
ALTER TABLE 子命令 | 是否重写 | 说明 |
|---|---|---|
ADD COLUMN ... DEFAULT <volatile> | 是 | 需要为已有行回填一个不稳定值 |
ADD COLUMN ... DEFAULT <非 volatile> | 否 | 只记录默认值,已有行按需填充(元数据变更) |
ADD COLUMN(无默认值) | 否 | 纯元数据操作 |
ALTER COLUMN ... SET DATA TYPE | 通常是 | 无隐含/赋值转换时、或需要转换数据时重写 |
ALTER COLUMN ... SET/DROP DEFAULT | 否 | 只影响后续 INSERT/UPDATE,官方明确:it does not cause rows already in the table to change |
ALTER COLUMN ... SET/DROP NOT NULL | 否 | 需要扫描表校验,但不是重写 |
ALTER COLUMN ... SET EXPRESSION AS (...) | 是(存储生成列) | 官方:Existing data in a stored generated column is rewritten |
ALTER COLUMN ... SET STORAGE | 否 | 官方:doesn't itself change anything in the table; it just sets the strategy |
ALTER COLUMN ... SET COMPRESSION | 否 | 官方:This does not cause the table to be rewritten |
ALTER COLUMN ... SET STATISTICS | 否 | 只设置统计目标,取 SHARE UPDATE EXCLUSIVE 锁 |
SET LOGGED / SET UNLOGGED | 是 | 持久性变化(原因码含 1) |
SET ACCESS METHOD | 是 | 访问方法变化(原因码含 8) |
SET TABLESPACE | 是 | 迁移到另一个表空间需要复制全部数据 |
SET ( storage_parameter ) | 否(多数) | 例如调 fillfactor 不重写,只影响后续写入 |
ADD CONSTRAINT / VALIDATE CONSTRAINT | 否 | 扫描校验,但不重写 |
RENAME / SET SCHEMA / OWNER TO | 否 | 纯元数据 |
关于 SET DATA TYPE 需要补充一句:官方说索引和「简单表约束」会被自动重新解析转换,USING 子句可自定义转换。当新旧类型之间没有隐含或赋值转换时,必须显式提供 USING,否则命令直接报错——这种情况一定伴随重写。
关于 ADD COLUMN ... DEFAULT 的 volatile 判定,有一个极具实操价值的对比:
-- 不重写:now() 是 stable,PostgreSQL 只存默认值表达式
ALTER TABLE big ADD COLUMN created_at timestamptz DEFAULT now();
-- 重写!random() 是 volatile,必须为每一行算出不同的值
ALTER TABLE big ADD COLUMN token uuid DEFAULT gen_random_uuid();官方原文即:Adding a column with a volatile DEFAULT or changing the type of an existing column will require the entire table and its indexes to be rewritten. 判断依据是该函数的 volatility 属性:volatile 需要逐行求值 → 重写;stable / immutable 可以只存表达式 → 不重写。
ALTER TYPE 也会触发
除了 ALTER TABLE,对类型本身的修改也可能连带重写使用该类型的表。官方定义里明确写了触发来源包含 ALTER TYPE。因此给一个自定义类型加属性、改变类型的存储表示等操作,都可能让所有使用该类型的表被重写——这往往是「我只改了个类型,怎么全库都在锁」的根因。
ALTER TYPE composite_t ADD ATTRIBUTE note text; -- 使用该类型的表可能被重写排查这类问题时,table_rewrite 触发器是唯一能在重写发生前给出告警的机制。
官方范例:只在维护窗口允许重写
这是 table_rewrite 最经典的用法,官方 §38.4 给出了完整实现:
CREATE OR REPLACE FUNCTION no_rewrite()
RETURNS event_trigger
LANGUAGE plpgsql AS
$$
---
--- Implement local Table Rewriting policy:
--- public.foo is not allowed rewriting, ever
--- other tables are only allowed rewriting between 1am and 6am
--- unless they have more than 100 blocks
---
DECLARE
table_oid oid := pg_event_trigger_table_rewrite_oid();
current_hour integer := extract('hour' from current_time);
pages integer;
max_pages integer := 100;
BEGIN
IF pg_event_trigger_table_rewrite_oid() = 'public.foo'::regclass
THEN
RAISE EXCEPTION 'you''re not allowed to rewrite the table %',
table_oid::regclass;
END IF;
SELECT INTO pages relpages FROM pg_class WHERE oid = table_oid;
IF pages > max_pages
THEN
RAISE EXCEPTION 'rewrites only allowed for table with less than % pages',
max_pages;
END IF;
IF current_hour NOT BETWEEN 1 AND 6
THEN
RAISE EXCEPTION 'rewrites only allowed between 1am and 6am';
END IF;
END;
$$;
CREATE EVENT TRIGGER no_rewrite_allowed
ON table_rewrite
EXECUTE FUNCTION no_rewrite();这份官方示例直接用 RAISE EXCEPTION 阻止重写,这回答了「能不能阻断」的问题:
结论:table_rewrite 中可以阻断重写
在 table_rewrite 事件中 RAISE EXCEPTION 可以阻止表被重写——因为事件在重写之前触发,此时命令还没执行,抛错会让整个 ALTER TABLE 失败回滚。官方文档的示例正是用这个手段实现「只在维护窗口允许重写」的策略。因此 table_rewrite 不仅能「告警」,也能「管控」。
这份示例还顺手演示了三种实用判据:
| 判据 | 用到的信息 |
|---|---|
| 表是否在禁止清单里 | pg_event_trigger_table_rewrite_oid() = 'public.foo'::regclass |
| 表是否「太大」 | 查 pg_class.relpages 与阈值比较 |
| 是否在允许的时间窗内 | extract('hour' from current_time) BETWEEN 1 AND 6 |
顺带说说 relpages
上面示例里的 pages 来自 pg_class.relpages(单位是 8KB 块)。注意它是 ANALYZE / VACUUM 时更新的估计值,不是实时精确值,用做「量级判断」足够,用做「精确额度」不可靠。
SELECT relpages,
pg_size_pretty(relpages::bigint * 8192) AS approx_size
FROM pg_class WHERE oid = 'big_table'::regclass;要做更稳的容量判断,可以结合 pg_total_relation_size():
SELECT pg_size_pretty(pg_total_relation_size('big_table')) AS total;盲区:CLUSTER、VACUUM FULL、REINDEX 不触发
这一点必须反复强调。table_rewrite 只能看到 ALTER TABLE / ALTER TYPE 引起的重写,而下面这些命令同样会把表整体重写一遍,却完全不触发事件:
| 命令 | 是否重写表 | 是否触发 table_rewrite |
|---|---|---|
CLUSTER / CLUSTER ... USING idx | 是 | 否 |
VACUUM FULL | 是 | 否 |
REINDEX / REINDEX TABLE | 重建索引(非表) | 否 |
ALTER TABLE ... SET TABLESPACE | 是 | 是 |
官方原文已明确:While other control statements are available to rewrite a table, like CLUSTER and VACUUM, the table_rewrite event is not triggered by them.
因此不能把 table_rewrite 当作「全量重写监控」。完整的重写治理必须同时覆盖:
① ALTER TABLE / ALTER TYPE 引起的重写 → table_rewrite 事件触发器
② CLUSTER / VACUUM FULL → 靠流程管控(禁止在生产直接执行)+ 监控长事务/锁等待
③ SET TABLESPACE 迁移 → table_rewrite 事件触发器(能覆盖)原因码为 0 的情况
如果触发器被触发但原因码为 0,说明触发了重写路径但四个已定义的位都没置位——这通常意味着成因在官方文档列出的四项之外(例如某些类型的连带重写),或者是版本差异。不要把 0 当成「没有原因」而忽略,在做管控策略时应把它视为「未知原因,按最严格处理」或至少记录告警。
DECLARE reason integer := pg_event_trigger_table_rewrite_reason();
BEGIN
IF reason = 0 THEN
RAISE WARNING 'table_rewrite 触发但原因码为 0,table=%,请人工确认',
pg_event_trigger_table_rewrite_oid()::regclass;
END IF;
END;拿到表名之外还能拿到什么
table_rewrite 事件没有类似 pg_event_trigger_ddl_commands() 的对象清单函数。你能拿到的只有:
- 即将重写的表的 OID(进而可查到表名、schema、大小、页数、所有者)
- 重写原因码
TG_TAG 变量在 plpgsql 里仍然可用,可以告诉你触发命令的命令标签:
RAISE NOTICE 'tag=% table=% reason=%',
TG_TAG,
pg_event_trigger_table_rewrite_oid()::regclass,
pg_event_trigger_table_rewrite_reason();NOTICE: tag=ALTER TABLE table=demo reason=4实战示例
示例一:重写审计表(记录 before/after 两阶段)
CREATE SCHEMA IF NOT EXISTS audit;
CREATE TABLE audit.rewrite_log (
id bigserial PRIMARY KEY,
logged_at timestamptz NOT NULL DEFAULT clock_timestamp(),
who text NOT NULL DEFAULT session_user,
db_name text NOT NULL DEFAULT current_database(),
tag text,
table_name text,
reason integer,
reason_text text,
relpages integer,
total_size bigint,
blocked boolean NOT NULL DEFAULT false
);
CREATE OR REPLACE FUNCTION audit.log_table_rewrite() RETURNS event_trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, audit AS $$
DECLARE
r_oid oid := pg_event_trigger_table_rewrite_oid();
r_reason integer := pg_event_trigger_table_rewrite_reason();
r_pages integer;
r_size bigint;
BEGIN
SELECT relpages INTO r_pages FROM pg_class WHERE oid = r_oid;
r_size := pg_total_relation_size(r_oid);
INSERT INTO audit.rewrite_log
(tag, table_name, reason, reason_text, relpages, total_size)
VALUES (TG_TAG, r_oid::regclass::text, r_reason,
rewrite_reason_text(r_reason), r_pages, r_size);
RAISE WARNING '表 % 即将被重写:reason=% (%),约 % 页',
r_oid::regclass, r_reason, rewrite_reason_text(r_reason), r_pages;
END;
$$;
CREATE EVENT TRIGGER trg_log_table_rewrite
ON table_rewrite
EXECUTE FUNCTION audit.log_table_rewrite();postgres=# ALTER TABLE demo ALTER COLUMN code TYPE varchar(128);
WARNING: 表 demo 即将被重写:reason=4 (column data type changed),约 12 页
ALTER TABLE
postgres=# SELECT table_name, reason, reason_text, relpages FROM audit.rewrite_log;
table_name | reason | reason_text | relpages
------------+--------+------------------------+----------
demo | 4 | column data type changed | 12
(1 row)示例二:生产环境「大表重写拦截器」
小表随便重写,大表必须走维护窗口——这是最实用的策略。
CREATE OR REPLACE FUNCTION guard.table_rewrite_policy() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
t_oid oid := pg_event_trigger_table_rewrite_oid();
t_name text := t_oid::regclass::text;
t_pages integer;
t_size bigint;
reason integer := pg_event_trigger_table_rewrite_reason();
hour_now integer := extract('hour' from current_time);
max_pages integer := 20000; -- 约 156MB,按你的环境调整
BEGIN
SELECT relpages INTO t_pages FROM pg_class WHERE oid = t_oid;
t_size := pg_total_relation_size(t_oid);
IF t_pages IS NULL OR t_pages <= max_pages THEN
RETURN; -- 小表放行
END IF;
-- 大表:仅允许维护窗口 02:00 - 05:00
IF hour_now BETWEEN 2 AND 5 THEN
RAISE WARNING '维护窗口内允许重写大表 %(% 页,reason=%)',
t_name, t_pages, reason;
RETURN;
END IF;
RAISE EXCEPTION '大表 % 禁止在非维护窗口重写(% 页,约 %,reason=%)',
t_name, t_pages, pg_size_pretty(t_size), rewrite_reason_text(reason)
USING HINT = '请在维护窗口(02:00-05:00)执行,或改用在线变更工具(如 pg_repack)';
END;
$$;
CREATE EVENT TRIGGER trg_table_rewrite_policy
ON table_rewrite
EXECUTE FUNCTION guard.table_rewrite_policy();postgres=# ALTER TABLE orders ALTER COLUMN order_no TYPE varchar(64);
ERROR: 大表 orders 禁止在非维护窗口重写(184320 页,约 1440 MB,reason=column data type changed)
HINT: 请在维护窗口(02:00-05:00)执行,或改用在线变更工具(如 pg_repack)注意这里用了 pg_size_pretty() 让报错信息带上人类可读的大小——一线工程师看到 1440 MB 会立刻明白为什么被拦,比只报页数有效得多。
示例三:纯告警模式(记录上下文,不阻断)
有些团队不希望事件触发器阻断变更(一旦策略写错,会连带阻塞所有发布),只想要「可观测性」。
CREATE OR REPLACE FUNCTION observe.rewrite_warning() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
t_oid oid := pg_event_trigger_table_rewrite_oid();
reason integer := pg_event_trigger_table_rewrite_reason();
BEGIN
RAISE WARNING '[table_rewrite] table=% reason=% session_user=% application_name=%',
t_oid::regclass, reason, session_user, current_setting('application_name')
USING HINT = '若为大表请确认已评估锁与 WAL 影响';
END;
$$;
CREATE EVENT TRIGGER trg_observe_rewrite
ON table_rewrite
EXECUTE FUNCTION observe.rewrite_warning();application_name 在这里是关键信息:它能告诉你这次重写是谁发起的(哪个应用、哪个迁移工具),比只知道「表被重写了」有用得多。可以配合 lsn / backend 信息落到日志系统。
示例四:把重写从「隐式」变成「显式决策」
下面这个函数把原因码翻译成人话,并针对最常见的两类场景给出不同建议:
CREATE OR REPLACE FUNCTION advise_rewrite() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
reason integer := pg_event_trigger_table_rewrite_reason();
t_name text := pg_event_trigger_table_rewrite_oid()::regclass::text;
BEGIN
IF reason & 2 <> 0 THEN
RAISE WARNING '%:列默认值变化触发重写(volatile 默认值)', t_name
USING HINT = '考虑先 ADD COLUMN 不带默认值,再分批 UPDATE,最后 SET DEFAULT';
END IF;
IF reason & 4 <> 0 THEN
RAISE WARNING '%:列类型变化触发重写', t_name
USING HINT = '评估是否可用「新增列 + 双写 + 切换」替代原地改类型';
END IF;
IF reason & 1 <> 0 THEN
RAISE WARNING '%:持久性变化触发重写(LOGGED/UNLOGGED)', t_name;
END IF;
IF reason & 8 <> 0 THEN
RAISE WARNING '%:访问方法变化触发重写', t_name;
END IF;
END;
$$;按位判断的价值在这里体现得最清楚:reason & 2 <> 0 能在 reason = 3(默认值 + 持久性同时变)时依然正确命中。
示例五:验证「阻断」确实生效,且事务被完整回滚
BEGIN;
ALTER TABLE demo ALTER COLUMN n TYPE bigint;
ROLLBACK;ERROR: 大表 demo 禁止在非维护窗口重写(...)被拦下后要确认:表结构没有被改动(因为报错来自重写之前,命令未执行)。
SELECT attname, atttypid::regtype
FROM pg_attribute
WHERE attrelid = 'demo'::regclass AND attnum > 0 AND NOT attisdropped; attname | atttypid
---------+----------
n | integer ← 仍是 integer,未被改成 bigint
(1 row)这是 table_rewrite 强于 ddl_command_end 的地方:它在命令执行前就拦住了,而不是执行完再回滚整张表的重写。
示例六:巡检当前的表重写策略
SELECT e.evtname,
e.evtevent,
e.evtenabled,
n.nspname || '.' || p.proname AS func,
pg_get_functiondef(p.oid) LIKE '%RAISE EXCEPTION%' AS can_block
FROM pg_event_trigger e
JOIN pg_proc p ON p.oid = e.evtfoid
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE e.evtevent = 'table_rewrite'; evtname | evtevent | evtenabled | func | can_block
-----------------------+---------------+------------+-------------------------+-----------
trg_log_table_rewrite | table_rewrite | O | audit.log_table_rewrite | f
trg_table_rewrite_policy | table_rewrite | O | guard.table_rewrite_policy | t
(2 rows)can_block 这一列虽然只是粗略的文本匹配,但足以在巡检时提醒你「这个触发器是会阻断变更的」——这在生产环境是一等重要信息。
注意事项
不要用等值比较判断原因码。 它是 bitmap,必须用按位与。
sql-- 错误! IF pg_event_trigger_table_rewrite_reason() = 4 THEN ... -- 正确 IF pg_event_trigger_table_rewrite_reason() & 4 <> 0 THEN ...不要引用官方未公开的
AT_REWRITE_*宏名。 官方 SQL 文档只给出数值1/2/4/8;源码宏名不是稳定接口。不要在
table_rewrite里调用pg_event_trigger_ddl_commands()或pg_event_trigger_dropped_objects()。 它们分别只允许在ddl_command_end与sql_drop中调用。不要以为
CLUSTER/VACUUM FULL会触发。 官方明确说明它们不触发table_rewrite。重写治理必须额外覆盖这两条路径。不要假设所有
ALTER TABLE都会重写,也不要假设都不会。SET DEFAULT免重写,但ADD COLUMN ... DEFAULT random()会重写——差别只在默认值表达式的 volatility。sql-- 不重写 ALTER TABLE t ADD COLUMN a timestamptz DEFAULT now(); -- 重写!volatile 默认值必须逐行求值 ALTER TABLE t ADD COLUMN b uuid DEFAULT gen_random_uuid();不要忽略
ALTER TYPE这条触发路径。 它不在ALTER TABLE的语感里,但官方定义明确包含它。不要把
relpages当精确大小。 它是ANALYZE/VACUUM时的估计值,做阈值判断足够,做精确计量请用pg_total_relation_size()。不要在管控触发器的报错信息里只写「禁止」。 要把表名、大小、原因、正确做法(窗口时间、替代工具)写进
HINT。不要让管控触发器「误伤」小表。 小表重写是毫秒级的,如果一刀切全部禁止,会逼得开发绕过机制。按
relpages或pg_total_relation_size()设阈值。不要忘记管控触发器一旦出错会阻塞所有发布。
RAISE EXCEPTION逻辑必须简单、无副作用、不依赖外部系统。建议先在影子库跑一轮全量迁移验证。不要在触发器中做重查询。 它会延长
ACCESS EXCLUSIVE锁的持有时间(重写前的等待也算进锁窗口)。不要忘记给管控留逃生通道。 被自己的策略锁死时,可以用
ALTER EVENT TRIGGER ... DISABLE(对事件触发器自身的命令不触发事件触发器)或event_triggers = false绕过。sqlALTER EVENT TRIGGER trg_table_rewrite_policy DISABLE; -- 执行必要变更 ALTER EVENT TRIGGER trg_table_rewrite_policy ENABLE;不要在审计表里只记表名不记操作者。 记
session_user、current_user、application_name,否则事后无法追责。
最佳实践
小表放行、大表管控,用阈值而不是一刀切。
sqlIF t_pages <= max_pages THEN RETURN; END IF;审计与管控拆成两个触发器。 审计永不阻断(只
RAISE WARNING+ 落库),管控负责拦截。这样某个策略写错也不至于让审计断档。sqlCREATE EVENT TRIGGER trg_log_rewrite ON table_rewrite EXECUTE FUNCTION audit.log_table_rewrite(); CREATE EVENT TRIGGER trg_guard_rewrite ON table_rewrite EXECUTE FUNCTION guard.table_rewrite_policy();用
application_name记录「谁发起的重写」。 这是事后定位「哪个迁移工具干的」最直接的线索。sqlcurrent_setting('application_name')把原因码解码成可读文本再落库与报错。 存数值方便统计,存文本方便人读,两个字段都留。
sqlreason, rewrite_reason_text(reason)对
reason = 0单独告警。 官方定义的四个位都没置位时不要静默放过。报错信息必须可操作。 带上表名、实际大小、重写原因、允许的时间窗、替代方案。
sqlRAISE EXCEPTION '大表 % 禁止在非维护窗口重写(约 %)', t_name, pg_size_pretty(t_size) USING HINT = '请在 02:00-05:00 执行,或改用 pg_repack';大表改类型优先考虑「新增列 + 双写 + 切换 + 删旧列」,把一次重写拆成多次轻量操作。
table_rewrite触发器的作用正是把这条路径「逼」出来。SET TABLESPACE也走同一套策略。 它是重写,会产生等量 WAL,务必安排在低峰期并预估磁盘余量。把重写的完整覆盖面写进运维手册。
table_rewrite事件 +CLUSTER/VACUUM FULL流程管控 + 长事务与锁等待监控,三者缺一不可。管控触发器逻辑保持极简、无外部依赖。 它是变更路径上的一环,任何额外故障都会放大成「发布全部失败」。
部署前在影子库用真实大小的数据演练。 确认阈值合理、报错可读、不会误伤常规迁移。
下一节
继续学习:事件触发器应用场景——把前面三节的能力组合成可落地的治理方案:DDL 审计、命名规范强制、权限管控、结构同步与变更告警。
参考链接
- PostgreSQL 18 官方文档:Handling a Table Rewrite Event(
table_rewrite信息函数与原因码) - PostgreSQL 18 官方文档:A Table Rewrite Event Trigger Example(官方
no_rewrite()范例) - PostgreSQL 18 官方文档:Overview of Event Trigger Behavior(
table_rewrite定义) - PostgreSQL 18 官方文档:ALTER TABLE(
SET DATA TYPE/ADD COLUMN/SET TABLESPACE/SET COMPRESSION) - PostgreSQL 18 官方文档:CREATE EVENT TRIGGER
- PostgreSQL 18 官方文档:pg_class(
relpages/relfilenode) - PostgreSQL 18 官方文档:System Information Functions(
pg_total_relation_size/pg_size_pretty)