web-dev-qa-db-fra.com

SQL Server - Existe-t-il un moyen d'empêcher les verrous de plage partagés à «l'infini» lors de l'utilisation d'une vue indexée?

J'ai votre en-tête/tableau de détails de base (pensez aux commandes et aux détails de la commande). La table d'en-tête a une colonne d'identité comme clé en cluster, le détail a l'ID d'en-tête et une colonne de numéro de ligne comme clé en cluster. L'ID d'en-tête est une valeur d'identité toujours incrémentée et le numéro de ligne est également une valeur incrémentielle.

J'essayais d'ajouter une vue indexée sur les détails pour agréger les données afin que nous n'ayons pas à le faire dans le code ou via des déclencheurs, qui a son propre ensemble de problèmes de concurrence dans le système existant.

Tout semble et fonctionne bien jusqu'à ce que nous commencions à le tester. Il est prévu qu'il y aura ~ 1500 détails/sec (90 000/min) ajoutés au tableau.

Lorsqu'une ligne est insérée dans la table détaillée, la vue indexée est également mise à jour. Pendant l'insertion, il apparaît qu'un verrou de plage partagé (RangeS-U) est pris sur la vue indexée. La plage prise est la clé actuelle à la clé suivante, semblable à la façon dont les verrous seraient pris sous le niveau d'isolement sérialisable. La connexion est établie sous lecture validée. Le goulot d'étranglement semble se produire lorsque la clé "suivant" n'existe pas dans le tableau. Dans cette situation, le verrou partagé est porté à la clé "infini (ffffffff)".

Cela décrit essentiellement le comportement que je vois mais ne fournit aucune solution de contournement. https://www.brentozar.com/archive/2018/09/locks-taken-during-indexed-view-modifications/

Sous la charge ci-dessus, le serveur ne peut tout simplement pas suivre les insertions, et les choses commencent à sauvegarder assez rapidement. 500 connexions simultanées sur 600 sont bloquées à un moment donné. Il ne semble pas qu'une vue indexée agrégée sur une clé en constante augmentation puisse suivre nos exigences de concurrence.

Nous utilisons SQL Server 2012 Standard Edition et procédons à une mise à niveau vers 2019 bientôt.

Existe-t-il un moyen de modifier ce comportement de verrouillage sur les vues indexées ou est-ce un effort inutile de ma part, auquel cas je devrai emprunter la voie des agrégats basés sur le code/déclencheur, ou ai-je raté quelque chose? Si 2019 ne présente pas le même comportement qui fonctionne pour moi, la base de données sera mise à niveau avant la fin des travaux.

Les scripts inclus représentent les tables impliquées, mais ne sont évidemment pas les tables réelles. Le comportement est reproductible en les utilisant.

Configuration

if object_id(N'dbo.LockTest') is null
begin
  create table dbo.LockTest
  ( LockTestID    int not null primary key
  , LockTestValue int     null
  );
  insert into dbo.locktest values(1, 1), (2, 2), (3, 3), (7, 7), (8, 8);
end;

if object_id(N'dbo.LockTestDetails') is null
begin
  create table dbo.LockTestDetails
  ( LockTestID  int not null
  , LineNumber  int not null
  , Val         int not null
  , PRIMARY KEY(LockTestID, LineNumber)
  , foreign key(LockTestID) references dbo.LockTest(LockTestID)
  );
  insert into dbo.LockTestDetails values(2, 1, 5), (2, 2, 4);
end

if object_id(N'dbo.LockTestTotals') is null
begin
  exec sp_executesql N'
    CREATE VIEW dbo.LockTestTotals with schemabinding as
    SELECT d.LockTestID, Lines = COUNT_BIG(*), Val = SUM(Val)
    FROM dbo.LockTestDetails d
    GROUP BY d.LockTestID';
  exec sp_executesql N'
    create unique clustered index PK_LockTestTotals on dbo.LockTestTotals(LockTestID)';
end

Exemple 1

-- run in session 1.  
-- range lock taken from 1 to the next key, 2.
begin transaction
insert into dbo.LockTestDetails values(1, 1, 1);
waitfor delay '00:00:20';
rollback

-- run in session 2
-- record is inserted.  not blocked by session 1 range lock.
-- range lock taken from 7 to next key, 'infinity(ffffffff)'
-- no other details can be added with an id higher than 7.
begin transaction
insert into dbo.LockTestDetails values(7, 1, 1);
waitfor delay '00:00:20';
rollback

Exemple 2

-- run in session 1.  
-- range lock taken from 7 to next key, 'infinity(ffffffff)'
-- no other details can be added with an id higher than 7.
begin transaction
insert into dbo.LockTestDetails values(7, 1, 1);
waitfor delay '00:00:20';
rollback

