web-dev-qa-db-fra.com

L'instruction de fusion se bloque

J'ai la procédure suivante (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey forment la clé composite de la table cible. CompanyId est une clé étrangère vers une table parent. En outre, il existe un index non clusterisé sur CompanyId asc, UserId asc.

Il est appelé à partir de nombreux threads différents, et je reçois constamment des blocages entre différents processus appelant cette même instruction. Ma compréhension était que le "avec (verrou)" était nécessaire pour éviter les erreurs d'insertion/mise à jour des conditions de concurrence.

Je suppose que deux threads différents verrouillent des lignes (ou des pages) dans des ordres différents lorsqu'ils valident les contraintes, et donc bloquent.

Est-ce une supposition correcte?

Quelle est la meilleure façon de résoudre cette situation (c.-à-d. Pas de blocages, impact minimum sur les performances multi-thread)?

Query Plan Image (Si vous affichez l'image dans un nouvel onglet, elle est lisible. Désolé pour la petite taille.)

  • Il y a au plus 28 lignes dans le @datatable.
  • J'ai retracé le code et je ne vois nulle part où nous commençons une transaction ici.
  • La clé étrangère est configurée pour cascade uniquement lors de la suppression, et il n'y a pas eu de suppression de la table parent.
22
Sako73

OK, après avoir tout examiné plusieurs fois, je pense que votre hypothèse de base était correcte. Ce qui se passe probablement ici, c'est que:

  1. La partie MATCH du MERGE vérifie l'index pour les correspondances, verrouillant en lecture ces lignes/pages au fur et à mesure.

  2. Lorsqu'il a une ligne sans correspondance, il essaiera d'insérer la nouvelle ligne d'index en premier afin de demander un verrouillage en écriture ligne/page ...

Mais si un autre utilisateur est également passé à l'étape 1 sur la même ligne/page, le premier utilisateur sera bloqué de la mise à jour et ...

Si le deuxième utilisateur doit également insérer sur la même page, il se trouve dans une impasse.

AFAIK, il n'y a qu'une seule (simple) façon d'être sûr à 100% que vous ne pouvez pas obtenir un blocage avec cette procédure et ce serait d'ajouter un indice TABLOCKX au MERGE, mais cela aurait probablement un très mauvais impact sur les performances.

Il est possible que l'ajout d'un indice TABLOCK à la place serait suffisant pour résoudre le problème sans avoir un effet important sur vos performances.

Enfin, vous pouvez également essayer d'ajouter PAGLOCK, XLOCK ou les deux PAGLOCK et XLOCK. Encore une fois, pourrait travail et performances pourrait ne pas être trop horrible. Vous devrez l'essayer pour voir.

12
RBarryYoung

Il n'y aurait pas de problème si la variable de table ne contenait qu'une seule valeur. Avec plusieurs lignes, il existe une nouvelle possibilité de blocage. Supposons que deux processus simultanés (A et B) s'exécutent avec des variables de table contenant (1, 2) et (2, 1) pour la même société.

Le processus A lit la destination, ne trouve aucune ligne et insère la valeur "1". Il détient un verrou de ligne exclusif sur la valeur "1". Le processus B lit la destination, ne trouve aucune ligne et insère la valeur "2". Il détient un verrou de ligne exclusif sur la valeur "2".

Le processus A doit désormais traiter la ligne 2 et le processus B doit traiter la ligne 1. Aucun des deux processus ne peut progresser car il nécessite un verrou incompatible avec le verrou exclusif détenu par l'autre processus.

Pour éviter les blocages avec plusieurs lignes, les lignes doivent être traitées (et les tables accédées) dans le même ordre à chaque fois. La variable de table dans le plan d'exécution montré dans la question est un tas, donc les lignes n'ont pas d'ordre intrinsèque (elles sont très susceptibles d'être lues dans l'ordre d'insertion, bien que cela ne soit pas garanti):

Existing plan

L'absence d'un ordre de traitement des lignes cohérent conduit directement à l'opportunité de blocage. Une deuxième considération est que l'absence d'une garantie d'unicité clé signifie qu'une bobine de table est nécessaire pour fournir une protection Halloween correcte. La bobine est une bobine enthousiaste, ce qui signifie que toutes les lignes sont écrites dans un tempdb table de travail avant d'être relue et relue pour l'opérateur d'insertion.

Redéfinir le TYPE de la variable de table pour inclure un PRIMARY KEY En cluster:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

Le plan d'exécution affiche désormais une analyse de l'index clusterisé et la garantie d'unicité signifie que l'optimiseur est en mesure de supprimer le spouleur de table en toute sécurité:

With primary key

Dans les tests avec 5000 itérations de l'instruction MERGE sur 128 threads, aucun blocage n'a eu lieu avec la variable de table en cluster. Je dois souligner que ce n'est que sur la base de l'observation; la variable de table en cluster pourrait également ( techniquement ) produire ses lignes dans une variété d'ordres, mais les chances d'un ordre cohérent sont considérablement améliorées. Le comportement observé devrait être retesté pour chaque nouvelle mise à jour cumulative, service pack ou nouvelle version de SQL Server, bien sûr.

Dans le cas où la définition de variable de table ne peut pas être modifiée, il existe une autre alternative:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Cela permet également d'éliminer le spool (et la cohérence de l'ordre des lignes) au prix de l'introduction d'un tri explicite:

Sort plan

Ce plan n'a également produit aucun blocage en utilisant le même test. Script de reproduction ci-dessous:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
31
Paul White 9

Je pense que SQL_Kiwi a fourni une très bonne analyse. Si vous devez résoudre le problème dans la base de données, vous devez suivre sa suggestion. Bien sûr, vous devez retester qu'il fonctionne toujours pour vous chaque fois que vous effectuez une mise à niveau, appliquez un service pack ou ajoutez/modifiez un index ou une vue indexée.

Il existe trois autres alternatives:

  1. Vous pouvez sérialiser vos insertions afin qu'elles ne se heurtent pas: vous pouvez invoquer sp_getapplock au début de votre transaction et acquérir un verrou exclusif avant d'exécuter votre MERGE. Bien sûr, vous devez encore le tester.

  2. Vous pouvez avoir un thread gérer toutes vos insertions, de sorte que votre serveur d'applications gère la concurrence.

  3. Vous pouvez réessayer automatiquement après les blocages - cela peut être l'approche la plus lente si la concurrence est élevée.

Quoi qu'il en soit, vous seul pouvez déterminer l'impact de votre solution sur les performances.

En règle générale, nous n'avons pas de blocages dans notre système, bien que nous ayons beaucoup de potentiel pour les avoir. En 2011, nous avons fait une erreur dans un déploiement et une demi-douzaine de blocages se sont produits en quelques heures, tous suivant le même scénario. J'ai corrigé cela rapidement et c'était tous les blocages pour l'année.

Nous utilisons principalement l'approche 1 dans notre système. Cela fonctionne vraiment bien pour nous.

8
A-K