Disparador para alterar o agrupamento do banco de dados na criação

9

Estou tentando criar um gatilho, para alterar o agrupamento de um banco de dados em sua criação, mas como posso pegar o nome do banco de dados para usar dentro do gatilho?

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, isso não está funcionando.

Racer SQL
fonte
11
Existe uma razão para você não poder simplesmente alterar o banco de dados MODEL para o agrupamento necessário? - todos os bancos de dados recém-criados usaria modelo como um modelo
Scott Hodgin
Eu tentei isso, mas ele diz que o banco de dados modelo é um banco de dados do sistema, então não posso alterá-lo.
Racer SQL
Portanto, os bancos de dados do sistema estarão em um agrupamento diferente dos bancos de dados do usuário? Você já considerou possíveis problemas de agrupamento em tabelas temporárias etc.?
22817 George.Palacios
Uau, sim, eu li isso 5 minutos atrás. Não pensei nisso. Isso não é uma boa idéia.
Racer SQL

Respostas:

8

De maneira geral, você não pode emitir ALTER DATABASEum acionador (ou qualquer transação que contenha outras instruções). Se você tentar, você receberá o seguinte erro:

Msg 226, Nível 16, Estado 6, Linha xxxx Instrução
ALTER DATABASE não permitida na transação de várias instruções.

A razão pela qual esse erro não foi encontrado na resposta do @ sp_BlitzErik é resultado do caso de teste específico fornecido: o erro mostrado acima é um erro em tempo de execução, enquanto o erro encontrado em sua resposta é um erro em tempo de compilação. Esse erro em tempo de compilação impede a execução do comando e, portanto, não há "tempo de execução". Podemos ver a diferença executando o seguinte:

SET NOEXEC ON;

SELECT N'g' COLLATE Latin1;

SET NOEXEC OFF;

O lote acima apresentará um erro, enquanto o seguinte não:

SET NOEXEC ON;

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

SET NOEXEC OFF;

Isso deixa você com duas opções:

  1. Confirme a transação no gatilho DDL, para que não haja outras instruções na transação. Essa não é uma boa ideia se houver vários gatilhos DDL que podem ser disparados por uma CREATE DATABASEinstrução e possivelmente seja uma má ideia em geral, mas funciona ;-). O truque é que você também precisa iniciar uma nova transação no gatilho. O SQL Server observará que os valores inicial e final de @@TRANCOUNTnão coincidem e gerará um erro relacionado a isso. O código abaixo faz exatamente isso e também emite apenas ALTERse o agrupamento não for o desejado, caso contrário, ele ignora o 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

    Teste com:

    -- 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 estabelecer um regular / externo SqlConnection, Enlist = false;na String de Conexão, para emitir o ALTERcomando, pois isso não fará parte da Transação.

    Parece que o SQLCLR não é realmente uma opção, embora não seja devido a qualquer limitação específica do SQLCLR. De alguma forma, digitar " como isso não fará parte da transação " diretamente acima não destacou suficientemente o fato de que há, de fato, uma transação ativa em torno da CREATE DATABASEoperação. O problema aqui é que, embora o SQLCLR possa ser usado para sair da Transação atual, ainda não há como outra Sessão modificar o Banco de Dados que está sendo criado atualmente até que a Transação inicial seja confirmada.

    Significado, a Sessão A cria a transação para a criação do banco de dados e o acionamento do gatilho. O Disparador, usando SQLCLR, criará a Sessão B para modificar o Banco de Dados que foi criado, mas a Transação ainda não foi confirmada, pois permanece em espera até a Sessão B ser concluída, o que não é possível porque está aguardando a transação inicial completo. Isso é um impasse, mas não pode ser detectado como tal pelo SQL Server, pois não sabe que a Sessão B foi criada por algo dentro da Sessão A. Esse comportamento pode ser visto substituindo a primeira parte da IFinstrução no exemplo acima em # 1 com o seguinte:

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

    A -t 15opção para SQLCMD define o tempo limite do comando / consulta para que o teste não espere um tempo para sempre com o tempo limite padrão. Mas você pode configurá-lo para mais de 15 segundos e, em outra sessão, verifique sys.dm_exec_requestsse todos os adoráveis ​​bloqueios estão acontecendo ;-).

  3. Enfileire o evento em algum lugar que lerá a partir dessa fila e executará a ALTER DATABASEinstrução apropriada . Isso permitirá que a CREATE DATABASEinstrução seja concluída e sua transação seja confirmada, após a qual uma ALTER DATABASEinstrução pode ser executada. O Service Broker pode ser usado aqui. OU, crie uma tabela, faça com que o Trigger seja inserido nessa tabela e faça com que um trabalho do SQL Server Agent chame um Procedimento Armazenado que leia essa tabela e execute a ALTER DATABASEinstrução e remova o registro da fila Tabela.

