J'ai besoin de migrer une base de données SQL Server 2017 sur site vers une base de données Azure SQL, et je suis confronté à certains défis car il y a un certain nombre de limitations à traverser.
En particulier, étant donné qu'une base de données Azure SQL ne fonctionne qu'en heure UTC (pas de fuseau horaire) et que nous avons besoin de l'heure locale, nous devons changer l'utilisation de GETDATE()
partout dans la base de données, qui s'est avéré être plus de travail que je ne l'avais prévu.
J'ai créé une fonction définie par l'utilisateur pour obtenir l'heure locale qui fonctionne correctement pour mon fuseau horaire:
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
Le problème qui me pose problème est de réellement modifier GETDATE()
avec cette fonction dans chaque vue, procédure stockée, colonnes calculées, valeurs par défaut, autres contraintes, etc.
Quelle serait la meilleure façon de mettre en œuvre ce changement?
Nous sommes dans l'aperçu public de Instances gérées . Il a toujours le même problème avec GETDATE()
, donc cela n'aide pas avec ce problème. Le passage à Azure est une exigence. Cette base de données est utilisée (et sera utilisée) toujours dans ce fuseau horaire.
Utilisez l'outil SQL Server pour exporter la définition des objets de base de données vers un fichier SQL qui doit inclure: tables, vues, déclencheurs, SP, fonctions, etc.
Modifiez le fichier SQL (faites d'abord une sauvegarde) à l'aide de n'importe quel éditeur de texte qui vous permet de trouver le texte "GETDATE()"
et de le remplacer par "[dbo].[getlocaldate]()"
Exécutez le fichier SQL modifié dans Azure SQL pour créer vos objets de base de données ...
Exécutez la migration des données.
Voici une référence de la documentation Azure: Génération de scripts pour SQL Azure
Quelle serait la meilleure façon de mettre en œuvre ce changement?
Je travaillerais dans l'autre sens. Convertissez tous vos horodatages de la base de données en UTC, et utilisez simplement UTC et suivez le flux. Si vous avez besoin d'un horodatage dans un autre tz, vous pouvez créer une colonne générée à l'aide de AT TIME ZONE
(comme vous l'avez fait ci-dessus) qui affiche l'horodatage dans la TZ spécifiée (pour l'application). Mais, j'envisagerais sérieusement de simplement retourner UTC dans l'application et d'écrire cette logique - la logique d'affichage - dans l'application.
Plutôt que d'exporter, de modifier manuellement et de réexécuter, vous pouvez essayer de faire le travail directement dans la base de données avec quelque chose comme:
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
bien sûr, l'étendre pour gérer les fonctions, les déclencheurs, etc.
Il y a quelques mises en garde:
Vous devrez peut-être être un peu plus lumineux et gérer les espaces blancs différents/supplémentaires entre CREATE
et PROCEDURE
/VIEW
/<other>
. Plutôt que le REPLACE
pour cela, vous préférerez peut-être laisser le CREATE
en place et exécuter d'abord un DROP
, mais cela risque de laisser sys.depends
et amis en panne où ALTER
ne peut pas, même si ALTER
échoue, vous avez au moins l'objet existant toujours en place où avec DROP
+ CREATE
vous ne pouvez pas.
Si votre code a des odeurs "intelligentes" comme la modification de son propre schéma avec TSQL ad hoc, vous devrez vous assurer que la recherche et le remplacement de CREATE
-> ALTER
n'interfèrent pas. avec ça.
Vous souhaiterez tester la régression de l'ensemble des applications après l'opération, que vous utilisiez le curseur ou les méthodes d'exportation + édition + exécution.
J'ai utilisé cette méthode pour effectuer des mises à jour similaires à l'échelle du schéma dans le passé. C'est un peu un hack et semble assez moche, mais parfois c'est le moyen le plus simple/le plus rapide.
Les valeurs par défaut et autres contraintes peuvent également être modifiées de la même manière, bien que celles-ci puissent uniquement être supprimées et recréées plutôt que modifiées. Quelque chose comme:
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 peu plus de plaisir auquel vous devrez peut-être faire face: si vous partitionnez en fonction du temps, ces parties peuvent également avoir besoin d'être modifiées. Bien que le partitionnement à l'heure de manière plus granulaire que le jour soit rare, vous pouvez rencontrer des problèmes où DATETIME
s sont interprétés par la fonction de partitionnement comme étant le jour précédent ou suivant selon le fuseau horaire, laissant vos partitions non alignées avec vos requêtes habituelles.
J'aime vraiment la réponse de David et j'ai voté pour une manière programmatique de faire les choses.
Mais vous pouvez essayer cela aujourd'hui pour une exécution de test dans Azure via SSMS:
Faites un clic droit sur votre base de données -> Tâches -> Générer des scripts ..
[Back Story] nous avions un DBA junior qui a mis à niveau tous nos environnements de test vers SQL 2008 R2 alors que nos environnements de production étaient à SQL 2008. C'est un changement qui me fait grincer des dents à ce jour. Pour migrer vers la production, à partir du test, nous avons dû générer des scripts dans SQL, en utilisant des scripts de génération, et dans les options avancées, nous avons utilisé l'option `` Type de données à script: schéma et données '' pour générer un fichier texte massif. Nous avons réussi à déplacer nos bases de données de test R2 vers nos serveurs SQL 2008 hérités - où une restauration de base de données vers une version inférieure n'aurait pas fonctionné. Nous avons utilisé sqlcmd pour entrer le gros fichier - car les fichiers étaient souvent trop gros pour le tampon de texte SSMS.
Ce que je dis ici, c'est que cette option fonctionnerait probablement aussi pour vous. Vous aurez juste besoin de faire une étape supplémentaire et de rechercher et remplacer getdate () par [dbo] .getlocaldate dans le fichier texte généré. (Je mettrais cependant votre fonction dans la base de données avant la migration).
(Je n'ai jamais voulu maîtriser ce pansement de restauration de base de données, mais pendant un certain temps, c'est devenu une façon de faire de facto. Et, cela a fonctionné à chaque fois.)
Si vous choisissez cette route, assurez-vous et sélectionnez le bouton Avancé et sélectionnez toutes les options dont vous avez besoin (lisez chacune) pour passer de l'ancienne base de données à la nouvelle base de données - comme les valeurs par défaut que vous avez mentionnées. Mais essayez-le quelques fois dans Azure. Je parie que vous constaterez que c'est une solution qui fonctionne - avec un minimum d'effort.
J'ai surévalué la réponse d'Evan Carrolls, car je pense que c'est la meilleure solution. Je n'ai pas réussi à convaincre mes collègues qu'ils devraient changer beaucoup de code C #, j'ai donc dû utiliser le code que David Spillett a écrit. J'ai résolu quelques problèmes avec les FDU, Dynamic SQL et les schémas (tous les codes n'utilisent pas "dbo") comme ceci:
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
et les contraintes par défaut comme ceci:
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
DF
La suggestion d'utiliser un UDF qui retourne la date et l'heure d'aujourd'hui semble agréable, mais je pense qu'il y a encore suffisamment de problèmes de performance avec les UDF, j'ai donc choisi d'utiliser le très long et laid AT Solution TIME ZONE.
Modifier dynamiquement tous les proc et udf pour changer la valeur
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
Remarque commentée colonne Type de sysobjects condition.Mon script ne modifiera que proc et UDF.
Ce script va modifier tous les Default Constraint
Qui contiennent GetDate()
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