Alterando o uso de GETDATE () em todo o banco de dados

27

Preciso migrar um banco de dados local do SQL Server 2017 para um banco de dados SQL do Azure e estou enfrentando alguns desafios, pois há algumas limitações a serem enfrentadas.

Em particular, como um banco de dados SQL do Azure funciona apenas no horário UTC (sem fuso horário) e precisamos do horário local, precisamos alterar o uso de GETDATE() qualquer lugar no banco de dados, o que provou ser mais trabalhoso do que eu esperava.

Criei uma função definida pelo usuário para obter a hora local que funciona corretamente para o meu fuso horário:

CREATE FUNCTION [dbo].[getlocaldate]()
RETURNS datetime
AS
BEGIN
    DECLARE @D datetimeoffset;
    SET @D = CONVERT(datetimeoffset, SYSDATETIMEOFFSET()) AT TIME ZONE 'Pacific SA Standard Time';
    RETURN(CONVERT(datetime,@D));
END

O problema com o qual estou tendo problemas é realmente mudar GETDATE()com essa função em todas as visualizações, procedimentos armazenados, colunas computadas, valores padrão, outras restrições etc.

Qual seria a melhor maneira de implementar essa mudança?

Estamos na pré-visualização pública de Instâncias gerenciadas . Ele ainda tem o mesmo problema e GETDATE(), portanto, não ajuda com esse problema. Mover para o Azure é um requisito. Esse banco de dados é usado (e será usado) sempre neste fuso horário.

Lamak
fonte

Respostas:

17
  1. Use a ferramenta SQL Server para exportar a definição de objetos de banco de dados para um arquivo SQL que deve incluir: tabelas, visualizações, gatilhos, SPs, funções e assim por diante

  2. Edite o arquivo SQL (faça um backup primeiro) usando qualquer editor de texto que permita localizar o texto "GETDATE()"e substituí-lo por"[dbo].[getlocaldate]()"

  3. Execute o arquivo SQL editado no SQL do Azure para criar seus objetos de banco de dados ...

  4. Execute a migração de dados.

Aqui você tem uma referência da documentação do azure: Gerando scripts para o SQL Azure

AMG
fonte
Embora, na prática, essa abordagem seja mais complicada do que parece, provavelmente é a resposta correta e melhor. Eu tive que fazer tarefas semelhantes a essa quantidade de vezes e tentei todas as abordagens disponíveis e não encontrei nada melhor (ou até mesmo próximo). as outras abordagens parecem ótimas a princípio, mas rapidamente se tornam um pântano de pesadelos e imprudências.
RBarryYoung
15

Qual seria a melhor maneira de implementar essa mudança?

Eu trabalharia o contrário. Converta todos os seus carimbos de data e hora no banco de dados para UTC e use apenas o UTC e siga o fluxo. Se você precisar de um carimbo de data e hora em um tz diferente, poderá criar uma coluna gerada usando AT TIME ZONE(como você fez acima) que renderiza o carimbo de data e hora na TZ especificada (para o aplicativo). Mas, seriamente, consideraria apenas ter o UTC retornado ao aplicativo e escrever essa lógica - a lógica de exibição - no aplicativo.

