web-dev-qa-db-fra.com

Dans Sql Server, existe-t-il un moyen de vérifier si un groupe de lignes sélectionné est verrouillé ou non?

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.

  • Sinon, procédez à la suppression/mise à jour.
  • S'ils sont verrouillés, passez au groupe d'enregistrements suivant.
  • À la fin, revenez au début et essayez de mettre à jour/supprimer ceux qui ont été laissés de côté.

Est-ce faisable?

Merci, ToC

21
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:

  1. La version de SQL Server que j'utilise est Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
  2. Ma base de données de test utilise le modèle de récupération COMPLET

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;

enter image description here

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é.

Proof of Deleted Row

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.

Range and eXclusive Locks

L'insert a généré ces verrous:

Insert's Locks

La suppression/sélection de grignotage contient ces verrous:

enter image description here

Notre insert bloque notre suppression comme prévu:

Insert Blocks Delete

Maintenant, commettons la transaction d'insertion et voyons ce qui se passe.

Commit the Delete

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é.

No Phantom Insert

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.

Supprimer l'opération

Insérer une opération

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

10
ooutwire

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.

9
Kin Shah

Résumant les autres approches initialement proposées dans commentaires à la question.


  1. 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écifier SET LOCK_TIMEOUT 0 pour une table spécifique. Le conseil NOWAIT ne fonctionne pas lorsque le conseil TABLOCK est également inclus. Pour terminer une requête sans attendre lors de l'utilisation de l'astuce TABLOCK, faites précéder la requête de SETLOCK_TIMEOUT 0; à la place.

  2. 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é.

2
Paul White 9

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;
0
Lebnik