Cambiar el uso de GETDATE () en toda la base de datos


27

Necesito migrar una base de datos local de SQL Server 2017 a una base de datos SQL de Azure, y me enfrento a algunos desafíos ya que hay muchas limitaciones por superar.

En particular, dado que una base de datos SQL de Azure funciona solo en hora UTC (sin zonas horarias) y necesitamos la hora local, tenemos que cambiar el uso de GETDATE() todas partes en la base de datos, lo que ha demostrado ser más trabajo de lo que esperaba.

Creé una función definida por el usuario para obtener la hora local que funciona correctamente para mi zona horaria:

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

El problema con el que tengo problemas es cambiar realmente GETDATE()con esta función en cada vista, procedimiento almacenado, columnas calculadas, valores predeterminados, otras restricciones, etc.

¿Cuál sería la mejor manera de implementar este cambio?

Estamos en la vista previa pública de instancias administradas . Todavía tiene el mismo problema GETDATE(), por lo que no ayuda con este problema. Mudarse a Azure es un requisito. Esta base de datos se usa (y se usará) siempre en esta zona horaria.

Respuestas:


17
  1. Use la herramienta SQL Server para exportar la definición de objetos de la base de datos a un archivo SQL que debe incluir: tablas, vistas, disparadores, SP, funciones, etc.

  2. Edite el archivo SQL (haga una copia de seguridad primero) usando cualquier editor de texto que le permita encontrar el texto "GETDATE()"y reemplazarlo"[dbo].[getlocaldate]()"

  3. Ejecute el archivo SQL editado en Azure SQL para crear sus objetos de base de datos ...

  4. Ejecute la migración de datos.

Aquí tiene una referencia de la documentación azul: Generación de scripts para SQL Azure


Aunque en la práctica este enfoque es más complicado de lo que parece, es probablemente la mejor y correcta respuesta. He tenido que hacer tareas similares a este número de veces y he probado todos los enfoques disponibles y no he encontrado nada mejor (o incluso cercano, en realidad). los otros enfoques parecen geniales al principio, pero rápidamente se convierten en un atolladero de pesadillas de descuidos y trampas.
RBarryYoung

15

¿Cuál sería la mejor manera de implementar este cambio?

Yo trabajaría al revés. Convierta todas sus marcas de tiempo en la base de datos a UTC, y simplemente use UTC y siga el flujo. Si necesita una marca de tiempo en una tz diferente, puede crear una columna generada utilizando AT TIME ZONE(como lo hizo anteriormente) que representa la marca de tiempo en esa TZ especificada (para la aplicación). Pero, consideraría seriamente que UTC volviera a la aplicación y escribiera esa lógica, la lógica de visualización, en la aplicación.


si se tratara solo de una base de datos, puedo considerar esto, pero ese cambio afecta a muchas otras aplicaciones y software que necesitarían una refactorización seria. Entonces, lamentablemente, no es una elección para mí
Lamak

55
¿Qué garantía tendrá de que ninguna de las "aplicaciones y software" usa getdate ()? es decir, código sql incrustado en las aplicaciones. Si no puede garantizar eso, refactorizar la base de datos para usar una función diferente solo generará inconsistencias.
Señor Magoo

@MisterMagoo Depende de las prácticas en la tienda, francamente, creo que esta es una preocupación menor, y no puedo ver que me tome tanto tiempo hacer la pregunta para solucionar el problema y luego solucionarlo. Sería interesante si esta pregunta no fuera Azure, porque podría hackearla y darle más comentarios. Sin embargo, las cosas de la nube apestan: no lo admiten, por lo que debes cambiar algo de tu lado. Preferiría seguir la ruta proporcionada en mi respuesta y dedicar el tiempo a hacerlo bien. Además, no tiene garantías de que algo funcionará cuando se mude a Azure, como siempre tias.
Evan Carroll

@EvanCarroll, lo siento, acabo de volver a leer mi comentario y no expresé bien mi intención. Quise apoyar su respuesta (upvoted) y plantear el punto de que las sugerencias de solo cambiar el uso de getdate () a getlocaldate () en la base de datos los dejarían abiertos a inconsistencias desde el lado de la aplicación, y además es solo un pegar yeso en un problema mayor. El 100% está de acuerdo con su respuesta, solucionar el problema central es el enfoque correcto.
Señor Magoo

@MisterMagoo Entiendo su preocupación, pero en este caso, puedo garantizar que las aplicaciones y el software interactúen con la base de datos solo a través de procedimientos almacenados
Lamak

6

En lugar de exportar, editar manualmente y volver a ejecutar, puede intentar hacer el trabajo directamente en la base de datos con 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

por supuesto, extenderlo para tratar funciones, disparadores, etc.

Hay algunas advertencias:

  • Es posible que deba ser un poco más brillante y lidiar con espacios en blanco diferentes / adicionales entre CREATEy PROCEDURE/ VIEW/ <other>. En lugar de REPLACEeso, es posible que prefiera dejar el CREATElugar en su lugar y ejecutar DROPprimero, pero esto corre el riesgo de dejar a sus sys.dependsamigos fuera de lugar donde ALTERno pueda, también si ALTERfalla, al menos tiene el objeto existente todavía en su lugar donde con DROP+ CREATEpuede no.

  • Si su código tiene algún olor "inteligente", como modificar su propio esquema con TSQL ad-hoc, entonces deberá asegurarse de que la búsqueda y el reemplazo de CREATE-> ALTERno interfieran con eso.

  • Querrá probar la regresión de la (s) aplicación (es) completa (s) después de la operación, ya sea que use el cursor o los métodos exportar + editar + ejecutar.

