Auslöser zum Ändern der Datenbankkollatierung bei der Erstellung

9

Ich versuche, einen Trigger zu erstellen, um die Sortierung einer Datenbank bei ihrer Erstellung zu ändern. Wie kann ich jedoch den Datenbanknamen abrufen, der im Trigger verwendet werden soll?

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

Offensichtlich funktioniert das nicht.

Racer SQL
quelle
1
Gibt es einen Grund, warum Sie die MODEL-Datenbank nicht einfach auf die gewünschte Sortierung ändern können? - Alle neu erstellten Datenbanken würden MODEL als Vorlage verwenden
Scott Hodgin
Ich habe das versucht, aber es heißt, dass die Modelldatenbank eine Systemdatenbank ist, daher kann ich sie nicht ändern.
Racer SQL
Ihre Systemdatenbanken befinden sich also in einer anderen Sortierung als die Ihrer Benutzerdatenbanken? Haben Sie mögliche Kollatierungsprobleme mit temporären Tabellen usw. berücksichtigt?
George.Palacios
Wow, ja, ich habe es wie vor 5 Minuten gelesen. Daran habe ich nicht gedacht. Das ist keine gute Idee.
Racer SQL

Antworten:

8

Sie können im Allgemeinen keine Ausgabe ALTER DATABASEinnerhalb eines Triggers (oder einer Transaktion mit anderen Anweisungen) ausgeben . Wenn Sie dies versuchen, wird folgende Fehlermeldung angezeigt:

Meldung 226, Ebene 16, Status 6, Zeile xxxx
ALTER DATABASE-Anweisung innerhalb einer Transaktion mit mehreren Anweisungen nicht zulässig.

Der Grund, warum dieser Fehler in der Antwort von @ sp_BlitzErik nicht aufgetreten ist, ist ein Ergebnis des angegebenen speziellen Testfalls: Der oben gezeigte Fehler ist ein Laufzeitfehler, während der in seiner Antwort aufgetretene Fehler ein Fehler zur Kompilierungszeit ist. Dieser Fehler bei der Kompilierung verhindert die Ausführung des Befehls und daher gibt es keine "Laufzeit". Wir können den Unterschied erkennen, indem wir Folgendes ausführen:

SET NOEXEC ON;

SELECT N'g' COLLATE Latin1;

SET NOEXEC OFF;

Der obige Stapel wird fehlerhaft sein, während der folgende nicht:

SET NOEXEC ON;

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

SET NOEXEC OFF;

