Activar para cambiar la clasificación de la base de datos en la creación

9

Estoy tratando de crear un disparador, para alterar la recopilación de una base de datos en su creación, pero ¿cómo puedo atrapar el nombre de la base de datos para usar dentro del disparador?

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

Obviamente, esto no está funcionando.

Racer SQL
fuente
1
¿Hay alguna razón por la que no puede simplemente cambiar la base de datos MODEL a la clasificación requerida? - todas las bases de datos recién creadas usarían MODEL como plantilla
Scott Hodgin
Lo intenté pero dice que la base de datos modelo es una base de datos del sistema, por lo que no puedo cambiarla.
Racer SQL
Entonces, ¿las bases de datos de su sistema estarán en una clasificación diferente a la de su usuario? ¿Ha considerado posibles problemas de colación con tablas temporales, etc.?
George.Palacios
Wow, sí, lo leí hace como 5 minutos. No pensé en eso. Esa no es una buena idea.
Racer SQL

Respuestas:

8

En general, no puede emitir ALTER DATABASEdentro de un Disparador (o cualquier Transacción que contenga otras declaraciones). Si lo intentas, obtendrás el siguiente error:

Msg 226, Nivel 16, Estado 6, Línea xxxx
ALTER DATABASE no está permitida en la transacción de varios estados.

La razón por la que este error no se encontró en la respuesta de @ sp_BlitzErik es el resultado del caso de prueba específico proporcionado: el error que se muestra arriba es un error en tiempo de ejecución, mientras que el error encontrado en su respuesta es un error en tiempo de compilación. Ese error en tiempo de compilación impide la ejecución del comando y, por lo tanto, no hay "tiempo de ejecución". Podemos ver la diferencia ejecutando lo siguiente:

SET NOEXEC ON;

SELECT N'g' COLLATE Latin1;

SET NOEXEC OFF;

El lote anterior producirá un error, mientras que lo siguiente no:

SET NOEXEC ON;

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

SET NOEXEC OFF;

Esto te deja con dos opciones:

  1. Confirme la transacción dentro del DDL Trigger de modo que no haya otras declaraciones en la transacción. Esta no es una buena idea si hay varios disparadores DDL que pueden ser disparados por una CREATE DATABASEdeclaración, y es posiblemente una mala idea en general, pero funciona ;-). El truco es que también debe comenzar una nueva transacción en el activador, de lo contrario, SQL Server notará que los valores iniciales y finales @@TRANCOUNTno coinciden y arrojará un error relacionado con eso. El código a continuación hace exactamente esto, y también solo emite ALTERsi la Clasificación no es la deseada, de lo contrario, omite el ALTERcomando.

    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
    

    Prueba con:

    -- 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. Use SQLCLR para establecer un regular / externo SqlConnection, con Enlist = false;la Cadena de conexión, para emitir el ALTERcomando ya que eso no será parte de la Transacción.

    Parece que SQLCLR no es realmente una opción, aunque no debido a ninguna limitación específica de SQLCLR. De alguna manera, escribir " ya que eso no será parte de la Transacción " directamente arriba no resaltó suficientemente el hecho de que hay, de hecho, una Transacción activa alrededor de la CREATE DATABASEoperación. El problema aquí es que, si bien SQLCLR se puede usar para salir de la transacción actual, todavía no hay forma de que otra sesión modifique la base de datos que se está creando actualmente hasta que se confirme la transacción inicial.

    Es decir, la sesión A crea la transacción para la creación de la base de datos y el disparo del disparador. El Trigger, utilizando SQLCLR, creará la Sesión B para modificar la Base de datos que se ha creado, pero la Transacción aún no se ha confirmado ya que está en espera hasta que se completa la Sesión B, lo cual no puede porque está esperando que esa Transacción inicial completar. Este es un punto muerto, pero SQL Server no puede detectarlo como tal, ya que no sabe que la sesión B fue creada por algo dentro de la sesión A. Este comportamiento se puede ver al reemplazar la primera parte de la IFdeclaración en el ejemplo arriba en el # 1 con lo siguiente:

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

    El -t 15conmutador para SQLCMD establece el tiempo de espera del comando / consulta para que la prueba no espere para siempre con el tiempo de espera predeterminado. Pero, puede configurarlo para que dure más de 15 segundos y, en otra sesión, verifique sys.dm_exec_requestsque se esté ejecutando todo el encantador bloqueo ;-).

  3. Ponga en cola el evento en algún lugar que luego leerá de esa cola y ejecutará la ALTER DATABASEinstrucción apropiada . Esto permitirá que la CREATE DATABASEdeclaración se complete y su transacción se confirme, después de lo cual ALTER DATABASEse puede ejecutar una declaración. Service Broker podría usarse aquí. O, cree una tabla, haga que el Trigger se inserte en esa tabla, luego haga que un trabajo del Agente SQL Server llame a un Procedimiento almacenado que lea de esa tabla y ejecute la ALTER DATABASEinstrucción y luego elimine el registro de la Tabla de cola.

