Ändern der Verwendung von GETDATE () in der gesamten Datenbank

27

Ich muss eine lokale SQL Server 2017-Datenbank auf eine Azure SQL-Datenbank migrieren und stehe vor einigen Herausforderungen, da einige Einschränkungen zu beachten sind.

Insbesondere, da eine Azure SQL-Datenbank nur in UTC-Zeit (keine Zeitzonen) funktioniert und wir die Ortszeit benötigen, müssen wir die Verwendung von GETDATE() überall in der Datenbank ändern , was sich als arbeitsintensiver herausgestellt hat, als ich erwartet hatte.

Ich habe eine benutzerdefinierte Funktion erstellt, um die Ortszeit zu ermitteln, die für meine Zeitzone korrekt funktioniert:

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

Das Problem, mit dem ich Probleme habe, besteht darin, GETDATE()diese Funktion in jeder Ansicht, gespeicherten Prozedur, berechneten Spalten, Standardwerten, anderen Einschränkungen usw. zu ändern .

Was wäre der beste Weg, um diese Änderung umzusetzen?

Wir befinden uns in der öffentlichen Vorschau von verwalteten Instanzen . Es hat immer noch das gleiche Problem GETDATE(), daher hilft es bei diesem Problem nicht. Der Wechsel zu Azure ist eine Voraussetzung. Diese Datenbank wird immer in dieser Zeitzone verwendet (und wird auch verwendet).

Lamak
quelle

Antworten:

17
  1. Verwenden Sie das SQL Server-Tool, um die Datenbankobjektdefinition in eine SQL-Datei zu exportieren, die Folgendes enthalten sollte: Tabellen, Ansichten, Trigger, SPs, Funktionen usw.

  2. Bearbeiten Sie die SQL-Datei (erstellen Sie zuerst eine Sicherungskopie) mit einem beliebigen Texteditor, mit dem Sie den Text suchen "GETDATE()"und ersetzen können"[dbo].[getlocaldate]()"

  3. Führen Sie die bearbeitete SQL-Datei in Azure SQL aus, um Ihre Datenbankobjekte zu erstellen.

  4. Führen Sie die Datenmigration durch.

Hier finden Sie eine Referenz aus der Azure- Dokumentation: Generieren von Skripten für SQL Azure

AMG
quelle
In der Praxis ist dieser Ansatz zwar komplizierter als es sich anhört, aber wahrscheinlich die richtige und beste Antwort. Ich musste viele Male ähnliche Aufgaben erledigen und habe alle verfügbaren Ansätze ausprobiert und nichts Besseres gefunden (oder auch nur annähernd). Die anderen Ansätze scheinen auf den ersten Blick großartig, werden aber schnell zu einem albtraumhaften Sumpf aus Versehen und Fallstricken.
RBarryYoung
15

Was wäre der beste Weg, um diese Änderung umzusetzen?

Ich würde umgekehrt arbeiten. Konvertieren Sie alle Ihre Zeitstempel in der Datenbank in UTC, und verwenden Sie einfach UTC. Wenn Sie einen Zeitstempel in einem anderen tz benötigen, können Sie eine generierte Spalte erstellen, indem Sie AT TIME ZONE(wie oben beschrieben) den Zeitstempel in der angegebenen TZ (für die App) rendern. Aber ich würde ernsthaft in Betracht ziehen, nur UTC in die App zurückzubringen und diese Logik - die Anzeigelogik - in die App zu schreiben.