Evan Carroll
fonte
se fosse apenas uma coisa de banco de dados, posso considerar isso, mas essa alteração afeta muitos outros aplicativos e softwares que precisariam de refatoração séria. Então, infelizmente, não é uma escolha para mim
Lamak
5
Que garantia você terá de que nenhum dos "aplicativos e software" use getdate ()? ou seja, código sql incorporado nos aplicativos. Se você não pode garantir isso, refatorar o banco de dados para usar uma função diferente levará à inconsistência.
Mister Magoo
@MisterMagoo Depende das práticas da loja, francamente, acho que essa é uma preocupação muito pequena, e não vejo muito tempo para fazer a pergunta para solucionar o problema e depois corrigi-lo. Seria interessante se essa pergunta não fosse o Azure, porque eu poderia invadir e fornecer mais comentários. As coisas na nuvem são péssimas: elas não suportam, então você precisa mudar algo do seu lado. Eu preferiria seguir a rota fornecida na minha resposta e passar o tempo fazendo a coisa certa. Além disso, você não tem garantias de que tudo funcionará quando você mudar para o Azure, como sempre.
Evan Carroll
@EvanCarroll, desculpe, acabei de reler meu comentário e não expressei bem minha intenção! Eu pretendia apoiar sua resposta (votada) e enfatizar que sugestões de apenas alterar o uso de getdate () para getlocaldate () no banco de dados os deixariam abertos a inconsistências do lado do aplicativo e, além disso, é apenas um esparadrapo em um problema maior. 100% concorda com sua resposta, resolver o problema principal é a abordagem correta.
Mister Magoo
@MisterMagoo eu entendo a sua preocupação, mas, neste caso, posso garantir que os aplicativos e software interagem com o banco de dados somente através de procedimentos armazenados
Lamak
6

Em vez de exportar, editar manualmente e executar novamente, você pode tentar fazer o trabalho diretamente no banco de dados com algo como:

DECLARE C CURSOR FOR
        SELECT sm.definition, so.type
        FROM   sys.objects so
        JOIN   sys.all_sql_modules sm ON sm.object_id = so.object_id
        WHERE  so.type IN ('P', 'V')
        ORDER BY so.name
DECLARE @SQL NVARCHAR(MAX), @ojtype NVARCHAR(MAX)
OPEN C
FETCH NEXT FROM C INTO @SQL, @ojtype
WHILE @@FETCH_STATUS = 0 BEGIN
    IF @objtype = 'P' SET @SQL = REPLACE(@SQL, 'CREATE PROCEDURE', 'ALTER PROCEDURE') 
    IF @objtype = 'V' SET @SQL = REPLACE(@SQL, 'CREATE VIEW'     , 'ALTER VIEW'     ) 
    SET @SQL = REPLACE(@SQL, 'GETDATE()', '[dbo].[getlocaldate]()') 
    --PRINT @SQL
    EXEC (@SQL)
    FETCH NEXT FROM C INTO @SQL, @ojtype
END
CLOSE C
DEALLOCATE C

é claro estendendo-o para lidar com funções, gatilhos e assim por diante também.

Existem algumas ressalvas:

  • Você pode precisar ser um pouco mais inteligente e lidar com espaços em branco diferentes / extras entre CREATEe PROCEDURE/ VIEW/ <other>. Ao invés do REPLACEpara que você pode preferir deixar em vez disso o CREATEno lugar e executar um DROPprimeiro, mas esta riscos deixando sys.dependse amigos fora dos eixos, onde ALTERnão podem, também, se ALTERfalhar, você pelo menos tem o objeto existente ainda no lugar onde, com DROP+ CREATEvocê pode não.

  • Se você codificar algum cheiro "inteligente", como modificar seu próprio esquema com TSQL ad-hoc, será necessário garantir que a pesquisa e a substituição de CREATE-> ALTERnão interfiram nisso.

  • Você desejará testar a regressão do (s) aplicativo (s) inteiro (s) após a operação, independentemente de usar o cursor ou exportar + editar + executar métodos.

Eu usei esse método para fazer atualizações semelhantes em todo o esquema no passado. É um pouco complicado, e parece muito feio, mas às vezes é a maneira mais fácil / rápida.

Padrões e outras restrições também podem ser modificados da mesma forma, embora apenas possam ser eliminados e recriados em vez de alterados. Algo como:

