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


21

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 le verrouillage de table), ce qui entraîne des délais d'attente 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


2
Avez-vous examiné READPAST dans le cadre de l'instruction delete ou NOWAIT (pour faire échouer tout le groupe)? L'un d'eux peut fonctionner pour vous. msdn.microsoft.com/en-us/library/ms187373.aspx
Sean dit Supprimer Sara Chipps

@SeanGallardy Je n'ai pas envisagé cette idée, mais maintenant je vais le faire. Mais existe-t-il un moyen plus simple de vérifier si une ligne particulière est verrouillée ou non? Merci.
ToC

3
Vous pouvez également consulter LOCK_TIMEOUT ( msdn.microsoft.com/en-us/library/ms189470.aspx ). Par exemple, c'est la façon dont sp_whoisactive d'Adam Machanic garantit que la procédure n'attend pas trop longtemps si elle est bloquée lors de la tentative de collecte d'un plan d'exécution. Vous pouvez définir un court délai ou même utiliser une valeur de 0 ("0 signifie ne pas attendre du tout et renvoyer un message dès qu'un verrou est rencontré.") Vous pouvez combiner cela avec un TRY / CATCH pour intercepter l'erreur 1222 ( "Délai d'expiration de la demande de verrouillage dépassé") et passez au lot suivant.
Geoff Patterson

@gpatterson Approche intéressante. Je vais essayer ça aussi.
ToC

2
Pour répondre, non, il n'y a pas de moyen plus simple de voir si les lignes sont verrouillées, sauf si quelque chose est spécifiquement fait dans l'application. Fondamentalement, vous pouvez d'abord faire une sélection avec HOLDLOCK et XLOCK avec un ensemble lock_timeout (c'est ce que NOWAIT dans mon commentaire d'origine concerne, en définissant le délai d'attente à 0). Si vous ne l'obtenez pas, vous savez que quelque chose est verrouillé. Il n'y a rien de facilement disponible pour dire "La ligne X du tableau Y utilise-t-elle l'index Z verrouillé par quelque chose". Nous pouvons voir si la table a des verrous ou si des pages / lignes / clés / etc ont des verrous, mais traduire cela en lignes spécifiques dans une requête ne serait pas facile.
Sean dit Supprimer Sara Chipps

Réponses:


10

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 d'ignorer les plages passées pouvant contenir des lignes bloquées. J'irai plus loin et recommanderai 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 de recommander que ma solution soit 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 vais mettre 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;

entrez la description de l'image ici

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 journaux, 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 vais le faire 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é.

Preuve de la ligne supprimée

Maintenant, pour 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.

Gamme et serrures eXclusive

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

Insérez les verrous

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

entrez la description de l'image ici

Notre insert bloque notre suppression comme prévu:

Insérer des blocs Supprimer

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

Valider la suppression

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

Pas d'insertion fantôme

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 attendez. 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 garantir 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 prochaines valeurs clés

Opérations d'égalité - extraction singleton de données existantes

Opérations d'égalité - extraction singleton de données inexistantes

Opérations d'inégalité - Verrouillage de plage de clés sur la plage et valeurs de clé suivantes


9

Donc, nous changeons l'approche pour supprimer un petit lot de lignes à la fois.

C'est une très bonne idée de supprimer par petits lots ou morceaux prudents . J'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, puis faire log backupet si SIMPLEensuite faire manual CHECKPOINTpour é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 delayne fonctionne pas (à condition de faire des tests appropriés), vous pouvez utiliser le query HINT.

Ne pas utiliser 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 SQL Server NOLOCK Hint & autres mauvaises idées .

READPASTun indice vous aidera dans votre scénario. L'essentiel de l' READPASTindice est - s'il y a un verrou au niveau de la ligne, 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 READPASTest 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 lors de l'utilisation DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)et de la définition du niveau d'isolement de la session de requête sur READ COMMITTEDusing, SET TRANSACTION ISOLATION LEVEL READ COMMITTEDqui est de toute façon le niveau d'isolement par défaut.


2

Résumant d'autres approches initialement proposées dans les commentaires de la question.


  1. À utiliser NOWAITsi le comportement souhaité consiste à faire échouer le bloc entier dès qu'un verrou incompatible est rencontré.

    De la NOWAITdocumentation :

    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 une table spécifique. L' NOWAITastuce ne fonctionne pas lorsque l' TABLOCKastuce est également incluse. Pour terminer une requête sans attendre lors de l'utilisation de l' TABLOCKindicateur, faites précéder la requête par à la SETLOCK_TIMEOUT 0;place.

  2. Utilisez SET LOCK_TIMEOUTpour obtenir un résultat similaire, mais avec un délai d'expiration configurable:

    De la SET LOCK_TIMEOUTdocumentation

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


0

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;

-1

Essayez de filtrer sur quelque chose comme ça - cela peut devenir compliqué si vous voulez devenir vraiment très précis. Regardez dans BOL pour la description de sys.dm_tran_locks

SELECT 
tl.request_session_id,
tl.resource_type,
tl.resource_associated_entity_id,
db_name(tl.resource_database_id) 'Database',
CASE 
    WHEN tl.resource_type = 'object' THEN object_name(tl.resource_associated_entity_id, tl.resource_database_id)
    ELSE NULL
END 'LockedObject',
tl.resource_database_id,
tl.resource_description,
tl.request_mode,
tl.request_type,
tl.request_status FROM [sys].[dm_tran_locks] tl WHERE resource_database_id <> 2order by tl.request_session_id

juste curieux - pourquoi le downvote?
rottengeek

-11

Vous pouvez utiliser NoLOCK pendant la suppression et si les lignes sont verrouillées, elles ne seront pas supprimées. Ce n'est pas idéal mais peut faire l'affaire pour vous.

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True

7
Si j'essaye ça sur ma machine locale, j'obtiens Msg 1065, Level 15, State 1, Line 15 The NOLOCK and READUNCOMMITTED lock hints are not allowed for target tables of INSERT, UPDATE, DELETE or MERGE statements., obsolète depuis 2005
Tom V - Team Monica
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.