NO ENTANTO, as opções acima são fornecidas principalmente para ajudar em cenários em que alguém realmente precisa fazer algum tipo ALTER DATABASEdentro de um gatilho DDL. Nesse cenário específico, se você realmente não deseja que nenhum banco de dados esteja usando o agrupamento padrão no nível do sistema / instância, provavelmente será melhor atendido por:

  1. Criando uma nova instância com o agrupamento desejado e movendo todos os bancos de dados do usuário para ele.
  2. Ou, se são apenas os bancos de dados do sistema que não são o agrupamento ideal, provavelmente é seguro alterar o agrupamento do sistema na linha de comando via setup.exe (por exemplo Setup.exe /Q /ACTION=Rebuilddatabase /INSTANCENAME=<instancename> /SQLCOLLATION=..., essa opção recria os bancos de dados do sistema, portanto, você precisará para criar scripts de objetos no nível do servidor, etc, para recriar mais tarde, além de reaplicar patches etc., FUN, FUN, FUN).
  3. Ou, para os aventureiros de coração, existe a opção não documentada (isto é, sem suporte, use por seu próprio risco, mas pode funcionar muito bem) sqlservr.exe -qque atualiza TODOS os DBs e TODAS as colunas (consulte Alteração o agrupamento da instância, os bancos de dados e todas as colunas em todos os bancos de dados do usuário: o que pode dar errado? para obter uma descrição detalhada do comportamento dessa opção e do escopo potencial do impacto).

    Independentemente da opção escolhida: sempre faça backups mastere msdbantes de tentar essas coisas.

A razão pela qual valeria a pena o esforço para alterar o agrupamento padrão no nível do servidor é que o agrupamento padrão da instância (ou seja, no nível do servidor) controla algumas áreas funcionais que podem levar a um comportamento inesperado / inconsistente, pois todos esperam que as operações de sequência funcionem ao longo das linhas do agrupamento padrão para todos os bancos de dados do usuário:

  1. Agrupamento padrão para colunas de sequência em tabelas temporárias. Esse é um problema apenas ao comparar / Unioning com outras colunas de string SE houver uma incompatibilidade entre as duas colunas de string. O problema aqui é que, ao não especificar o agrupamento explicitamente por meio da COLLATEpalavra-chave, é muito mais provável (embora não garantido) que ocorra problemas.

    Isso não é um problema para o tipo de dados XML, variáveis ​​de tabela ou bancos de dados contidos.

  2. Meta-dados no nível da instância. Por exemplo, o namecampo em sys.databasesusará o agrupamento padrão no nível da instância. Outras visualizações do catálogo do sistema também são afetadas, mas não tenho a lista completa.

    Os metadados no nível do banco de dados, como sys.objectse sys.indexes, não são afetados.

  3. Resolução de nomes para:
    1. variáveis ​​locais (ie @variable)
    2. cursores
    3. GOTO etiquetas

Por exemplo, se o agrupamento no nível da instância não diferencia maiúsculas de minúsculas, enquanto o agrupamento no nível do banco de dados é binário (ou seja, termina em _BINou _BIN2), a resolução de nomes de objetos no nível do banco de dados será binária (por exemplo [TableA] <> [tableA]), mas os nomes de variáveis ​​permitem a diferenciação de maiúsculas e minúsculas (por exemplo @VariableA = @variableA).

Solomon Rutzky
fonte
11

Você precisaria usar SQL dinâmico e a função 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

Apenas sub no seu agrupamento para o meu falso .

Agora, quando eu crio um banco de dados ...

CREATE DATABASE DingDong

Recebo esta mensagem (da impressão):

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

Observe que, se outros bancos de dados (incluindo tempdb) usarem agrupamentos diferentes, você poderá encontrar problemas ao comparar os dados da string. Você precisaria adicionar cláusulas COLLATE para comparações de cadeias de caracteres em que maiúsculas ou minúsculas são importantes e, mesmo que não sejam, você pode encontrar erros. Pergunta relacionada onde encontrei um problema de código semelhante aqui .

Erik Darling
fonte
11
@ RafaelPiccinelli e Erik: apenas para sua informação, esta resposta não está totalmente correta. O código não funciona, mas o erro real é mascarado devido ao teste usando um nome de agrupamento inválido. Atualizei minha resposta para explicar (no topo), pois era demais para um comentário.
Solomon Rutzky
2

Você não pode ALTER DATABASEem um gatilho. Você precisará ser criativo com a avaliação e a correção. 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';

Embora você não deva usar sp_MSforeachdb .

Henrico Bekker
fonte