He usado este método para hacer actualizaciones similares en todo el esquema en el pasado. Es un truco y se siente bastante feo, pero a veces es la forma más fácil / rápida.

Los valores predeterminados y otras restricciones también se pueden modificar de manera similar, aunque solo se pueden eliminar y recrear en lugar de modificar. 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

Un poco más de diversión con la que puede que tenga que lidiar: si está haciendo particiones por tiempo, entonces esas partes también pueden necesitar modificaciones. Si bien la partición a tiempo de forma más granular que por día es poco frecuente, podría tener problemas en DATETIMElos que la función de partición interpreta los s como el día anterior o siguiente, dependiendo de la zona horaria, dejando sus particiones sin alinear con sus consultas habituales.


Sí, las advertencias son lo que hace que esto sea difícil. Además, esto no tiene en cuenta los valores predeterminados de la columna. Gracias de todos modos
Lamak

Los valores predeterminados de columna y otras restricciones también se pueden escanear en el sysesquema y modificar mediante programación.
David Spillett

Tal vez reemplazar con, por ejemplo, CREATE OR ALTER PROCEDUREayuda con algunos problemas de generación de código; aún puede haber problemas, ya que la definición almacenada se leerá CREATE PROCEDURE(¡tres! espacios) y esto no se corresponde CREATE PROCEDUREni con CREATE OR ALTER PROCEDURE... ._.
TheConstructor

@TheConstructor: a eso me refería con wrt "espacio en blanco adicional". Puede solucionar esto escribiendo una función que busque el primero CREATEque no esté dentro de un comentario y lo reemplace. No he hecho esto / similar en el pasado, pero no tengo el código de la función a mano en este momento para publicar. O si puede garantizar que ninguna de sus definiciones de objeto tiene comentarios anteriores CREATE, ignore el problema de los comentarios y solo busque y reemplace la primera instancia de CREATE.
David Spillett

He intentado este enfoque muchas veces en el pasado y, en general, el enfoque Generar scripts fue mejor y casi siempre es lo que uso hoy a menos que el número de objetos a cambiar resulte ser relativamente pequeño.
RBarryYoung

5

Realmente me gusta la respuesta de David y voté por una forma programática de hacer las cosas.

Pero puede probar esto hoy para una prueba de ejecución en Azure a través de SSMS:

Haga clic derecho en su base de datos -> Tareas -> Generar secuencias de comandos ..

[Historia de fondo] tuvimos un DBA junior que actualizó todos nuestros entornos de prueba a SQL 2008 R2 mientras que nuestros entornos de producción estaban en SQL 2008. Es un cambio que me hace temblar hasta el día de hoy. Para migrar a producción, desde la prueba, tuvimos que generar scripts dentro de SQL, usando generar scripts, y en las opciones avanzadas usamos la opción 'Tipo de datos a script: esquema y datos' para generar un archivo de texto masivo. Pudimos mover con éxito nuestras bases de datos R2 de prueba a nuestros servidores SQL 2008 heredados, donde una restauración de la base de datos a una versión anterior no hubiera funcionado. Utilizamos sqlcmd para ingresar el archivo grande, ya que los archivos a menudo eran demasiado grandes para el búfer de texto SSMS.

Lo que digo aquí es que esta opción probablemente también funcione para usted. Solo tendrá que hacer un paso adicional y buscar y reemplazar getdate () con [dbo] .getlocaldate en el archivo de texto generado. (Sin embargo, pondría su función en la base de datos antes de la migración).

(Nunca quise ser competente en este curita de una restauración de la base de datos, pero por un tiempo se convirtió en una forma de facto de hacer las cosas. Y funcionó todo el tiempo).

Si elige esta ruta, asegúrese de seleccionar el botón Avanzado y seleccione todas las opciones que necesita (lea cada una) para pasar de la base de datos anterior a la nueva, como los valores predeterminados que mencionó. Pero dale algunas pruebas en Azure. Apuesto a que encontrarás que esta es una solución que funciona, con un mínimo de esfuerzo.

ingrese la descripción de la imagen aquí


1

Modifique dinámicamente todos los procesos y udf para cambiar el 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 los sysobjects comentados . Condición de columna de tipo. Mi script alterará solo el proceso y UDF.

Este script alterará todo lo Default Constraintque contengaGetDate()

    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   

1

He votado a favor de la respuesta de Evan Carrolls, ya que creo que esta es la mejor solución. No he podido convencer a mis colegas de que deberían cambiar mucho código C #, así que tuve que usar el código que escribió David Spillett. He solucionado un par de problemas con UDF, SQL dinámico y esquemas (no todo el código usa "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

y las restricciones predeterminadas 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


UDF
La sugerencia de usar un UDF que devuelva la fecha y la hora de hoy se ve bien, pero creo que todavía hay suficientes problemas de rendimiento con los UDF, por lo que he optado por usar la solución AT ZONA HORA muy larga y fea.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.