一般来说,您不能ALTER DATABASE
在触发器(或其中包含其他语句的任何事务)中发出命令。如果尝试这样做,将出现以下错误:
多语句事务中不允许消息226,级别16,状态6,行xxxx ALTER DATABASE语句。
@sp_BlitzErik的答案中未遇到此错误的原因是所提供的特定测试用例的结果:上面显示的错误是运行时错误,而他的答案中遇到的错误是编译时错误。该编译时错误阻止了命令的执行,因此没有“运行时”。通过运行以下命令,我们可以看到不同之处:
SET NOEXEC ON;
SELECT N'g' COLLATE Latin1;
SET NOEXEC OFF;
上面的批处理将出错,而下面的将不会出错:
SET NOEXEC ON;
BEGIN TRAN
CREATE TABLE #t (Col1 INT);
ALTER DATABASE CURRENT COLLATE Latin1_General_100_BIN2;
ROLLBACK TRAN;
SET NOEXEC OFF;
这给您两个选择:
在DDL触发器内提交事务,以使事务中没有其他语句。如果有一个CREATE DATABASE
语句可以触发多个DDL触发器,那么这不是一个好主意,通常这是个坏主意,但它确实有效;-)。诀窍是您还需要在触发器中开始新的事务,否则SQL Server将注意到的开始和结束值@@TRANCOUNT
不匹配,并会引发与此相关的错误。下面的代码仅执行此操作,并且仅ALTER
在排序规则不是所需的排序规则时才发出,否则将跳过该ALTER
命令。
USE [master];
GO
CREATE TRIGGER trg_DDL_ChangeDatabaseCollation
ON ALL SERVER
FOR CREATE_DATABASE
AS
SET NOCOUNT ON;
DECLARE @CollationName [sysname] = N'Latin1_General_100_BIN2',
@SQL NVARCHAR(4000);
SELECT @SQL = N'ALTER DATABASE ' + QUOTENAME(sd.[name]) + N' COLLATE ' + @CollationName
FROM sys.databases sd
WHERE sd.[name] = EVENTDATA().value(N'(/EVENT_INSTANCE/DatabaseName)[1]', N'sysname')
AND sd.[collation_name] <> @CollationName;
IF (@SQL IS NOT NULL)
BEGIN
PRINT @SQL; -- DEBUG
COMMIT TRAN; -- close existing Transaction, else will get error
EXEC sys.sp_executesql @SQL;
BEGIN TRAN; -- begin new Transaction, else will get different error
END;
ELSE
BEGIN
PRINT 'Collation already correct.';
END;
GO
测试:
-- skip ALTER:
CREATE DATABASE [tttt] COLLATE Latin1_General_100_BIN2;
DROP DATABASE [tttt];
-- perform ALTER:
CREATE DATABASE [tttt] COLLATE SQL_Latin1_General_CP1_CI_AI;
DROP DATABASE [tttt];
使用SQLCLR 在连接字符串中建立一个常规/外部SqlConnection
,Enlist = false;
以发出ALTER
命令,因为这将不是事务的一部分。
尽管不是由于SQLCLR的任何特定限制,但似乎SQLCLR并不是真正的选择。不知何故,直接在上方键入“ 因为那将不属于事务 ”,不足以突出显示以下事实:该CREATE DATABASE
操作周围确实存在活动的事务。这里的问题是,尽管可以使用SQLCLR移出当前事务,但是直到该初始事务提交之前,另一个会话仍无法修改当前正在创建的数据库。
意思是,会话A创建用于创建数据库和触发触发器的事务。触发器将使用SQLCLR创建会话B来修改已创建的数据库,但是由于事务在会话B完成之前一直处于暂挂状态,因此事务尚未提交,因为事务正在等待初始事务发送到数据库,所以触发器无法执行完成。这是一个死锁,但是SQL Server无法检测到这种死锁,因为它不知道会话B是由会话A中的某些东西创建的。可以通过替换IF
示例中语句的第一部分来看到此行为。以上在#1中具有以下内容:
IF (@SQL IS NOT NULL)
BEGIN
/*
PRINT @SQL; -- DEBUG
COMMIT TRAN; -- close existing Transaction, else will get error
EXEC sys.sp_executesql @sql;
BEGIN TRAN; -- begin new Transaction, else will get different error
*/
DECLARE @CMD NVARCHAR(MAX) = N'EXEC xp_cmdshell N''sqlcmd -S . -d master -E -Q "'
+ @SQL + N';" -t 15''';
PRINT @CMD;
EXEC (@CMD);
END;
ELSE
...
SQLCMD的-t 15
开关设置命令/查询超时,以便测试不会在默认超时后永远等待。但是,您可以将其设置为超过15秒,然后在另一个会话中检查所有可爱的阻塞是否正在进行;-)。sys.dm_exec_requests
将事件排队,然后将其从该队列中读取并执行适当的ALTER DATABASE
语句。这将允许该CREATE DATABASE
语句完成并提交其事务,然后ALTER DATABASE
可以执行一条语句。可以在这里使用Service Broker。或者,创建一个表,将Trigger插入到该表中,然后让一个SQL Server Agent作业调用一个存储过程,该存储过程从该表中读取并执行该ALTER DATABASE
语句,然后从队列表中删除该记录。
但是,以上选项主要用于在某些人确实确实需要ALTER DATABASE
在DDL触发器中执行某种类型的操作的情况下提供帮助。在这种特定情况下,如果您确实不希望任何数据库使用系统/实例级默认排序规则,那么最好通过以下方法为您提供服务:
- 使用所需的归类创建一个新实例,并将所有用户数据库移至该实例。
- 或者,如果只是非理想归类的系统数据库,则可以通过setup.exe从命令行更改系统归类(例如
Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=...
;此选项可重新创建系统DB,因此很安全)。脚本出服务器级别的对象等,以便稍后重新创建,以及重新应用补丁等(FUN,FUN,FUN)。
或者,对于有冒险精神的人,可以使用未记录的(即不受支持的,使用自己的风险,但可能工作得很好)sqlservr.exe -q
选项来更新所有数据库和所有列(请参阅更改实例,数据库和所有用户数据库中的所有列的排序规则:可能出错的地方?以详细说明此选项的行为以及可能的影响范围。
不管选项的选择:始终确保有备份master
和msdb
尝试这样的事情之前。
值得更改服务器级别默认排序规则的原因是实例(即服务器级别)的默认排序规则控制了一些可能导致意外/不一致行为的功能区域,因为每个人都希望字符串操作起作用遵循所有用户数据库的默认排序规则:
临时表中字符串列的默认排序规则。仅当与其他字符串列进行比较/合并时,如果两个字符串列之间不匹配,则这是一个问题。这里的问题是,当未通过COLLATE
关键字明确指定归类时,很有可能(尽管不能保证)遇到问题。
对于XML数据类型,表变量或所包含的数据库,这不是问题。
实例级元数据。例如,中的name
字段sys.databases
将使用实例级别的默认排序规则。其他系统目录视图也受到影响,但是我没有完整的列表。
数据库级元数据(例如sys.objects
和)sys.indexes
不受影响。
- 名称解析:
- 局部变量(即
@variable
)
- 游标
GOTO
标签
例如,如果实例级别的排序规则不区分大小写,而数据库级别的排序规则是二进制的(即,以_BIN
或结尾_BIN2
),则数据库级别的对象名称解析将为二进制的(例如[TableA] <> [tableA]
),而变量名称将允许不区分大小写(例如@VariableA = @variableA
)。