Evan Carroll
quelle
Wenn es sich nur um eine Datenbanksache handelte, denke ich vielleicht darüber nach, aber diese Änderung wirkt sich auf viele andere Apps und Software aus, die ernsthaft überarbeitet werden müssten. Also, leider ist es keine Wahl für mich
Lamak
5
Welche Garantie haben Sie, dass keine der "Apps und Software" getdate () verwendet? dh in die Apps eingebetteter SQL-Code. Wenn Sie dies nicht garantieren können, führt das Umgestalten der Datenbank mit einer anderen Funktion nur zu Inkonsistenzen.
Herr Magoo
@MisterMagoo Es hängt von den Praktiken im Geschäft ab, ganz ehrlich, ich denke, das ist ein sehr untergeordnetes Anliegen, und ich kann nicht sehen, dass es so lange dauert, die Frage zu stellen, um das Problem zu umgehen und es dann tatsächlich zu beheben. Es wäre interessant, wenn diese Frage nicht Azure wäre, da ich sie hacken und Ihnen mehr Feedback geben könnte. Cloud-Kram ist zum Kotzen: Sie unterstützen ihn nicht, also musst du etwas auf deiner Seite ändern. Ich würde es vorziehen, den in meiner Antwort angegebenen Weg zu gehen und die Zeit darauf zu verwenden, es richtig zu machen. Sie haben auch keine Garantie, dass beim Wechsel zu Azure alles funktioniert, wie immer bei tias.
Evan Carroll
@EvanCarroll, sorry, ich habe gerade meinen Kommentar noch einmal gelesen und meine Absicht nicht gut ausgedrückt! Ich wollte Ihre Antwort unterstützen (upvoted) und den Punkt ansprechen, dass Vorschläge, die Verwendung von getdate () zu getlocaldate () in der Datenbank zu ändern, sie für Inkonsistenzen seitens der App offen lassen würden, und darüber hinaus nur a ein größeres Problem mit Gips bekleben. 100% stimmen Ihrer Antwort zu, die Behebung des Kernproblems ist der richtige Ansatz.
Herr Magoo
@MisterMagoo Ich verstehe Ihr Anliegen, aber in diesem Fall kann ich garantieren, dass Apps und Software nur über gespeicherte Prozeduren mit der Datenbank interagieren
Lamak
6

Anstatt den Job zu exportieren, manuell zu bearbeiten und erneut auszuführen, können Sie den Job auch direkt in der Datenbank ausführen.

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

Natürlich auch um Funktionen, Trigger und so weiter zu erweitern.

Es gibt ein paar Einschränkungen:

  • Möglicherweise müssen Sie etwas heller sein und mit unterschiedlichen / zusätzlichen Leerzeichen zwischen CREATEund PROCEDURE/ VIEW/ umgehen <other>. Anstatt REPLACEdeswegen könnten Sie es vorziehen, stattdessen den CREATEPlatz zu belassen und einen DROPersten auszuführen , aber dies birgt die Gefahr, dass sys.dependsFreunde außer Kontrolle geraten, wo ALTERnicht, auch wenn dies ALTERfehlschlägt. Sie haben zumindest das vorhandene Objekt noch an Ort und Stelle, wo Sie mit DROP+ CREATEkönnen nicht.

  • Wenn Ihr Code nach dem Modifizieren seines eigenen Schemas mit Ad-hoc-TSQL riecht, müssen Sie sicherstellen, dass das Suchen und Ersetzen von CREATE-> ALTERdas nicht beeinträchtigt.

  • Sie möchten nach der Operation die gesamte (n) Anwendung (en) einer Regression unterziehen, unabhängig davon, ob Sie den Cursor verwenden oder die Methoden Exportieren + Bearbeiten + Ausführen.

Ich habe diese Methode in der Vergangenheit verwendet, um ähnliche schemaweite Aktualisierungen vorzunehmen. Es ist ein bisschen hässlich und fühlt sich ziemlich hässlich an, aber manchmal ist es der einfachste / schnellste Weg.

Standardeinstellungen und andere Einschränkungen können auf ähnliche Weise geändert werden. Diese können jedoch nur gelöscht und neu erstellt werden, anstatt sie zu ändern. So etwas wie:

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

Noch ein bisschen mehr Spaß, mit dem Sie möglicherweise zu kämpfen haben: Wenn Sie nach Zeit partitionieren, müssen diese Teile möglicherweise ebenfalls geändert werden. Wenngleich eine genauere Partitionierung der Zeit als am Tag selten vorkommt, können Probleme auftreten, bei denen DATETIMEdie Partitionierungsfunktion je nach Zeitrahmen den vorherigen oder den nächsten Tag angibt, sodass die Partitionen nicht mit den üblichen Abfragen abgeglichen werden.

