Триггер для изменения сортировки базы данных при создании

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

Очевидно, это не работает.

Racer SQL
источник
1
Есть ли причина, по которой вы не можете просто изменить базу данных MODEL на требуемое сопоставление? - все вновь созданные базы данных будут использовать MODEL в качестве шаблона
Скотт Ходгин
Я попробовал это, но там говорится, что база данных модели - это системная база данных, поэтому я не могу ее изменить.
Racer SQL
Таким образом, ваши системные базы данных будут в другом сопоставлении с базами данных вашего пользователя? Рассматривали ли вы потенциальные проблемы сопоставления с временными таблицами и т. Д.?
George.Palacios
Вау, да, я прочитал это как 5 минут назад. Не думал об этом. Это не хорошая идея.
Racer SQL

Ответы:

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 так, чтобы в транзакции не было других операторов. Это не очень хорошая идея, если есть несколько триггеров DDL, которые могут быть запущены CREATE DATABASEоператором, и, возможно, это плохая идея в целом, но она работает ;-). Хитрость заключается в том, что вам также нужно начать новую транзакцию в триггере, иначе 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, чтобы установить обычный / внешний 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
    ...

    -t 15Переключатель для SQLCMD задает команду / запрос тайм - аут , так что тест не ждать вечно с тайм - аут по умолчанию. Но вы можете установить его длительностью более 15 секунд и в другом сеансе проверить, sys.dm_exec_requestsчтобы увидеть все происходящие прекрасные блокировки ;-).

  3. Поставьте в очередь событие где-нибудь, чтобы потом прочитать из этой очереди и выполнить соответствующий ALTER DATABASEоператор. Это позволит CREATE DATABASEоператору завершиться и выполнить транзакцию, после чего ALTER DATABASEоператор может быть выполнен. Сервисный брокер может быть использован здесь. ИЛИ, создайте таблицу, вставьте триггер в эту таблицу, затем задание агента SQL Server вызовет хранимую процедуру, которая читает из этой таблицы и выполняет ALTER DATABASEинструкцию, а затем удаляет запись из таблицы очереди.

ОДНАКО вышеупомянутые опции в основном предоставлены, чтобы помочь в сценариях, где кто-то действительно должен сделать некоторый тип в ALTER DATABASEпределах DDL Trigger. В этом конкретном сценарии, если вы действительно не хотите, чтобы какие-либо базы данных использовали параметры сортировки по умолчанию на уровне системы / экземпляра, вам, вероятно, лучше всего подойдут:

  1. Создание нового экземпляра с желаемой сортировкой и перенос всех пользовательских баз данных на него.
  2. Или, если это не только системные базы данных, которые имеют неидеальную сортировку, вероятно, безопасно изменить системную сортировку из командной строки через setup.exe (например Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=..., эта опция воссоздает системные базы данных, поэтому вам потребуется для создания сценариев объектов уровня сервера и т. д. для последующего создания, а также повторного применения исправлений и т. д., FUN, FUN, FUN).
  3. Или, для приключений в глубине души, есть недокументированный (то есть неподдерживаемый sqlservr.exe -qвариант « используйте на свой страх и риск, но возможно, очень хорошо работает»), который обновляет ВСЕ БД и ВСЕ столбцы (см. Изменение «Сравнение экземпляра, баз данных и всех столбцов во всех пользовательских базах данных: что может быть неправильным» для подробного описания поведения этой опции, а также потенциальной области влияния).

    Независимо от выбранного варианта: всегда проверяйте наличие резервных копий masterи msdbперед попыткой таких действий .

Причиной того, что стоит изменить сортировку по умолчанию на уровне сервера, является то, что по умолчанию сортировка экземпляра (т.е. уровня сервера) контролирует несколько функциональных областей, которые могут привести к неожиданному / противоречивому поведению, поскольку все ожидают, что строковые операции будут функционировать в соответствии с параметрами сортировки по умолчанию для всех ваших пользовательских баз данных:

  1. Сортировка по умолчанию для строковых столбцов во временных таблицах. Эта проблема возникает только при сравнении / Объединение с другими строковыми столбцами, ЕСЛИ существует несоответствие между двумя строковыми столбцами. Проблема здесь заключается в том, что, если не указать Collation в явном виде через 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] COLLATE al'z ab-cee'z

Просто отметьте, что если другие базы данных (включая tempdb) используют другие параметры сортировки, вы можете столкнуться с проблемами при сравнении строковых данных. Вы должны были бы добавить предложения COLLATE к сравнениям строк, где регистр или акценты имеют значение, и даже если это не так, вы можете столкнуться с ошибками. Связанный вопрос, где я столкнулся с подобной проблемой кода здесь .

Эрик Дарлинг
источник
1
@RafaelPiccinelli и Эрик: просто к вашему сведению, этот ответ не совсем правильный. Код не работает, но настоящая ошибка маскируется из-за теста с использованием недопустимого имени сопоставления. Я обновил свой ответ, чтобы объяснить (к началу), поскольку это было слишком много для комментария.
Соломон Руцкий
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 .

Хенрико Беккер
источник