触发器不能直接获取表大小,只能作为变更信号触发后续统计;应避免在触发器中调用pg_total_relation_size()等重量级函数,而应通过标记表+定时任务方式获取真实大小。

触发器无法直接获取表大小,必须结合系统视图或外部工具
SQL 触发器(INSERT/UPDATE/DELETE)运行在 DML 语句执行期间,它能看到行级变更,但看不到物理存储变化——比如页分裂、索引重建、LOB 溢出等真正影响 pg_total_relation_size() 或 sys.dm_db_partition_stats 的因素。所以「监控表大小变化」不能靠触发器实时计算大小,而只能用它作为「变更信号」来触发后续统计动作。
常见错误是试图在触发器里写:SELECT pg_total_relation_size('my_table') 然后 INSERT 到日志表——这本身可行,但结果滞后且不准:事务未提交时大小未落盘;并发写入时多次采样可能重复或漏计;而且 PostgreSQL 中该函数会加轻量锁,高频触发易阻塞。
- 正确做法:触发器只记录「发生了变更」,由独立调度任务(如 cron / pg_cron / SQL Server Agent)定期去查真实大小
- 若坚持全链路由触发器驱动,需搭配
pg_stat_all_tables的n_tup_ins/n_tup_del等累计计数,再辅以定时采样估算趋势 - MySQL 用户注意:
information_schema.TABLES中的data_length+index_length是近似值,InnoDB 表可能滞后数秒到数分钟
PostgreSQL 示例:用 AFTER INSERT OR DELETE 触发器标记待统计表
核心思路是避免在触发器中做重量级查询,只写一行轻量标记。后续由 pg_cron 每 5 分钟扫描一次标记表,批量查真实大小并归档。
CREATE TABLE table_size_log ( id SERIAL PRIMARY KEY, table_name TEXT NOT NULL, schema_name TEXT DEFAULT 'public', triggered_at TIMESTAMPTZ DEFAULT NOW(), size_bytes BIGINT, recorded_at TIMESTAMPTZ ); <p>CREATE TABLE table_change_marker ( schema_name TEXT, table_name TEXT, marked_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (schema_name, table_name) );</p><p>CREATE OR REPLACE FUNCTION mark_table_for_size_check() RETURNS TRIGGER AS $$ BEGIN INSERT INTO table_change_marker (schema_name, table_name) VALUES (TG_TABLE_SCHEMA, TG_TABLE_NAME) ON CONFLICT (schema_name, table_name) DO UPDATE SET marked_at = EXCLUDED.marked_at; RETURN NULL; END; $$ LANGUAGE plpgsql;</p><p>CREATE TRIGGER trig_mark_user_changes AFTER INSERT OR DELETE OR UPDATE ON users FOR EACH STATEMENT EXECUTE FUNCTION mark_table_for_size_check();
关键点:
-
AFTER ... FOR EACH STATEMENT而非FOR EACH ROW,避免高频写入时刷爆table_change_marker -
ON CONFLICT确保同一张表短时间内多次变更只留最新标记 - 不要在触发器里调用
pg_total_relation_size()—— 它会强制扫描所有块,对大表是灾难
SQL Server 中触发器调用 xp_cmdshell 查大小?别这么做
有人想在 AFTER INSERT 里跑 xp_cmdshell 调用 sqlcmd 查 sp_spaceused,这是严重设计错误。后果包括:
-
xp_cmdshell默认禁用,开启需sysadmin权限,违反最小权限原则 - 触发器内执行外部命令会极大拖慢事务响应,甚至导致死锁(尤其涉及 tempdb 或锁升级)
-
sp_spaceused返回结果集,触发器无法直接消费,硬接会导致语法错误或静默失败
替代方案更稳:
1. 在触发器中仅插入一行到 dbo.size_audit_queue(含 object_id, queue_time);
2. 配置 SQL Server Agent 作业,每 2 分钟执行:INSERT INTO dbo.size_history SELECT object_id, rows, reserved_page_count * 8192, GETDATE() FROM sys.dm_db_partition_stats WHERE object_id IN (SELECT object_id FROM dbo.size_audit_queue);
3. 清理已处理队列:DELETE FROM dbo.size_audit_queue WHERE queue_time < DATEADD(minute, -1, GETDATE());
统计表结构设计要注意时间精度与分区裁剪
如果你的 table_size_log 表每天新增上万条,不加约束很快变慢。别只加个 created_at 索引就完事。
- PostgreSQL:按
recorded_at做范围分区(PARTITION BY RANGE (recorded_at)),每月一区,过期分区DROP比DELETE快百倍 - SQL Server:用
datetime2(0)而非datetime,减少存储和比较开销;聚集索引建在(schema_name, table_name, recorded_at)上,方便按表查趋势 - 字段别存「MB」或「GB」:统一用
BIGINT size_bytes,避免浮点误差和单位转换混乱
最常被忽略的是:没设 recorded_at 的默认值或没在应用层/调度脚本里显式赋值,导致大量 NULL,后续 GROUP BY 或 time_bucket 统计全乱套。

















