Une exigence courante dans ETL et divers scénarios de création de rapports consiste à charger discrètement une table de transfert SQL Server en arrière-plan, afin que les utilisateurs qui interrogent les données ne soient pas affectés par les écritures et vice-versa. L'astuce consiste à indiquer comment et quand diriger les utilisateurs vers la nouvelle version actualisée des données.
Exemple simplifié d'un tableau d'échelonnement :analogie avec le marché d'un agriculteur
Alors, qu'est-ce qu'une table intermédiaire en SQL ? Une table de mise en scène peut être plus facilement comprise à l'aide d'un exemple concret :imaginons que vous ayez une table pleine de légumes que vous vendez au marché fermier local. Au fur et à mesure que vos légumes se vendent et que vous apportez de nouveaux stocks :
- Lorsque vous apportez une charge de nouveaux légumes, il vous faudra 20 minutes pour débarrasser la table et remplacer le stock restant par le nouveau produit.
- Vous ne voulez pas que les clients restent assis et attendent 20 minutes pour que le changement se produise, car la plupart achèteront leurs légumes ailleurs.
Maintenant, que se passerait-il si vous aviez une deuxième table vide où vous chargez les nouveaux légumes, et pendant que vous faites cela, les clients peuvent toujours acheter les légumes plus anciens de la première table ? (Supposons que ce n'est pas parce que les légumes plus anciens se sont détériorés ou sont autrement moins désirables.)
Actualisation des tables dans SQL Server
Il existe plusieurs méthodes pour recharger des tables entières pendant qu'elles sont activement interrogées ; il y a deux décennies, j'ai profité sans retenue de sp_rename
- Je jouerais à un jeu shell avec un cliché instantané vide de la table, rechargeant joyeusement le cliché instantané, puis n'effectuant le renommage qu'à l'intérieur d'une transaction.
Dans SQL Server 2005, j'ai commencé à utiliser des schémas pour contenir des clichés instantanés de tables que j'ai simplement transférés en utilisant la même technique de jeu shell, dont j'ai parlé dans ces deux articles :
- Trick Shots :Schema Switch-a-Roo
- Schéma Switch-a-Roo, partie 2
Le seul avantage de transférer des objets entre schémas plutôt que de les renommer est qu'il n'y a pas de messages d'avertissement concernant le changement de nom d'un objet - ce qui n'est même pas un problème en soi, sauf que les messages d'avertissement remplissent les journaux d'historique de l'agent beaucoup plus rapidement.
Les deux approches nécessitent toujours un verrou de modification de schéma (Sch-M), elles doivent donc attendre que toutes les transactions existantes libèrent leurs propres verrous. Une fois qu'ils ont acquis leur verrou Sch-M, ils bloquent toutes les requêtes ultérieures nécessitant des verrous de stabilité de schéma (Sch-S)… ce qui est presque toutes les requêtes. Cela peut rapidement devenir un cauchemar de chaîne de blocage, car toute nouvelle requête nécessitant Sch-S doit être placée dans une file d'attente derrière le Sch-M. (Et non, vous ne pouvez pas contourner cela en utilisant RCSI ou NOLOCK
partout, puisque même ces requêtes nécessitent toujours Sch-S. Vous ne pouvez pas acquérir Sch-S avec un Sch-M en place, car ils sont incompatibles (Michael J. Swart en parle ici.)
Kendra Little m'a vraiment ouvert les yeux sur les dangers du transfert de schéma dans son article "Staging Data :Locking Danger with ALTER SCHEMA TRANSFER". Elle y montre pourquoi le transfert de schéma peut être pire que le renommage. Plus tard, elle a détaillé une troisième façon beaucoup moins percutante d'échanger des tables, que j'utilise maintenant exclusivement :la commutation de partition. Cette méthode permet au commutateur d'attendre à une priorité inférieure, ce qui n'est même pas une option avec les techniques de renommage ou de transfert de schéma. Joe Sack est entré dans les détails de cette amélioration ajoutée dans SQL Server 2014 :"Exploration des options d'attente de verrouillage à faible priorité dans SQL Server 2014 CTP1."
Exemple de changement de partition SQL Server
Regardons un exemple de base, en suivant l'essentiel de Kendra ici. Tout d'abord, nous allons créer deux nouvelles bases de données :
CREATE DATABASE NewWay; CREATE DATABASE OldWay; GO
Dans la nouvelle base de données, nous allons créer une table pour contenir notre inventaire de légumes, et deux copies de la table pour notre jeu de coquillage :
USE NewWay; GO CREATE TABLE dbo.Vegetables_NewWay ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO -- we need to create two extra copies of the table. CREATE TABLE dbo.Vegetables_NewWay_prev ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO CREATE TABLE dbo.Vegetables_NewWay_hold ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO
Nous créons une procédure qui charge la copie intermédiaire de la table, puis utilise une transaction pour désactiver la copie actuelle.
CREATE PROCEDURE dbo.DoTheVeggieSwap_NewWay AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Vegetables_NewWay_prev; INSERT dbo.Vegetables_NewWay_prev SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, LEFT(m.definition, 500) FROM sys.dm_exec_sessions AS s CROSS JOIN model.sys.all_objects AS o INNER JOIN model.sys.all_sql_modules AS m ON o.[object_id] = m.[object_id]; -- need to take Sch-M locks here: BEGIN TRANSACTION; ALTER TABLE dbo.Vegetables_NewWay SWITCH TO dbo.Vegetables_NewWay_hold WITH (WAIT_AT_LOW_PRIORITY (MAX_DURATION = 1 MINUTES, ABORT_AFTER_WAIT = BLOCKERS)); ALTER TABLE dbo.Vegetables_NewWay_prev SWITCH TO dbo.Vegetables_NewWay; COMMIT TRANSACTION; -- and now users will query the new data in dbo -- can switch the old copy back and truncate it -- without interfering with other queries ALTER TABLE dbo.Vegetables_NewWay_hold SWITCH TO dbo.Vegetables_NewWay_prev; TRUNCATE TABLE dbo.Vegetables_NewWay_prev; END GO
La beauté de WAIT_AT_LOW_PRIORITY
est que vous pouvez contrôler complètement le comportement avec le ABORT_AFTER_WAIT
choix :
ABORT_AFTER_WAIT paramètre | Description / symptômes |
---|---|
SOI | Cela signifie que le commutateur va abandonner après n minutes. Pour la session tentant d'effectuer le changement, cela apparaîtra comme le message d'erreur : Délai d'expiration de la demande de verrouillage dépassé. |
BLOQUEURS | Cela indique que le commutateur attendra jusqu'à n minutes, puis se force en tête de file en tuant tous les bloqueurs devant lui . Les sessions essayant d'interagir avec la table qui sont bloquées par l'opération de basculement verront une combinaison de ces messages d'erreur : Votre session a été déconnectée en raison d'une opération DDL hautement prioritaire.Impossible de poursuivre l'exécution car la session est à l'état kill. Une erreur grave s'est produite sur la commande actuelle. Les résultats, le cas échéant, doivent être ignorés. |
AUCUN | Cela indique que le commutateur attendra avec plaisir jusqu'à ce qu'il obtienne son tour, indépendamment de MAX_DURATION .
C'est le même comportement que vous obtiendriez avec le changement de nom, le transfert de schéma ou le changement de partition sans |
Les BLOCKERS
L'option n'est pas la façon la plus conviviale de gérer les choses, puisque vous dites déjà que c'est bien grâce à cette opération de mise en scène/commutation pour que les utilisateurs voient des données un peu obsolètes. Je préférerais probablement utiliser SELF
et faire réessayer l'opération dans les cas où elle n'a pas pu obtenir les verrous requis dans le temps imparti. Cependant, je garderais une trace de la fréquence des échecs, en particulier des échecs consécutifs, car vous voulez vous assurer que les données ne deviennent jamais trop obsolètes.
Comparé à l'ancienne façon de basculer entre les schémas
Voici comment j'aurais géré le changement auparavant :
USE OldWay; GO -- create two schemas and two copies of the table CREATE SCHEMA prev AUTHORIZATION dbo; GO CREATE SCHEMA hold AUTHORIZATION dbo; GO CREATE TABLE dbo.Vegetables_OldWay ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO CREATE TABLE prev.Vegetables_OldWay ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO CREATE PROCEDURE dbo.DoTheVeggieSwap_OldWay AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE prev.Vegetables_OldWay; INSERT prev.Vegetables_OldWay SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, LEFT(m.definition, 500) FROM sys.dm_exec_sessions AS s CROSS JOIN model.sys.all_objects AS o INNER JOIN model.sys.all_sql_modules AS m ON o.[object_id] = m.[object_id]; -- need to take Sch-M locks here: BEGIN TRANSACTION; ALTER SCHEMA hold TRANSFER dbo.Vegetables_OldWay; ALTER SCHEMA dbo TRANSFER prev.Vegetables_OldWay; COMMIT TRANSACTION; -- and now users will query the new data in dbo -- can transfer the old copy back and truncate it without -- interfering with other queries: ALTER SCHEMA prev TRANSFER hold.Vegetables_OldWay; TRUNCATE TABLE prev.Vegetables_OldWay; END GO
J'ai exécuté des tests de concurrence en utilisant deux fenêtres de SQLQueryStress d'Erik Ejlskov Jensen :une pour répéter un appel à la procédure toutes les minutes, et l'autre pour exécuter 16 threads comme celui-ci, des milliers de fois :
BEGIN TRANSACTION; UPDATE TOP (1) dbo.<table> SET name += 'x'; SELECT TOP (10) name FROM dbo.<table> ORDER BY NEWID(); WAITFOR DELAY '00:00:02'; COMMIT TRANSACTION;
Vous pouvez regarder la sortie de SQLQueryStress, ou sys.dm_exec_query_stats, ou Query Store, et vous verrez quelque chose dans le sens des résultats suivants (mais je recommande fortement d'utiliser un outil de surveillance des performances SQL Server de qualité si vous êtes sérieux au sujet de optimisation proactive des environnements de base de données) :
Durée et taux d'erreur | Transfert de schéma | ABORT_AFTER_WAIT : SELF | ABORT_AFTER_WAIT : BLOQUEURS |
---|---|---|---|
Durée moyenne – Transfert/Changement | 96,4 secondes | 68,4 secondes | 20,8 secondes |
Durée moyenne – DML | 18,7 secondes | 2,7 secondes | 2,9 secondes |
Exceptions – Transfert/Changement | 0 | 0,5/minute | 0 |
Exceptions – DML | 0 | 0 | 25,5/minute |
Notez que les durées et le nombre d'exceptions dépendront fortement des spécifications de votre serveur et de ce qui se passe d'autre dans votre environnement. Notez également que, bien qu'il n'y ait aucune exception pour les tests de transfert de schéma lors de l'utilisation de SQLQueryStress, vous pouvez atteindre des délais d'expiration plus stricts en fonction de l'application consommatrice. Et c'était tellement plus lent en moyenne, parce que le blocage s'accumulait beaucoup plus agressivement. Personne ne veut d'exceptions, mais lorsqu'il y a un compromis comme celui-ci, vous préférerez peut-être quelques exceptions ici et là (en fonction de la fréquence de l'opération d'actualisation) plutôt que tout le monde attend toujours plus longtemps.
Commutation de partition vs Renommage/Transfert de schéma pour actualiser les tables SQL Server
La commutation de partition vous permet de choisir quelle partie de votre processus supporte le coût de la simultanéité. Vous pouvez donner la préférence au processus de commutation, afin que les données soient plus fraîches de manière fiable, mais cela signifie que certaines de vos requêtes échoueront. Inversement, vous pouvez hiérarchiser les requêtes, au prix d'un processus d'actualisation plus lent (et d'échecs occasionnels). L'idée principale est que la commutation de partition SQL Server est une méthode supérieure pour actualiser les tables SQL Server par rapport aux techniques précédentes de transfert de nom/schéma sur presque tous les points, et vous pouvez utiliser une logique de nouvelle tentative plus robuste ou expérimenter des tolérances de durée pour atterrir au bon endroit. pour votre charge de travail.