Damit haben Sie zwei Möglichkeiten:

  1. Übernehmen Sie die Transaktion innerhalb des DDL-Triggers so, dass die Transaktion keine weiteren Anweisungen enthält. Dies ist keine gute Idee, wenn es mehrere DDL-Trigger gibt, die von einer CREATE DATABASEAnweisung ausgelöst werden können , und ist möglicherweise eine schlechte Idee im Allgemeinen, aber es funktioniert ;-). Der Trick besteht darin, dass Sie auch eine neue Transaktion im Trigger starten müssen, da SQL Server sonst feststellt, dass die Anfangs- und Endwerte für @@TRANCOUNTnicht übereinstimmen, und einen diesbezüglichen Fehler auslöst. Der folgende Code macht genau dies und gibt auch nur dann aus, ALTERwenn die Sortierung nicht die gewünschte ist, andernfalls wird der ALTERBefehl übersprungen .

    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

    Test mit:

    -- 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. Verwenden SQLCLR ein regelmäßigen / extern zu schaffen SqlConnection, mit Enlist = false;der Connection String, der zur Ausgabe von ALTERBefehl als das nicht Teil der Transaktion sein wird.

    Es scheint, dass SQLCLR keine echte Option ist, obwohl dies nicht auf eine spezifische Einschränkung von SQLCLR zurückzuführen ist. Irgendwie hat die Eingabe von " da dies nicht Teil der Transaktion sein wird " direkt oben die Tatsache nicht ausreichend hervorgehoben, dass es tatsächlich eine aktive Transaktion um die CREATE DATABASEOperation gibt. Das Problem hierbei ist , dass während SQLCLR kann zu Schritt außerhalb der aktuellen Transaktion verwendet wird, gibt es noch keine Möglichkeit für eine weitere Sitzung der Datenbank zu ändern , zur Zeit erstellt wird , bis dass die erste Transaktion Commits.

    Das heißt, Sitzung A erstellt die Transaktion für die Erstellung der Datenbank und das Auslösen des Triggers. Der Abzug, mit SQLCLR wird Session B erstellen , die Datenbank zu ändern , die erstellt wurde, aber die Transaktion noch nicht begangen , da sie in der Warteschleife, bis Session B abgeschlossen ist , was es kann nicht , weil es für diese erste Transaktion wartet auf Komplett. Dies ist ein Deadlock, kann jedoch von SQL Server nicht als solcher erkannt werden, da nicht bekannt ist, dass Sitzung B von etwas in Sitzung A erstellt wurde. Dieses Verhalten kann durch Ersetzen des ersten Teils der IFAnweisung im Beispiel festgestellt werden oben in # 1 mit folgendem:

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

    Der -t 15Schalter für SQLCMD legt das Befehls- / Abfragezeitlimit fest, damit der Test mit dem Standardzeitlimit nicht ewig wartet. Aber du kannst es länger als 15 Sekunden einstellen und in einer anderen Sitzung überprüfen sys.dm_exec_requests, ob all die schönen Blockierungen stattfinden ;-).

  3. Stellen Sie das Ereignis an eine Stelle, die dann aus dieser Warteschlange gelesen wird, und führen Sie die entsprechende ALTER DATABASEAnweisung aus. Auf diese Weise kann die CREATE DATABASEAnweisung abgeschlossen und ihre Transaktion festgeschrieben werden. Anschließend ALTER DATABASEkann eine Anweisung ausgeführt werden. Service Broker könnte hier verwendet werden. ODER erstellen Sie eine Tabelle, lassen Sie den Trigger in diese Tabelle einfügen, und lassen Sie einen SQL Server-Agent-Job eine gespeicherte Prozedur aufrufen, die aus dieser Tabelle liest, die ALTER DATABASEAnweisung ausführt und dann den Datensatz aus der Warteschlangentabelle entfernt.