DECLARE C CURSOR FOR
        SELECT AlterDefaultSQL = 'ALTER TABLE [' +st.name+ '] DROP CONSTRAINT [' + si.name + '];'
                               + CHAR(10)
                               + 'ALTER TABLE [' +st.name+ '] ADD CONSTRAINT [' + si.name + '] DEFAULT '+REPLACE(si.definition, 'GETDATE()', '[dbo].[getlocaldate]()')+' FOR '+sc.name+';'
        FROM   sys.tables st
        JOIN   sys.default_constraints si ON si.parent_object_id = st.object_id
        JOIN   sys.columns sc ON sc.default_object_id = si.object_id
DECLARE @SQL NVARCHAR(MAX)
OPEN C
FETCH NEXT FROM C INTO @SQL
WHILE @@FETCH_STATUS = 0 BEGIN
    --PRINT @SQL
    EXEC (@SQL)
    FETCH NEXT FROM C INTO @SQL
END
CLOSE C
DEALLOCATE C

Um pouco mais divertido com o qual você pode precisar lidar: se você estiver particionando com o tempo, essas partes também precisarão ser alteradas. Embora o particionamento no horário mais granularmente do que o dia seja raro, você pode ter problemas em que DATETIMEs são interpretados pela função de particionamento como sendo o dia anterior ou o próximo, dependendo do fuso horário, deixando suas partições desalinhadas com as consultas habituais.

David Spillett
fonte
sim, as ressalvas são o que torna isso difícil. Além disso, isso não leva em consideração os valores padrão da coluna. De qualquer forma, obrigado #
274 Lamak
Os valores padrão da coluna e outras restrições também podem ser verificados no sysesquema e modificados programaticamente.
David Spillett
Talvez substituir por, por exemplo, CREATE OR ALTER PROCEDUREajude a solucionar alguns problemas de geração de código; ainda pode haver problemas, pois a definição armazenada será lida CREATE PROCEDURE(três! espaços) e isso não é correspondido por CREATE PROCEDUREnem CREATE OR ALTER PROCEDURE… ._.
TheConstructor
@ TheConstructor - era o que eu estava me referindo ao espaço em branco extra do wrt. Você pode contornar isso escrevendo uma função que procura a primeira CREATEque não está em um comentário e a substitui. Eu não tenho isso / semelhante no passado, mas não tenho o código da função à mão agora para postar. Ou se você pode garantir que nenhuma das suas definições de objeto possua comentários anteriores CREATE, ignore o problema dos comentários e localize e substitua a primeira instância de CREATE.
David Spillett
Eu tentei essa abordagem várias vezes no passado e, no geral, a abordagem Generate-Scripts era melhor e é quase sempre o que uso hoje, a menos que o número de objetos a serem alterados seja relativamente pequeno.
RBarryYoung
5

Eu realmente gosto da resposta de David e votei nisso por uma maneira programática de fazer as coisas.

Mas você pode tentar isso hoje para uma execução de teste no Azure via SSMS:

Clique com o botão direito do mouse em seu banco de dados -> Tarefas -> Gerar scripts.

[História anterior], tivemos um DBA júnior que atualizou todos os nossos ambientes de teste para o SQL 2008 R2 enquanto nossos ambientes de produção estavam no SQL 2008. É uma mudança que me faz estremecer até hoje. Para migrar para a produção, a partir do teste, tivemos que gerar scripts dentro do SQL, usando gerar scripts, e nas opções avançadas usamos a opção 'Tipo de dados para script: esquema e dados' para gerar um arquivo de texto massivo. Conseguimos mover com êxito nossos bancos de dados R2 de teste para nossos servidores herdados do SQL 2008 - onde uma restauração de banco de dados para uma versão inferior não teria funcionado. Usamos o sqlcmd para inserir o arquivo grande - pois os arquivos geralmente eram grandes demais para o buffer de texto do SSMS.

O que estou dizendo aqui é que essa opção provavelmente funcionaria para você também. Você só precisará executar uma etapa adicional e pesquisar e substituir getdate () por [dbo] .getlocaldate no arquivo de texto gerado. (Eu colocaria sua função no banco de dados antes da migração).

