Le mardi T-SQL de ce mois-ci est hébergé par Mike Fal (blog | twitter), et le sujet est Trick Shots, où nous sommes invités à parler à la communauté d'une solution que nous avons utilisée dans SQL Server qui nous a semblé, du moins, comme une sorte de "trick shot" - quelque chose de similaire à l'utilisation de coups de banque massés, "anglais" ou compliqués au billard ou au snooker. Après avoir travaillé avec SQL Server pendant une quinzaine d'années, j'ai eu l'occasion de proposer des astuces pour résoudre des problèmes assez intéressants, mais qui semblent assez réutilisables, s'adaptent facilement à de nombreuses situations et sont simples à mettre en œuvre, c'est quelque chose que j'appelle "schema switch-a-roo".
Supposons que vous ayez un scénario dans lequel vous avez une grande table de recherche qui doit être actualisée périodiquement. Cette table de recherche est nécessaire sur de nombreux serveurs et peut contenir des données qui sont renseignées à partir d'une source externe ou tierce, par ex. Données IP ou de domaine, ou peuvent représenter des données provenant de votre propre environnement.
Les premiers scénarios pour lesquels j'avais besoin d'une solution consistaient à rendre les métadonnées et les données dénormalisées disponibles pour les "caches de données" en lecture seule - en réalité, uniquement les instances SQL Server MSDE (et plus tard Express) installées sur divers serveurs Web, de sorte que les serveurs Web tiraient ces données mises en cache localement au lieu de déranger le système OLTP principal. Cela peut sembler redondant, mais le déchargement de l'activité de lecture loin du système OLTP principal et la possibilité de retirer complètement la connexion réseau de l'équation ont entraîné une véritable augmentation des performances globales et, plus particulièrement, pour les utilisateurs finaux. .
Ces serveurs n'avaient pas besoin de copies à la minute près des données ; en fait, beaucoup de tables de cache n'étaient mises à jour que quotidiennement. Mais comme les systèmes fonctionnaient 24 heures sur 24, 7 jours sur 7 et que certaines de ces mises à jour pouvaient prendre plusieurs minutes, elles empêchaient souvent les vrais clients de faire de vraies choses sur le système.
La ou les approches originales
Au tout début, le code était plutôt simpliste :nous supprimions les lignes qui avaient été supprimées de la source, mettions à jour toutes les lignes dont nous pouvions dire qu'elles avaient changé et insérions toutes les nouvelles lignes. Cela ressemblait à ceci (traitement des erreurs, etc. supprimé pour plus de brièveté) :
BEGIN TRANSACTION; DELETE dbo.Lookup WHERE [key] NOT IN (SELECT [key] FROM [source]); UPDATE d SET [col] = s.[col] FROM dbo.Lookup AS d INNER JOIN [source] AS s ON d.[key] = s.[key] -- AND [condition to detect change]; INSERT dbo.Lookup([cols]) SELECT [cols] FROM [source] WHERE [key] NOT IN (SELECT [key] FROM dbo.Lookup); COMMIT TRANSACTION;
Inutile de dire que cette transaction pouvait entraîner de réels problèmes de performances lorsque le système était utilisé. Il y avait sûrement d'autres façons de le faire, mais toutes les méthodes que nous avons essayées étaient tout aussi lentes et coûteuses. Comment lent et cher? "Laissez-moi compter les scans…"
Étant donné que ce MERGE est antérieur et que nous avions déjà abandonné les approches "externes" telles que DTS, nous avons déterminé, grâce à certains tests, qu'il serait plus efficace de simplement effacer la table et de la remplir à nouveau, plutôt que d'essayer de synchroniser avec la source. :
BEGIN TRANSACTION; TRUNCATE TABLE dbo.Lookup; INSERT dbo.Lookup([cols]) SELECT [cols] FROM [source]; COMMIT TRANSACTION;
Maintenant, comme je l'ai expliqué, cette requête de [source] pourrait prendre quelques minutes, surtout si tous les serveurs Web étaient mis à jour en parallèle (nous avons essayé d'échelonner là où nous le pouvions). Et si un client se trouvait sur le site et essayait d'exécuter une requête impliquant la table de recherche, il devait attendre la fin de cette transaction. Dans la plupart des cas, s'ils exécutent cette requête à minuit, peu importe qu'ils obtiennent la copie des données de recherche d'hier ou celle d'aujourd'hui ; donc, les faire attendre le rafraîchissement semblait idiot et a en fait conduit à un certain nombre d'appels d'assistance.
Alors même si c'était mieux, c'était certainement loin d'être parfait.
Ma solution initiale :sp_rename
Ma solution initiale, à l'époque où SQL Server 2000 était cool, était de créer une table "fantôme":
CREATE TABLE dbo.Lookup_Shadow([cols]);
De cette façon, je pouvais remplir la table fantôme sans interrompre du tout les utilisateurs, puis effectuer un changement de nom à trois voies - une opération rapide, uniquement sur les métadonnées - uniquement une fois le remplissage terminé. Quelque chose comme ça (encore une fois, grossièrement simplifié):
TRUNCATE TABLE dbo.Lookup_Shadow; INSERT dbo.Lookup_Shadow([cols]) SELECT [cols] FROM [source]; BEGIN TRANSACTION; EXEC sp_rename N'dbo.Lookup', N'dbo.Lookup_Fake'; EXEC sp_rename N'dbo.Lookup_Shadow', N'dbo.Lookup'; COMMIT TRANSACTION; -- if successful: EXEC sp_rename N'dbo.Lookup_Fake', N'dbo.Lookup_Shadow';
L'inconvénient de cette approche initiale était que sp_rename avait un message de sortie non supprimable vous avertissant des dangers de renommer des objets. Dans notre cas, nous avons effectué cette tâche via les travaux de l'Agent SQL Server, et nous avons géré de nombreuses métadonnées et autres tables de cache, de sorte que l'historique des travaux a été inondé de tous ces messages inutiles et a en fait provoqué la troncation des erreurs réelles à partir des détails de l'historique. (Je me suis plaint à ce sujet en 2007, mais ma suggestion a finalement été rejetée et classée comme "Ne résoudra pas".)
Une meilleure solution :les schémas
Une fois que nous avons mis à niveau vers SQL Server 2005, j'ai découvert cette commande fantastique appelée CREATE SCHEMA. Il était trivial d'implémenter le même type de solution en utilisant des schémas au lieu de renommer des tables, et maintenant l'historique de l'Agent ne serait plus pollué par tous ces messages inutiles. En gros, j'ai créé deux nouveaux schémas :
CREATE SCHEMA fake AUTHORIZATION dbo; CREATE SCHEMA shadow AUTHORIZATION dbo;
Ensuite, j'ai déplacé la table Lookup_Shadow dans le schéma de cache et je l'ai renommée :
ALTER SCHEMA shadow TRANSFER dbo.Lookup_Shadow; EXEC sp_rename N'shadow.Lookup_Shadow', N'Lookup';
(Si vous ne faites que mettre en œuvre cette solution, vous créerez une nouvelle copie de la table dans le schéma, sans y déplacer la table existante et la renommer.)
Avec ces deux schémas en place et une copie de la table de recherche dans le schéma fantôme, mon changement de nom à trois voies est devenu un transfert de schéma à trois voies :
TRUNCATE TABLE shadow.Lookup; INSERT shadow.Lookup([cols]) SELECT [cols] FROM [source]; -- perhaps an explicit statistics update here BEGIN TRANSACTION; ALTER SCHEMA fake TRANSFER dbo.Lookup; ALTER SCHEMA dbo TRANSFER shadow.Lookup; COMMIT TRANSACTION; ALTER SCHEMA shadow TRANSFER fake.Lookup;
À ce stade, vous pouvez bien sûr vider le cliché instantané de la table, mais dans certains cas, j'ai trouvé utile de laisser l'"ancienne" copie des données à des fins de dépannage :
TRUNCATE TABLE shadow.Lookup;
Tout ce que vous ferez ensuite avec le cliché instantané, vous voudrez vous assurer de le faire en dehors de la transaction - les deux opérations de transfert doivent être aussi concises et rapides que possible.
Quelques mises en garde
- Clés étrangères
Cela ne fonctionnera pas immédiatement si la table de recherche est référencée par des clés étrangères. Dans notre cas, nous n'avons signalé aucune contrainte sur ces tables de cache, mais si vous le faites, vous devrez peut-être vous en tenir à des méthodes intrusives telles que MERGE. Ou utilisez des méthodes d'ajout uniquement et désactivez ou supprimez les clés étrangères avant d'effectuer des modifications de données (puis recréez-les ou réactivez-les par la suite). Si vous vous en tenez aux techniques MERGE / UPSERT et que vous le faites entre serveurs ou, pire encore, à partir d'un système distant, je vous recommande fortement d'obtenir les données brutes localement plutôt que d'essayer d'utiliser ces méthodes entre serveurs.
- Statistiques
Changer les tables (en utilisant le renommage ou le transfert de schéma) entraînera un basculement des statistiques entre les deux copies de la table, et cela peut évidemment être un problème pour les plans. Vous pouvez donc envisager d'ajouter des mises à jour de statistiques explicites dans le cadre de ce processus.
- Autres approches
Il existe bien sûr d'autres façons de faire cela que je n'ai tout simplement pas eu l'occasion d'essayer. La commutation de partition et l'utilisation d'une vue + synonyme sont deux approches que je pourrais étudier à l'avenir pour un traitement plus approfondi du sujet. Je serais intéressé d'entendre vos expériences et comment vous avez résolu ce problème dans votre environnement. Et oui, je me rends compte que ce problème est en grande partie résolu par les groupes de disponibilité et les secondaires lisibles dans SQL Server 2012, mais je considère que c'est un "coup de pouce" si vous pouvez résoudre le problème sans jeter de licences haut de gamme sur le problème, ou répliquer un toute la base de données pour rendre quelques tables redondantes. :-)
Conclusion
Si vous pouvez vivre avec les limites ici, cette approche pourrait bien être plus performante qu'un scénario dans lequel vous mettez essentiellement une table hors ligne à l'aide de SSIS ou de votre propre routine MERGE/UPSERT, mais assurez-vous de tester les deux techniques. Le point le plus important est que l'utilisateur final accédant à la table devrait avoir exactement la même expérience, à tout moment de la journée, même s'il accède à la table au milieu de votre mise à jour périodique.