Die oben genannten Optionen werden jedoch hauptsächlich zur Unterstützung von Szenarien bereitgestellt, in denen jemand wirklich eine Art von ALTER DATABASEDDL-Trigger ausführen muss. Wenn Sie in diesem speziellen Szenario wirklich nicht möchten, dass Datenbanken die Standardkollatierung auf System- / Instanzebene verwenden, werden Sie wahrscheinlich am besten bedient von:

  1. Erstellen einer neuen Instanz mit der gewünschten Sortierung und Verschieben aller Benutzerdatenbanken in diese.
  2. Wenn es sich nur um die Systemdatenbanken handelt, die nicht der idealen Sortierung entsprechen, ist es wahrscheinlich sicher, die Systemkollatierung über die Befehlszeile über setup.exe zu ändern (z Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=.... B. diese Option erstellt die System-DBs neu, sodass Sie sie benötigen um Objekte auf Serverebene usw. zu skripten, um sie später neu zu erstellen, und um Patches usw. erneut anzuwenden (FUN, FUN, FUN).
  3. Oder für Abenteuerlustige gibt es die undokumentierte sqlservr.exe -qOption (dh nicht unterstützte Option, die auf eigenes Risiko verwendet wird, aber möglicherweise sehr gut funktioniert) , mit der ALLE DBs und ALLE Spalten aktualisiert werden (siehe Ändern Die Zusammenstellung der Instanz, der Datenbanken und aller Spalten in allen Benutzerdatenbanken: Was könnte möglicherweise schief gehen? ( für eine detaillierte Beschreibung des Verhaltens dieser Option sowie des möglichen Einflussbereichs).

    Unabhängig von der gewählten Option: Stellen Sie immer sicher, dass Sie Backups von masterund haben, msdbbevor Sie solche Dinge versuchen.

Der Grund, warum es sich lohnt, die Standardkollatierung auf Serverebene zu ändern, besteht darin, dass die Standardkollatierung der Instanz (dh auf Serverebene) einige Funktionsbereiche steuert, die zu unerwartetem / inkonsistentem Verhalten führen können, da jeder erwartet, dass Zeichenfolgenoperationen funktionieren in Anlehnung an die Standardkollatierung für alle Ihre Benutzerdatenbanken:

  1. Standardkollatierung für Zeichenfolgenspalten in temporären Tabellen. Dies ist nur beim Vergleich mit / Unioning mit anderen Zeichenfolgenspalten ein Problem, wenn zwischen den beiden Zeichenfolgenspalten eine Nichtübereinstimmung besteht. Das Problem hierbei ist, dass COLLATEes viel wahrscheinlicher (wenn auch nicht garantiert) zu Problemen kommt, wenn die Sortierung nicht explizit über das Schlüsselwort angegeben wird.

    Dies ist kein Problem für den XML-Datentyp, Tabellenvariablen oder enthaltene Datenbanken.

  2. Metadaten auf Instanzebene. In dem nameFeld in sys.databaseswird beispielsweise die Standardkollatierung auf Instanzebene verwendet. Andere Systemkatalogansichten sind ebenfalls betroffen, aber ich habe nicht die vollständige Liste.

    Metadaten auf Datenbankebene wie sys.objectsund sys.indexessind nicht betroffen.

  3. Namensauflösung für:
    1. lokale Variablen (dh @variable)
    2. Cursor
    3. GOTO Etiketten

Wenn bei der Sortierung auf Instanzebene beispielsweise die Groß- und Kleinschreibung nicht berücksichtigt wird, während die Sortierung auf Datenbankebene binär ist (dh mit _BINoder endet _BIN2), ist die Auflösung von Objektnamen auf Datenbankebene binär (z. B. [TableA] <> [tableA]), bei Variablennamen wird jedoch die Groß- und Kleinschreibung nicht berücksichtigt (zB @VariableA = @variableA).

Solomon Rutzky
quelle
11

Sie müssten dynamisches SQL und die Funktion EVENTDATA () verwenden .

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

Geben Sie einfach Ihre Sammlung für meine Fälschung ein .

Jetzt, wenn ich eine Datenbank erstelle ...

CREATE DATABASE DingDong

Ich bekomme diese Nachricht (aus dem Druck):

ALTER DATABASE [DingDong] COLLATE al'z ab-cee'z

Beachten Sie nur, dass beim Vergleich anderer Zeichenfolgendaten Probleme auftreten können, wenn andere Datenbanken (einschließlich Tempdb) andere Kollatierungen verwenden. Sie müssten COLLATE-Klauseln zu Zeichenfolgenvergleichen hinzufügen, bei denen Groß- und Kleinschreibung oder Akzente eine Rolle spielen, und selbst wenn dies nicht der Fall ist, können Sie Fehler feststellen. Verwandte Frage , wo ich in einem ähnlichen Code Problem lief hier .

Erik Darling
quelle
1
@ RafaelPiccinelli und Erik: Nur zu Ihrer Information, diese Antwort ist nicht ganz richtig. Der Code funktioniert nicht, aber der tatsächliche Fehler wird aufgrund des Tests unter Verwendung eines ungültigen Sortierungsnamens maskiert. Ich habe meine Antwort aktualisiert, um sie zu erklären (nach oben), da sie für einen Kommentar zu viel war.
Solomon Rutzky
2

Sie können nicht ALTER DATABASEin einem Auslöser. Sie müssen kreativ mit der Bewertung und Korrektur werden. Etwas wie:

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';

Sie sollten sp_MSforeachdb jedoch nicht verwenden .

Henrico Bekker
quelle