创建时更改数据库排序规则的触发器

Rac*_*SQL 9 trigger sql-server collation sql-server-2008-r2 ddl

我正在尝试创建一个触发器,以在创建时更改数据库的排序规则,但是如何捕获要在触发器内部使用的数据库名称?

USE master
GO
CREATE TRIGGER trg_DDL_ChangeCOllationDatabase
ON ALL SERVER
FOR CREATE_DATABASE
AS
declare @databasename varchar(200)
set @databasename =db_name()
    ALTER DATABASE @databasename COLLATE xxxxxxxxxxxxxxxxxxx
GO
Run Code Online (Sandbox Code Playgroud)

显然,这是行不通的。

Eri*_*ing 11

您需要使用动态 SQL 和EVENTDATA() 函数

USE master
GO
CREATE TRIGGER trg_DDL_ChangeCOllationDatabase
ON ALL SERVER
FOR CREATE_DATABASE
AS
SET NOCOUNT ON; 
DECLARE @databasename NVARCHAR(256) = N''
DECLARE @event_data XML; 
DECLARE @sql NVARCHAR(4000) = N''

SET @event_data = EVENTDATA()

SET @databasename = @event_data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(256)') 

SET @sql += 'ALTER DATABASE ' + QUOTENAME(@databasename) + ' COLLATE al''z a-b-cee''z'

PRINT @sql

EXEC sys.sp_executesql @sql

GO
Run Code Online (Sandbox Code Playgroud)

只需在您的校对中替换我的假名即可

现在当我创建一个数据库...

CREATE DATABASE DingDong
Run Code Online (Sandbox Code Playgroud)

我收到这条消息(来自印刷品):

ALTER DATABASE [叮咚] COLLATE al'z ab-cee'z

请注意,如果其他数据库(包括 tempdb)使用不同的排序规则,您可能会遇到比较字符串数据的问题。您必须将 COLLATE 子句添加到字符串比较中,其中大小写或重音很重要,即使它们不重要,您也可能会遇到错误。相关问题在那里我遇到了一个类似的代码的问题在这里


Sol*_*zky 8

一般来说,您不能ALTER DATABASE在触发器(或任何包含其他语句的事务)内发出。如果您尝试这样做,您将收到以下错误:

消息 226,级别 16,状态 6,行 xxxx
ALTER DATABASE 语句不允许在多语句事务中使用。

@sp_BlitzErik 的回答中没有遇到此错误的原因是提供的特定测试用例的结果:上面显示的错误是运行时错误,而他的回答中遇到的错误是编译时错误。该编译时错误阻止了命令的执行,因此没有“运行时”。我们可以通过运行以下命令来查看差异:

SET NOEXEC ON;

SELECT N'g' COLLATE Latin1;

SET NOEXEC OFF;
Run Code Online (Sandbox Code Playgroud)

上面的批处理会出错,而下面的不会:

SET NOEXEC ON;

BEGIN TRAN
CREATE TABLE #t (Col1 INT);
ALTER DATABASE CURRENT COLLATE Latin1_General_100_BIN2;
ROLLBACK TRAN;

SET NOEXEC OFF;
Run Code Online (Sandbox Code Playgroud)

这给您留下了两个选择:

  1. 在 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
    
    Run Code Online (Sandbox Code Playgroud)

    测试:

    -- 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];
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用 SQLCLR在连接字符串中建立一个常规 / 外部SqlConnectionEnlist = 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
    ...
    
    Run Code Online (Sandbox Code Playgroud)

    SQLCMD-t 15开关设置命令/查询超时,以便测试不会在默认超时下永远等待。但是,您可以将其设置为超过 15 秒,并在另一个会话中检查以查看所有可爱的阻塞正在进行 ;-)。sys.dm_exec_requests

  3. 将事件排队,然后从该队列中读取并执行适当的ALTER DATABASE语句。这将允许CREATE DATABASE语句完成并提交其事务,然后ALTER DATABASE可以执行语句。这里可以使用 Service Broker。或者,创建一个表,让触发器插入到该表中,然后让 SQL Server 代理作业调用一个存储过程,该过程从该表中读取并执行该ALTER DATABASE语句,然后从队列表中删除该记录。

但是,提供上述选项主要是为了在有人确实需要ALTER DATABASE在 DDL 触发器中执行某种类型的情况下提供帮助。在这种特殊情况下,如果您真的不希望任何数据库使用系统/实例级默认排序规则,那么您可能会得到最好的服务:

  1. 创建具有所需排序规则的新实例并将所有用户数据库移至该实例。
  2. 或者,如果只是系统数据库属于非理想排序规则,则通过 setup.exe 从命令行更改系统排序规则可能是安全的(例如Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=...;此选项会重新创建系统 DB,因此您需要编写服务器级对象等的脚本以便稍后重新创建,加上重新应用补丁等,FUN,FUN,FUN)。
  3. 或者,对于具有冒险精神的人来说,有一个未记录的(即不受支持的,使用您自己的风险,但可能会很好地工作)sqlservr.exe -q选项可以更新所有数据库和所有列(请参阅更改实例、数据库和所有用户数据库中的所有列的整理:可能出现什么问题?有关此选项行为的详细说明以及潜在影响范围)。

    无论选择哪个选项:在尝试此类操作之前,请始终确保备份mastermsdb

更改服务器级默认排序规则值得努力的原因是,实例(即服务器级)默认排序规则控制了一些可能导致意外/不一致行为的功能区域,因为每个人都希望字符串操作正常运行沿着所有用户数据库的默认排序规则:

  1. 临时表中字符串列的默认排序规则。如果两个字符串列之间不匹配,则仅在与其他字符串列进行比较/联合时才会出现此问题。这里的问题是,当没有通过COLLATE关键字明确指定排序规则时,更有可能(虽然不能保证)遇到问题。

    对于 XML 数据类型、表变量或包含的数据库,这不是问题。

  2. 实例级元数据。例如,name字段 insys.databases将使用实例级别的默认排序规则。其他系统目录视图也受到影响,但我没有完整列表。

    数据库级元数据,例如sys.objectssys.indexes,不受影响。

  3. 名称解析:
    1. 局部变量(即@variable
    2. 游标
    3. GOTO 标签

例如,如果实例级排序规则不区分大小写而数据库级排序规则是二进制的(即以_BIN或结尾_BIN2),那么数据库级对象名称解析将是二进制的(例如[TableA] <> [tableA]),但变量名称将允许不区分大小写(例如@VariableA = @variableA)。