(Eu nunca quis ser proficiente nesse curativo de restauração de banco de dados, mas por um tempo tornou-se uma maneira defacto de fazer as coisas. E funcionou sempre.)

Se você escolher esta rota, certifique-se de selecionar o botão Avançado e todas as opções necessárias (leia cada uma) para passar do banco de dados antigo para o novo - como os padrões mencionados. Mas faça alguns testes no Azure. Aposto que você descobrirá que essa é uma solução que funciona - com um mínimo de esforço.

insira a descrição da imagem aqui

Picada
fonte
1

Altere dinamicamente todos os proc e udf para alterar o valor

    DECLARE @Text   NVARCHAR(max), 
        @spname NVARCHAR(max), 
        @Type   CHAR(5), 
        @Sql    NVARCHAR(max) 
DECLARE @getobject CURSOR 

SET @getobject = CURSOR 
FOR SELECT sc.text, 
           so.NAME, 
           so.type 
    FROM   sys.syscomments sc 
           INNER JOIN sysobjects so 
                   ON sc.id = so.id 
    WHERE  sc.[text] LIKE '%getdate()%' 

--and type in('P','FN') 
OPEN @getobject 

FETCH next FROM @getobject INTO @Text, @spname, @Type 

WHILE @@FETCH_STATUS = 0 
  BEGIN 
      IF ( @Type = 'P' 
            OR @Type = 'FN' ) 
        SET @Text = Replace(@Text, 'getdate', 'dbo.getlocaldate') 

      SET @Text = Replace(@Text, 'create', 'alter') 

      EXECUTE Sp_executesql 
        @Text 

      PRINT @Text 

      --,@spname,@Type 
      FETCH next FROM @getobject INTO @Text, @spname, @Type 
  END 

CLOSE @getobject 

DEALLOCATE @getobject  

 

    CREATE PROCEDURE [dbo].[Testproc1] 
AS 
    SET nocount ON; 

  BEGIN 
      DECLARE @CurDate DATETIME = Getdate() 
  END

Observe que os sysobjects comentados digitam a condição da coluna. Meu script alterará apenas proc e UDF.

Este script irá alterar tudo o Default Constraintque contémGetDate()

    DECLARE @TableName      VARCHAR(300), 
        @constraintName VARCHAR(300), 
        @colName        VARCHAR(300), 
        @Sql            NVARCHAR(max) 
DECLARE @getobject CURSOR 

SET @getobject = CURSOR 
FOR SELECT ds.NAME, 
           sc.NAME AS colName, 
           so.NAME AS Tablename 
    --,ds.definition 
    FROM   sys.default_constraints ds 
           INNER JOIN sys.columns sc 
                   ON ds.object_id = sc.default_object_id 
           INNER JOIN sys.objects so 
                   ON so.object_id = ds.parent_object_id 
    WHERE  definition LIKE '%getdate()%' 

OPEN @getobject 

FETCH next FROM @getobject INTO @constraintName, @colName, @TableName 

WHILE @@FETCH_STATUS = 0 
  BEGIN 
      SET @Sql = 'ALTER TABLE ' + @TableName 
                 + ' DROP CONSTRAINT ' + @constraintName + '; ' 
                 + Char(13) + Char(10) + '           ' + Char(13) + Char(10) + '' 
      SET @Sql = @Sql + ' ALTER TABLE ' + @TableName 
                 + ' ADD CONSTRAINT ' + @constraintName 
                 + '          DEFAULT dbo.GetLocaledate() FOR ' 
                 + @colName + ';' + Char(13) + Char(10) + '          ' + Char(13) 
                 + Char(10) + '' 

      PRINT @Sql 

      EXECUTE sys.Sp_executesql 
        @Sql 

      --,@spname,@Type 
      FETCH next FROM @getobject INTO @constraintName, @colName, @TableName 
  END 

CLOSE @getobject 

DEALLOCATE @getobject   
KumarHarsh
fonte
1

