只读副本上无法创建DDL触发器拦截变更,因数据库只读禁止写入系统表;必须在主库创建ON DATABASE级启用的DDL触发器,覆盖DROP/ALTER/TRUNCATE事件,用EVENTDATA解析对象名并RAISERROR中断,且需TRY/CATCH防XML解析失败。

只读副本上不能建 DDL 触发器拦截变更
SQL Server 的只读副本(如 Always On 辅助副本)默认禁止任何写操作,包括 CREATE TRIGGER。执行建触发器语句会直接报错:Msg 3906, Level 16, State 1: Failed to update database "xxx" because the database is read-only. —— 触发器本身是数据库对象,创建即写入系统表,只读副本不允许。所以“在只读副本上部署触发器来拦截变更”这个思路从底层就走不通。
真正起效的拦截必须在主副本上做 DDL 触发器
所有对用户表结构或数据的修改请求(DROP TABLE、TRUNCATE TABLE、ALTER TABLE 等)都必须先到达主副本,再通过日志复制同步到只读副本。因此,唯一可靠的位置是在主库上建数据库级 DDL 触发器:
- 触发器作用域必须是
ON DATABASE,不是ON TABLE - 必须显式启用:
ENABLE TRIGGER tr_block_ddl ON DATABASE(新建后默认is_disabled = 1) - 事件类型要覆盖全:
@EventType IN ('DROP_TABLE', 'ALTER_TABLE', 'TRUNCATE_TABLE'),漏掉TRUNCATE_TABLE就等于留后门 - 对象名必须用
CAST(EVENTDATA() AS XML).value('(/EVENT_INSTANCE/ObjectName)[1]', 'sysname')提取,OBJECT_NAME()在 DDL 触发器里返回NULL -
RAISERROR(..., 16, 1)才能中断事务;15 级以下只是警告,语句照常执行
别指望权限控制替代触发器
仅靠收回 db_owner 或 ALTER 权限,在生产环境不够用:
- 运维账号通常有
CONTROL权限,它能绕过大部分 DDL 权限检查 - SSMS 图形界面改列名、索引等操作,底层调用的是系统存储过程(如
sp_rename),不触发 DDL 事件,权限控制也拦不住 - 临时表命名绕过(如
CREATE TABLE #temp_drop_me)可被用于测试误删逻辑,但触发器若没用PROGRAM_NAME()或CONTEXT_INFO做上下文判断,就只能靠白名单严格过滤
触发器里必须加 TRY/CATCH 包裹 XML 解析
EVENTDATA() 返回的 XML 结构虽稳定,但 SQL Server 新版本(比如 2025+)可能调整节点名(如把 ObjectName 改成 TargetObjectName)。没 TRY/CATCH 的话,解析失败会导致整个事务回滚失败,连合法的 CREATE INDEX 都卡住:
BEGIN TRY
SET @ObjectName = CAST(EVENTDATA() AS XML).value('(/EVENT_INSTANCE/ObjectName)[1]', 'sysname')
SET @SchemaName = CAST(EVENTDATA() AS XML).value('(/EVENT_INSTANCE/SchemaName)[1]', 'sysname')
END TRY
BEGIN CATCH
RAISERROR('DDL event parsing failed. Operation blocked.', 16, 1)
ROLLBACK
RETURN
END CATCH
真正的难点不在语法,而在于:XML 节点名拼错(比如 ObjectNamee)、没处理空值、忘记 ENABLE TRIGGER、或者误建在 master 库——这些都会让拦截彻底失效,且不易排查。

















