Nous tentons de mettre à jour/supprimer un grand nombre d'enregistrements dans une table de plusieurs milliards de lignes. Puisqu'il s'agit d'un tableau populaire, il y a beaucoup d'activité dans les différentes sections de ce tableau. Toute activité de mise à jour/suppression importante est bloquée pendant de longues périodes (car elle attend d'obtenir des verrous sur toutes les lignes ou le verrouillage de page ou de table), ce qui entraîne des délais d'expiration ou plusieurs jours pour terminer la tâche.
Donc, nous changeons l'approche pour supprimer un petit lot de lignes à la fois. Mais nous voulons vérifier si les lignes sélectionnées (disons 100 ou 1000 ou 2000 lignes) sont actuellement verrouillées par un processus différent ou non.
Est-ce faisable?
Merci, ToC
Si je comprends bien la demande, l'objectif est de supprimer des lots de lignes, tandis qu'en même temps, des opérations DML se produisent sur les lignes de la table. Le but est de supprimer un lot; cependant, si des lignes sous-jacentes contenues dans la plage définie par ledit lot sont verrouillées, nous devons ignorer ce lot et passer au lot suivant. Nous devons ensuite revenir à tous les lots qui n'ont pas été précédemment supprimés et réessayer notre logique de suppression d'origine. Nous devons répéter ce cycle jusqu'à ce que tous les lots de lignes requis soient supprimés.
Comme cela a été mentionné, il est raisonnable d'utiliser une indication READPAST et le niveau d'isolement READ COMMITTED (par défaut), afin de sauter les plages passées qui peuvent contenir des lignes bloquées. Je vais aller plus loin et recommander d'utiliser le niveau d'isolement SERIALISABLE et les suppressions de grignotage.
SQL Server utilise des verrous de plage de clés pour protéger une plage de lignes implicitement incluses dans un jeu d'enregistrements lu par une instruction Transact-SQL lors de l'utilisation du niveau d'isolation de transaction sérialisable ... en savoir plus ici: https: // technet .Microsoft.com/en-US/library/ms191272 (v = SQL.105) .aspx
Avec les suppressions de grignotage, notre objectif est d'isoler une plage de lignes et de garantir qu'aucune modification ne se produira sur ces lignes pendant que nous les supprimons, c'est-à-dire que nous ne voulons pas de lectures ou d'insertions fantômes. Le niveau d'isolement sérialisable est destiné à résoudre ce problème.
Avant de présenter ma solution, je voudrais ajouter que je ne recommande pas de basculer le niveau d'isolement par défaut de votre base de données sur SERIALIZABLE ni que ma solution est la meilleure. Je souhaite simplement le présenter et voir où nous pouvons aller d'ici.
Quelques notes d'entretien:
Pour commencer mon expérience, je mettrai en place une base de données de test, un exemple de table, et je remplirai la table de 2 000 000 de lignes.
USE [master];
GO
SET NOCOUNT ON;
IF DATABASEPROPERTYEX (N'test', N'Version') > 0
BEGIN
ALTER DATABASE [test] SET SINGLE_USER
WITH ROLLBACK IMMEDIATE;
DROP DATABASE [test];
END
GO
-- Create the test database
CREATE DATABASE [test];
GO
-- Set the recovery model to FULL
ALTER DATABASE [test] SET RECOVERY FULL;
-- Create a FULL database backup
-- in order to ensure we are in fact using
-- the FULL recovery model
-- I pipe it to dev null for simplicity
BACKUP DATABASE [test]
TO DISK = N'nul';
GO
USE [test];
GO
-- Create our table
IF OBJECT_ID('dbo.tbl','U') IS NOT NULL
BEGIN
DROP TABLE dbo.tbl;
END;
CREATE TABLE dbo.tbl
(
c1 BIGINT IDENTITY (1,1) NOT NULL
, c2 INT NOT NULL
) ON [PRIMARY];
GO
-- Insert 2,000,000 rows
INSERT INTO dbo.tbl
SELECT TOP 2000
number
FROM
master..spt_values
ORDER BY
number
GO 1000
À ce stade, nous aurons besoin d'un ou plusieurs index sur lesquels les mécanismes de verrouillage du niveau d'isolement SERIALIZABLE peuvent agir.
-- Add a clustered index
CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1
ON dbo.tbl (c1);
GO
-- Add a non-clustered index
CREATE NONCLUSTERED INDEX IX_tbl_c2
ON dbo.tbl (c2);
GO
Maintenant, vérifions que nos 2 000 000 de lignes ont été créées
SELECT
COUNT(*)
FROM
tbl;
Nous avons donc notre base de données, notre table, nos index et nos lignes. Alors, configurons l'expérience pour supprimer les suppressions. Tout d'abord, nous devons décider de la meilleure façon de créer un mécanisme de suppression de grignotage typique.
DECLARE
@BatchSize INT = 100
, @LowestValue BIGINT = 20000
, @HighestValue BIGINT = 20010
, @DeletedRowsCount BIGINT = 0
, @RowCount BIGINT = 1;
SET NOCOUNT ON;
GO
WHILE @DeletedRowsCount < ( @HighestValue - @LowestValue )
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION
DELETE
FROM
dbo.tbl
WHERE
c1 IN (
SELECT TOP (@BatchSize)
c1
FROM
dbo.tbl
WHERE
c1 BETWEEN @LowestValue AND @HighestValue
ORDER BY
c1
);
SET @RowCount = ROWCOUNT_BIG();
COMMIT TRANSACTION;
SET @DeletedRowsCount += @RowCount;
WAITFOR DELAY '000:00:00.025';
CHECKPOINT;
END;
Comme vous pouvez le voir, j'ai placé la transaction explicite dans la boucle while. Si vous souhaitez limiter les débits de journal, n'hésitez pas à le placer en dehors de la boucle. De plus, étant donné que nous sommes dans le modèle de récupération COMPLET, vous souhaiterez peut-être créer des sauvegardes du journal des transactions plus souvent lors de l'exécution de vos opérations de suppression de grignotage, afin de vous assurer que votre journal des transactions ne puisse pas croître de manière scandaleuse.
Donc, j'ai quelques objectifs avec cette configuration. Tout d'abord, je veux mes verrous de plage de clés; donc, j'essaie de garder les lots aussi petits que possible. Je ne veux pas non plus avoir un impact négatif sur la concurrence sur ma table "gigantesque"; donc je veux prendre mes serrures et les laisser le plus vite possible. Je vous recommande donc de réduire la taille de vos lots.
Maintenant, je veux fournir un exemple très court de cette routine de suppression en action. Nous devons ouvrir une nouvelle fenêtre dans SSMS et supprimer une ligne de notre tableau. Je le ferai dans une transaction implicite en utilisant le niveau d'isolement READ COMMITTED par défaut.
DELETE FROM
dbo.tbl
WHERE
c1 = 20005;
Cette ligne a-t-elle été supprimée?
SELECT
c1
FROM
dbo.tbl
WHERE
c1 BETWEEN 20000 AND 20010;
Oui, il a été supprimé.
Maintenant, afin de voir nos verrous, ouvrons une nouvelle fenêtre dans SSMS et ajoutons un ou deux extraits de code. J'utilise sp_whoisactive d'Adam Mechanic, qui peut être trouvé ici: sp_whoisactive
SELECT
DB_NAME(resource_database_id) AS DatabaseName
, resource_type
, request_mode
FROM
sys.dm_tran_locks
WHERE
DB_NAME(resource_database_id) = 'test'
AND resource_type = 'KEY'
ORDER BY
request_mode;
-- Our insert
sp_lock 55;
-- Our deletions
sp_lock 52;
-- Our active sessions
sp_whoisactive;
Maintenant, nous sommes prêts à commencer. Dans une nouvelle fenêtre SSMS, commençons une transaction explicite qui tentera de réinsérer la ligne que nous avons supprimée. En même temps, nous lancerons notre opération de suppression de grignotage.
Le code d'insertion:
BEGIN TRANSACTION
SET IDENTITY_INSERT dbo.tbl ON;
INSERT INTO dbo.tbl
( c1 , c2 )
VALUES
( 20005 , 1 );
SET IDENTITY_INSERT dbo.tbl OFF;
--COMMIT TRANSACTION;
Commençons les deux opérations en commençant par l'insertion et suivies de nos suppressions. Nous pouvons voir les serrures à clés et les serrures exclusives.
L'insert a généré ces verrous:
La suppression/sélection de grignotage contient ces verrous:
Notre insert bloque notre suppression comme prévu:
Maintenant, commettons la transaction d'insertion et voyons ce qui se passe.
Et comme prévu, toutes les transactions sont terminées. Maintenant, nous devons vérifier si l'insertion était un fantôme ou si l'opération de suppression l'a également supprimée.
SELECT
c1
FROM
dbo.tbl
WHERE
c1 BETWEEN 20000 AND 20015;
En fait, l'encart a été supprimé; ainsi, aucun insert fantôme n'était autorisé.
Donc, en conclusion, je pense que la véritable intention de cet exercice n'est pas d'essayer de suivre chaque verrou de ligne, de page ou de table et de déterminer si un élément d'un lot est verrouillé et nécessiterait donc notre opération de suppression pour attendre. C'était peut-être l'intention des intervenants; cependant, cette tâche est herculéenne et pratiquement impossible, voire impossible. Le véritable objectif est de s'assurer qu'aucun phénomène indésirable ne se produit une fois que nous avons isolé la plage de notre lot avec des verrous qui nous sont propres, puis que nous supprimons le lot. Le niveau d'isolement SERIALISABLE atteint cet objectif. La clé est de garder vos petits morceaux, votre journal des transactions sous contrôle et d'éliminer les phénomènes indésirables.
Si vous voulez de la vitesse, ne construisez pas de tables gigantesques qui ne peuvent pas être partitionnées et ne pouvez donc pas utiliser la commutation de partition pour les résultats les plus rapides. La clé de la vitesse est le partitionnement et le parallélisme; la clé de la souffrance est le grignotage et le verrouillage des vies.
S'il vous plait, faite moi part de votre avis.
J'ai créé quelques exemples supplémentaires du niveau d'isolement SERIALIZABLE en action. Ils devraient être disponibles sur les liens ci-dessous.
Opérations d'égalité - Verrouillage de plage de clés sur les valeurs de clé suivantes
Opérations d'égalité - Récupération singleton de données existantes
Opérations d'égalité - Récupération singleton de données inexistantes
Opérations d'inégalité - Verrouillage de plage de clés sur la plage et valeurs de clé suivantes
Donc, nous changeons l'approche pour supprimer un petit lot de lignes à la fois.
C'est une très bonne idée de supprimer dans petits lots prudents ou morceaux . Je voudrais ajouter un petit waitfor delay '00:00:05'
Et selon le modèle de récupération de la base de données - si FULL
, alors faites un log backup
Et si SIMPLE
alors faites un manual CHECKPOINT
Pour éviter les ballonnements du journal des transactions - entre les lots.
Mais nous voulons vérifier si les lignes sélectionnées (disons 100 ou 1000 ou 2000 lignes) sont actuellement verrouillées par un processus différent ou non.
Ce que vous dites n'est pas entièrement possible hors de la boîte (en gardant à l'esprit vos 3 points). Si la suggestion ci-dessus - small batches + waitfor delay
Ne fonctionne pas (à condition de faire les tests appropriés), vous pouvez utiliser le query HINT
.
N'utilisez pas NOLOCK
- voir kb/308886 , Problèmes de cohérence de lecture de SQL Server par Itzik Ben-Gan , Mettre NOLOCK partout - Par Aaron Bertrand et Astuce NOLOCK SQL Server et autres mauvaises idées .
READPAST
hint vous aidera dans votre scénario. L'astuce Gist of READPAST
est - s'il y a un verrou au niveau ligne alors le serveur SQL ne le lira pas.
Spécifie que le moteur de base de données ne lit pas les lignes verrouillées par d'autres transactions. Lorsque
READPAST
est spécifié, les verrous au niveau des lignes sont ignorés. Autrement dit, le moteur de base de données ignore les lignes au lieu de bloquer la transaction en cours jusqu'à ce que les verrous soient libérés.
Au cours de mes tests limités, j'ai trouvé un très bon débit en utilisant DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)
et en définissant le niveau d'isolement de la session de requête sur READ COMMITTED
En utilisant SET TRANSACTION ISOLATION LEVEL READ COMMITTED
Qui est de toute façon le niveau d'isolement par défaut.
Résumant les autres approches initialement proposées dans commentaires à la question.
Utilisez NOWAIT
si le comportement souhaité consiste à faire échouer le bloc entier dès qu'un verrou incompatible est rencontré.
Depuis la documentation NOWAIT
:
Demande au moteur de base de données de renvoyer un message dès qu'un verrou est rencontré sur la table.
NOWAIT
équivaut à spécifierSET LOCK_TIMEOUT 0
pour une table spécifique. Le conseilNOWAIT
ne fonctionne pas lorsque le conseilTABLOCK
est également inclus. Pour terminer une requête sans attendre lors de l'utilisation de l'astuceTABLOCK
, faites précéder la requête deSETLOCK_TIMEOUT 0;
à la place.
Utilisation SET LOCK_TIMEOUT
pour obtenir un résultat similaire, mais avec un délai d'expiration configurable:
Du SET LOCK_TIMEOUT
documentation
Spécifie le nombre de millisecondes pendant lequel une instruction attend qu'un verrou soit libéré.
Lorsqu'une attente de verrouillage dépasse la valeur du délai d'expiration, une erreur est renvoyée. Une valeur de 0 signifie de ne pas attendre du tout et de renvoyer un message dès qu'un verrou est rencontré.
Pour supposer que nous avons 2 requêtes parallèles:
connect/session 1: verrouillera la ligne = 777
SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777
connect/session 2: ignorera la ligne verrouillée = 777
SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777
OU connect/session 2: lèvera une exception
DECLARE @id integer;
SELECT @id = id FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777;
IF @id is NULL
THROW 51000, 'Hi, a record is locked or does not exist.', 1;