David Spillett
quelle
Ja, die Vorbehalte sind es, die das schwer machen. Dabei werden auch die Standardwerte der Spalten nicht berücksichtigt.
Trotzdem
Spaltenstandardwerte und andere Einschränkungen können ebenfalls im sysSchema gesucht und programmgesteuert geändert werden.
David Spillett
Vielleicht CREATE OR ALTER PROCEDUREhilft das Ersetzen durch z. B. bei Problemen mit der Codegenerierung. Es kann jedoch Probleme geben, da die gespeicherte Definition CREATE PROCEDURE(drei! Leerzeichen) lautet und dies weder mit CREATE PROCEDUREnoch mit CREATE OR ALTER PROCEDURE... übereinstimmt.
TheConstructor
@TheConstructor - das ist es, worauf ich mich bezog, um "extra Whitespace" zu schreiben. Sie können dies umgehen, indem Sie eine Funktion schreiben, die nach der ersten Funktion sucht CREATE, die sich nicht in einem Kommentar befindet und diese ersetzt. Ich habe dies / ähnliches in der Vergangenheit nicht getan, aber ich habe den Funktionscode gerade nicht zum Posten zur Hand. Wenn Sie garantieren können, dass keiner Ihrer Objektdefinitionen Kommentare vorangestellt sind CREATE, ignorieren Sie das Kommentarproblem und suchen und ersetzen Sie einfach die erste Instanz von CREATE.
David Spillett
Ich habe diesen Ansatz in der Vergangenheit mehrfach selbst ausprobiert und insgesamt war der Generate-Scripts-Ansatz besser und wird heute fast immer verwendet, es sei denn, die Anzahl der zu ändernden Objekte ist relativ gering.
RBarryYoung
5

Ich mag Davids Antwort sehr und habe sie für eine programmatische Vorgehensweise empfohlen.

Sie können dies jedoch heute für einen Testlauf in Azure über SSMS versuchen:

Klicken Sie mit der rechten Maustaste auf Ihre Datenbank -> Aufgaben -> Skripte erstellen.

[Back Story] Wir hatten einen Junior-DBA, der alle unsere Testumgebungen auf SQL 2008 R2 aktualisiert hat, während sich unsere Produktionsumgebungen auf SQL 2008 befanden. Es ist eine Änderung, die mich bis heute erschreckt. Um vom Test in die Produktion zu migrieren, mussten wir Skripte in SQL mithilfe von Generierungsskripten generieren. In den erweiterten Optionen haben wir die Option "Zu skriptierender Datentyp: Schema und Daten" verwendet, um eine umfangreiche Textdatei zu generieren. Wir konnten unsere Test R2-Datenbanken erfolgreich auf unsere älteren SQL 2008-Server verlagern. Eine Datenbankwiederherstellung auf eine niedrigere Version hätte nicht funktioniert. Wir haben sqlcmd verwendet, um die große Datei einzugeben - da die Dateien oft zu groß für den SSMS-Textpuffer waren.

Was ich hier sage ist, dass diese Option wahrscheinlich auch für Sie funktionieren würde. Sie müssen nur einen weiteren Schritt ausführen und getdate () durch [dbo] .getlocaldate in der generierten Textdatei suchen und ersetzen. (Ich würde Ihre Funktion jedoch vor der Migration in die Datenbank stellen.)

(Ich wollte mich nie mit diesem Band-Aid einer Datenbankwiederherstellung auskennen, aber für eine Weile wurde es zu einer defakten Methode. Und es funktionierte jedes Mal.)

Wenn Sie sich für diese Route entscheiden, stellen Sie sicher, dass Sie auf die Schaltfläche Erweitert klicken und alle Optionen auswählen (jeweils lesen) , um von der alten Datenbank in die neue Datenbank zu wechseln - wie die von Ihnen erwähnten Standardeinstellungen. Probieren Sie es doch mal in Azure aus. Ich wette, Sie werden feststellen, dass dies eine Lösung ist, die funktioniert - mit ein wenig Aufwand.

Bildbeschreibung hier eingeben

Stachel
quelle
1

Ändern Sie dynamisch alle proc und udf, um den Wert zu ändern

    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

Beachten Sie die kommentierten sysobjects. Geben Sie die Spaltenbedingung ein. Mein Skript ändert nur proc und UDF.

Dieses Skript ändert alle Default ConstraintInhalteGetDate()

    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
quelle
1

Ich habe die Antwort von Evan Carrolls positiv bewertet, da ich denke, dass dies die beste Lösung ist. Ich war nicht in der Lage, meine Kollegen davon zu überzeugen, dass sie viel C # -Code ändern sollten, daher musste ich den Code verwenden, den David Spillett schrieb. Ich habe einige Probleme mit UDFs, Dynamic SQL und Schemas behoben (nicht jeder Code verwendet "dbo"):

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

und die Standardbedingungen wie folgt:

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
Der Vorschlag, eine UDF zu verwenden, die das heutige Datum und die aktuelle Uhrzeit zurückgibt, sieht gut aus, aber ich denke, es gibt immer noch genügend Leistungsprobleme mit UDFs, deshalb habe ich mich für die sehr lange und hässliche AT TIME ZONE-Lösung entschieden.

Henrik Staun Poulsen
quelle