Promovi a resposta de Evan Carrolls, pois acho que essa é a melhor solução. Não consegui convencer meus colegas de que eles deveriam alterar muito código C #; portanto, tive que usar o código que David Spillett escreveu. Corrigi alguns problemas com UDFs, SQL dinâmico e esquemas (nem todos os códigos usam "dbo.") Como este:

DECLARE C CURSOR LOCAL STATIC FOR
        SELECT sm.definition, so.type
        FROM   sys.objects so
        JOIN   sys.all_sql_modules sm ON sm.object_id = so.object_id
        WHERE  so.type IN ('P', 'V')
        AND CHARINDEX('getdate()', sm.definition) > 0
        ORDER BY so.name

DECLARE @SQL NVARCHAR(MAX), @objtype NVARCHAR(MAX)
OPEN C
WHILE 1=1 BEGIN
    FETCH NEXT FROM C INTO @SQL, @objtype
    IF @@FETCH_STATUS <> 0 BREAK

    IF @objtype = 'P' SET @SQL = REPLACE(@SQL, 'CREATE PROCEDURE', 'ALTER PROCEDURE') 
    IF @objtype = 'P' SET @SQL = REPLACE(@SQL, 'CREATE   PROCEDURE', 'ALTER PROCEDURE') /* when you write "create or alter proc" */
    IF @objtype = 'V' SET @SQL = REPLACE(@SQL, 'CREATE VIEW'     , 'ALTER VIEW'     ) 
    IF CHARINDEX('getdate())''', @sql) > 0 BEGIN  /* when dynamic SQL is used */
        IF CHARINDEX('utl.getdate())''', @sql) = 0 SET @SQL = REPLACE(@SQL, 'GETDATE()', 'utl.getdate()') 
    end
    ELSE begin
        SET @SQL = REPLACE(@SQL, 'GETDATE()', 'CONVERT(DATETIME, CONVERT(datetimeoffset,  SYSDATETIME()) AT TIME ZONE ''Central Europe Standard Time'')') 
    end
    EXEC dbo.LongPrint @String = @sql    
    EXEC (@SQL)
END
CLOSE C
DEALLOCATE C

e as restrições padrão como esta:

DECLARE C CURSOR LOCAL STATIC FOR
        SELECT AlterDefaultSQL = 'ALTER TABLE [' +sch.name+ '].[' +st.name+ '] DROP CONSTRAINT [' + si.name + '];'
                               + CHAR(10)
                               + 'ALTER TABLE [' +sch.name+ '].[' +st.name+ '] ADD CONSTRAINT [' + si.name + '] DEFAULT '+REPLACE(si.definition, 'GETDATE()', 'CONVERT(DATETIME, CONVERT(datetimeoffset,  SYSDATETIME()) AT TIME ZONE ''Central Europe Standard Time'')')+' FOR '+sc.name+';'
        FROM   sys.tables st
        JOIN   sys.default_constraints si ON si.parent_object_id = st.object_id
        JOIN   sys.columns sc ON sc.default_object_id = si.object_id
        INNER JOIN sys.schemas sch ON sch.schema_id = st.schema_id
        WHERE CHARINDEX('getdate()', si.definition) > 0
        ORDER BY st.name, sc.name

DECLARE @SQL NVARCHAR(MAX)
OPEN C
WHILE 1=1 BEGIN
    FETCH NEXT FROM C INTO @SQL
    IF @@FETCH_STATUS <> 0 BREAK

    EXEC dbo.LongPrint @String = @sql  
    EXEC (@SQL)
    FETCH NEXT FROM C INTO @SQL
END
CLOSE C
DEALLOCATE C


UDFs
A sugestão de usar um UDF que retorne a data e a hora de hoje parece bom, mas acho que ainda existem problemas de desempenho suficientes com os UDFs, por isso escolhi usar a solução AT TIME ZONE muito longa e feia.

Henrik Staun Poulsen
fonte