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


9

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

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

显然,这是行不通的。


1
您是否有理由不能仅将MODEL数据库更改为所需的排序规则?-所有新创建的数据库将使用模型作为模板
斯科特霍金

我试过了,但是它说模型数据库是系统数据库,所以我不能更改它。
Racer SQL

因此,您的系统数据库将与用户数据库的排序规则不同?您是否考虑过临时表等潜在的整理问题?
George.Palacios '17

哇,是的,我5分钟前就读过。没想到。那不是一个好主意。
Racer SQL

Answers:


8

一般来说,您不能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;

这给您两个选择:

  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

    测试:

    -- 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];
  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
    ...

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

  3. 将事件排队,然后将其从该队列中读取并执行适当的ALTER DATABASE语句。这将允许该CREATE DATABASE语句完成并提交其事务,然后ALTER DATABASE可以执行一条语句。可以在这里使用Service Broker。或者,创建一个表,将Trigger插入到该表中,然后让一个SQL Server Agent作业调用一个存储过程,该存储过程从该表中读取并执行该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字段sys.databases将使用实例级别的默认排序规则。其他系统目录视图也受到影响,但是我没有完整的列表。

    数据库级元数据(例如sys.objects和)sys.indexes不受影响。

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

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


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

只需将您的归类分给我的假货即可

现在,当我创建数据库时...

CREATE DATABASE DingDong

我收到此消息(从打印):

ALTER DATABASE [DingDong]收集al'z ab-cee'z

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


1
@RafaelPiccinelli和Erik:仅供参考,这个答案并不完全正确。该代码不起作用,但是由于使用无效的归类名称进行测试,因此掩盖了实际错误。我更新了答案以进行解释(朝上),因为它太多了,无法发表评论。
所罗门·鲁兹基

2

您无法ALTER DATABASE触发。您需要通过评估和纠正来发挥创造力。就像是:

EXEC sp_MSforeachdb N'IF EXISTS 
(
     select top 1 name from sys.databases where collation_name != 
     SQL_Latin1_General_CP1_CI_AS
)
BEGIN
    -- do something
END';

尽管您不应该使用sp_MSforeachdb

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.