SIN EMBARGO, las opciones anteriores se proporcionan principalmente para ayudar en escenarios en los que alguien realmente necesita hacer algún tipo de ALTER DATABASEdisparador DDL. En este escenario particular, si realmente no desea que ninguna base de datos esté utilizando la Clasificación predeterminada del nivel de instancia / sistema, entonces probablemente será mejor para usted:

  1. Crear una nueva instancia con la Clasificación deseada y mover todas sus Bases de datos de usuario a ella.
  2. O bien, si solo las bases de datos del sistema son de la clasificación no ideal, probablemente sea seguro cambiar la clasificación del sistema desde la línea de comandos a través de setup.exe (por ejemplo Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=...; esta opción recrea las bases de datos del sistema, por lo que necesitará para crear scripts de objetos a nivel de servidor, etc. para recrearlos más tarde, además de volver a aplicar parches, etc., FUN, FUN, FUN).
  3. O, para los aventureros de corazón, existe la sqlservr.exe -qopción indocumentada (es decir, no admitida, usar bajo su propio riesgo, pero podría funcionar) que actualiza TODOS los DB y TODAS las columnas (consulte Cambiar la recopilación de la instancia, las bases de datos y todas las columnas en todas las bases de datos de usuario: ¿qué podría ir mal? para obtener una descripción detallada del comportamiento de esta opción, así como el alcance potencial del impacto).

    Independientemente de la opción elegida: siempre asegúrese de tener copias de seguridad de mastery msdbantes de intentar tales cosas.

La razón por la que valdría la pena cambiar la Clasificación predeterminada del nivel del servidor es que la Clasificación predeterminada de la instancia (es decir, el nivel del servidor) controla algunas áreas funcionales que podrían conducir a un comportamiento inesperado / inconsistente si todos esperan que las operaciones de cadena funcionen siguiendo las líneas de la Clasificación predeterminada para todas sus Bases de datos de usuario:

  1. Clasificación predeterminada para columnas de cadena en tablas temporales. Este es un problema solo cuando se compara / se une con otras columnas de cadena SI hay una discrepancia entre las dos columnas de cadena. El problema aquí es que cuando no se especifica la Clasificación explícitamente a través de la COLLATEpalabra clave, es mucho más probable (aunque no está garantizado) tener problemas.

    Esto no es un problema para el tipo de datos XML, las variables de tabla o las bases de datos contenidas.

  2. Metadatos a nivel de instancia. Por ejemplo, el namecampo en sys.databasesutilizará la Clasificación predeterminada de nivel de instancia. Otras vistas del catálogo del sistema también se ven afectadas, pero no tengo la lista completa.

    Los metadatos a nivel de base de datos, como sys.objectsy sys.indexes, no se ven afectados.

  3. Resolución de nombre para:
    1. variables locales (es decir @variable)
    2. cursores
    3. GOTO etiquetas

Por ejemplo, si la Intercalación a nivel de instancia no distingue entre mayúsculas y minúsculas, mientras que la Intercalación a nivel de base de datos es binaria (es decir, terminada en _BINo _BIN2), la resolución de nombre de objeto a nivel de base de datos será binaria (p [TableA] <> [tableA]. Ej. ), Pero los nombres variables permitirán la insensibilidad a mayúsculas y minúsculas (por ejemplo @VariableA = @variableA)

Solomon Rutzky
fuente
11

Debería usar SQL dinámico y la función 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

Solo sustituye tu colación por mi falso .

Ahora cuando creo una base de datos ...

CREATE DATABASE DingDong

Recibo este mensaje (de la impresión):

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

Solo tenga en cuenta que si otras bases de datos (incluyendo tempdb) usan diferentes intercalaciones, puede tener problemas al comparar datos de cadena. Tendría que agregar cláusulas COLLATE a las comparaciones de cadenas donde la carcasa o los acentos importan, e incluso cuando no lo hacen, puede detectar errores. Pregunta relacionada donde me encontré con un problema de código similar aquí .

Erik Darling
fuente
1
@RafaelPiccinelli y Erik: solo para tu información, esta respuesta no es del todo correcta. El código no funciona, pero el error real se enmascara debido a que la prueba utiliza un nombre de clasificación no válido. Actualicé mi respuesta para explicar (hacia la parte superior) ya que era demasiado para un comentario.
Solomon Rutzky
2

No puedes ALTER DATABASEen un gatillo. Tendrá que ser creativo con la evaluación y la corrección. Algo como:

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

Aunque no deberías usar sp_MSforeachdb .

Henrico Bekker
fuente