-- run in session 2
-- record is blocked by session 1 range lock.
begin transaction
insert into dbo.LockTestDetails values(8, 1, 1);
waitfor delay '00:00:20';
rollback

Script pour afficher les verrous

declare @session int = null;

select
  l.request_session_id
, l.resource_type
, resource_description = rtrim(l.resource_description)
, [object_name] = CASE
    WHEN resource_type = 'OBJECT'
    THEN OBJECT_SCHEMA_NAME(l.resource_associated_entity_id) + '.' + OBJECT_NAME(l.resource_associated_entity_id)
    ELSE OBJECT_SCHEMA_NAME(p.[OBJECT_ID]) + '.' + OBJECT_NAME(p.[object_id])
    END
, index_name = i.[name]
, l.request_mode
, l.request_status
, l.resource_subtype
, l.resource_associated_entity_id
from sys.dm_tran_locks l
  left join sys.partitions p 
    ON p.hobt_id = l.resource_associated_entity_id
  LEFT JOIN sys.indexes i
    ON i.[OBJECT_ID] = p.[OBJECT_ID] 
    AND i.index_id = p.index_id
where resource_database_id = db_id()
and request_session_id between  isnull(@session, 0) and isnull(@session, 5000)
and request_session_id <> @@spid
order by 
  [object_name]
, CASE 
    WHEN i.[name] is null then 0
    WHEN LEFT(i.[name], 2) = 'PK' THEN 1
    WHEN LEFT(i.[name], 2) = 'UK' THEN 2
    ELSE 3 END
, index_name
, case resource_type
    when 'DATABASE' then 0
    when 'OBJECT' then 1
    when 'PAGE' then 2
    when 'KEY' then 3
    when 'RID' then 4
    else 99 end
, resource_description
, request_session_id;
7
ScorpionJL

Vous ne pouvez rien y faire. SQL Server prend automatiquement des mesures pour garantir que la vue indexée toujours reste synchronisée avec les tables de base.

Lors de la lecture de la vue indexée pour voir si les données associées aux clés modifiées existent ou non, SQL Server doit s'assurer que les données ne changent pas tant que la maintenance de la vue n'est pas terminée. Cela inclut le cas où la clé n'existe pas - elle doit continuer ne pas exister tant qu'elle n'a pas été insérée. Le moteur répond à cette exigence en accédant à la vue indexée sous serializable isolation. Cette escalade d'isolement local se produit quel que soit le niveau d'isolement actuel de la session.

Par intérêt, les astuces ajoutées à la lecture de la vue indexée sont:

UPDLOCK SERIALIZABLE DETECT-SNAPSHOT-CONFLICT

Le DETECT-SNAPSHOT-CONFLICT hint indique à SQL Server de vérifier les conflits d'écriture sous l'isolement de l'instantané.

Dans votre exemple, le moteur ajoute également des conseils à la lecture de la table parent pour valider la relation de clé étrangère:

READ-COMMITTEDLOCK FORCEDINDEX DETECT-SNAPSHOT-CONFLICT

Le READ-COMMITTEDLOCK hint garantit que les verrous partagés sont pris lors de l'exécution sous l'isolement de capture instantanée de lecture validée.

Ces conseils sont requis pour exactitude et ne peuvent pas être désactivés.

Solutions de contournement

Vous pourriez penser à faire de l'index cluster descendant au lieu de ascendant, mais cela introduirait des problèmes supplémentaires (pour les insertions ascendantes), et ne déplacerait le point de conflit que d'une extrémité de la structure à l'autre.

Si vous essayez d'écrire la même logique à l'aide de déclencheurs ou de code en dehors de la base de données, vous finirez par manquer un cas Edge (conduisant à un résumé inexact) ou en utilisant à peu près les mêmes indications que SQL Server. Ce type de logique est notoirement difficile à obtenir du premier coup, et nécessite des tests approfondis sous forte concurrence pour être validé. D'un autre côté, les totaux approximatifs peuvent être suffisants dans certains cas.

Si vous pouvez tolérer une certaine latence, vous pouvez regrouper les insertions et les appliquer à la vue indexée en bloc sur une seule session/thread. Par exemple, en conservant des lignes insérées dans une zone de transfert, puis en mettant à jour les tables de base d'une instruction d'insertion de temps en temps. La signification de "volume" ici n'a pas besoin d'être terriblement grande, juste assez pour suivre confortablement la charge de travail maximale attendue. Cela compliquera le signalement des erreurs.

Fondamentalement, les vues indexées ne sont pas bien adaptées aux mises à jour très rapides des tables de base en général, et aux insertions de fin de gamme en particulier.

8
Paul White 9