INSERT massifs bloquant les SELECT

14

J'ai un problème avec une quantité massive d'INSERT qui bloquent mes opérations SELECT.

Schéma

J'ai une table comme celle-ci:

CREATE TABLE [InverterData](
    [InverterID] [bigint] NOT NULL,
    [TimeStamp] [datetime] NOT NULL,    
    [ValueA] [decimal](18, 2) NULL,
    [ValueB] [decimal](18, 2) NULL
    CONSTRAINT [PrimaryKey_e149e28f-5754-4229-be01-65fafeebce16] PRIMARY KEY CLUSTERED 
    (
        [TimeStamp] DESC,
        [InverterID] ASC
    ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
    , IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON)
)

J'ai également cette petite procédure d'aide, qui me permet d'insérer ou de mettre à jour (mise à jour en cas de conflit) avec la commande MERGE:

CREATE PROCEDURE [InsertOrUpdateInverterData]
    @InverterID bigint, @TimeStamp datetime
    , @ValueA decimal(18,2), @ValueB decimal(18,2)
AS
BEGIN
    MERGE [InverterData] AS TARGET
        USING (VALUES (@InverterID, @TimeStamp, @ValueA, @ValueB))
        AS SOURCE ([InverterID], [TimeStamp], [ValueA], [ValueB])
        ON TARGET.[InverterID] = @InverterID AND TARGET.[TimeStamp] = @TimeStamp
    WHEN MATCHED THEN
        UPDATE
        SET [ValueA] = SOURCE.[ValueA], [ValueB] = SOURCE.[ValueB]              
    WHEN NOT MATCHED THEN
        INSERT ([InverterID], [TimeStamp], [ValueA], [ValueB]) 
        VALUES (SOURCE.[InverterID], SOURCE.[TimeStamp], SOURCE.[ValueA], SOURCE.[ValueB]);
END

Usage

J'ai maintenant exécuté des instances de service sur plusieurs serveurs qui effectuent des mises à jour massives en appelant la [InsertOrUpdateInverterData]procédure rapidement.

Il existe également un site Web qui effectue des requêtes SELECT sur [InverterData] table.

Problème

Si je fais des requêtes SELECT sur le [InverterData] table, elles sont traitées dans des intervalles de temps différents, en fonction de l'utilisation INSERT de mes instances de service. Si je mets en pause toutes les instances de service, le SELECT est ultra-rapide, si l'instance effectue une insertion rapide, les SELECT deviennent vraiment lents ou même une annulation de timeout.

Tentatives

J'ai fait quelques SELECT sur la [sys.dm_tran_locks]table pour trouver des processus de verrouillage, comme celui-ci

SELECT
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingText,
tl.request_mode

FROM sys.dm_tran_locks AS tl

INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

Voici le résultat:

entrez la description de l'image ici

S = partagé. La session d'attente bénéficie d'un accès partagé à la ressource.

Question

Pourquoi les SELECT sont-ils bloqués par la [InsertOrUpdateInverterData]procédure qui utilise uniquement les commandes MERGE?

Dois-je utiliser une sorte de transaction avec un mode d'isolement défini à l'intérieur de [InsertOrUpdateInverterData]?

Mise à jour 1 (liée à la question de @Paul)

Base sur les rapports internes du serveur MS-SQL sur [InsertOrUpdateInverterData]les statistiques suivantes:

  • Temps CPU moyen: 0,12 ms
  • Processus de lecture moyens: 5,76 par / s
  • Processus d'écriture moyens: 0,4 par / s

Sur cette base, il semble que la commande MERGE soit principalement occupée à lire des opérations qui verrouillent la table! (?)

Mise à jour 2 (liée à la question de @Paul)

La [InverterData]table a les statistiques de stockage suivantes:

  • Espace de données: 26901,86 Mo
  • Nombre de lignes: 131 827 749
  • Partitionné: vrai
  • Nombre de partitions: 62

Voici l' ensemble de résultats sp_WhoIsActive (le plus) complet :

SELECT commander

  • jj hh: mm: ss.mss: 00 00: 01: 01.930
  • session_id: 73
  • wait_info: (12629ms) LCK_M_S
  • CPU: 198
  • blocking_session_id: 146
  • lit: 99,368
  • écrit: 0
  • statut: suspendu
  • open_tran_count: 0

[InsertOrUpdateInverterData]Commande de blocage

  • jj hh: mm: ss.mss: 00 00: 00: 00.330
  • session_id: 146
  • wait_info: NULL
  • Processeur: 3 972
  • blocking_session_id: NULL
  • lit: 376,95
  • écrit: 126
  • statut: dormir
  • open_tran_count: 1
Steffen Mangold
la source
Le ([TimeStamp] DESC, [InverterID] ASC)ressemble à un choix étrange pour l'index clusterisé. Je veux dire la DESCpartie.
ypercubeᵀᴹ
Je comprends votre point: un index clusterisé DESC insérant des données forcerait la reconstruction de la table plutôt en ajoutant à la fin ... performance dog; verrouillerait la table pendant la reconstruction ... oui. par Jove, vous l'avez. La structure est la cause du verrouillage plus que des verrous.
Alocyte

Réponses:

12

Tout d'abord, bien que légèrement sans rapport avec la question principale, votre MERGEdéclaration est potentiellement à risque d'erreurs en raison d'une condition de concurrence . Le problème, en résumé, est qu'il est possible pour plusieurs threads simultanés de conclure que la ligne cible n'existe pas, ce qui entraîne des tentatives de collision pour l'insertion. La cause principale est qu'il n'est pas possible de prendre un verrou partagé ou de mise à jour sur une ligne qui n'existe pas. La solution consiste à ajouter un indice:

MERGE [dbo].[InverterData] WITH (SERIALIZABLE) AS [TARGET]

L' indice de niveau d'isolement sérialisable garantit que la plage de clés où la ligne irait est verrouillée. Vous avez un index unique pour prendre en charge le verrouillage de plage, donc cette indication n'aura pas d'effet négatif sur le verrouillage, vous obtiendrez simplement une protection contre cette condition de course potentielle.

Question principale

Pourquoi sont-ils SELECTsbloqués par la procédure [InsertOrUpdateInverterData] qui utilise uniquement des MERGEcommandes?

Sous le niveau d'isolement validé par lecture verrouillée par défaut, des verrous partagés (S) sont pris lors de la lecture des données et généralement (mais pas toujours) libérés peu de temps après la fin de la lecture. Certains verrous partagés sont conservés à la fin de l'instruction.

Une MERGEinstruction modifie les données, elle acquiert donc des verrous S ou de mise à jour (U) lors de la localisation des données à modifier, qui sont converties en verrous exclusifs (X) juste avant d'effectuer la modification réelle. Les verrous U et X doivent être maintenus jusqu'à la fin de la transaction.

Cela est vrai à tous les niveaux d'isolement, à l'exception de l'isolement «optimiste» des instantanés (SI), à ne pas confondre avec la version validée en lecture, également connue sous le nom d' isolement des instantanés validés en lecture (RCSI).

Rien dans votre question ne montre qu'une session en attente d'un verrou S est bloquée par une session détenant un verrou U. Ces verrous sont compatibles . Tout blocage est presque certainement causé par un blocage sur un verrou X maintenu. Cela peut être un peu difficile à capturer lorsqu'un grand nombre de verrous à court terme sont pris, convertis et libérés dans un court intervalle de temps.

La open_tran_count: 1commande InsertOrUpdateInverterData mérite d'être étudiée. Bien que la commande n'ait pas été exécutée très longtemps, vous devez vérifier que vous n'avez pas de transaction conteneur (dans l'application ou la procédure stockée de niveau supérieur) qui est inutilement longue. La meilleure pratique consiste à garder les transactions aussi courtes que possible. Ce n'est peut-être rien, mais vous devez absolument vérifier.

Solution potentielle

Comme Kin l'a suggéré dans un commentaire, vous pouvez chercher à activer un niveau d'isolement de version de ligne (RCSI ou SI) sur cette base de données. RCSI est le plus souvent utilisé, car il ne nécessite généralement pas autant de changements d'application. Une fois activé, le niveau d'isolement validé par défaut utilise des versions de ligne au lieu de prendre des verrous S pour les lectures, de sorte que le blocage SX est réduit ou éliminé. Certaines opérations (par exemple les vérifications de clés étrangères) acquièrent toujours des verrous S sous RCSI.

Sachez cependant que les versions de lignes consomment de l'espace tempdb, globalement proportionnel au taux d'activité de changement et à la durée des transactions. Vous devrez tester votre mise en œuvre à fond sous charge pour comprendre et planifier l'impact de RCSI (ou SI) dans votre cas.

Si vous souhaitez localiser votre utilisation de la gestion des versions, plutôt que de l'activer pour l'ensemble de la charge de travail, SI pourrait toujours être un meilleur choix. En utilisant SI pour les transactions de lecture, vous éviterez les conflits entre les lecteurs et les écrivains, au détriment des lecteurs de voir la version de la ligne avant le début de toute modification simultanée (plus correctement, l'opération de lecture sous SI verra toujours l'état engagé de la ligne au moment où la transaction SI a commencé). Il y a peu ou pas d'avantages à utiliser SI pour les transactions d'écriture, car les verrous d'écriture seront toujours pris et vous devrez gérer tous les conflits d'écriture. Sauf si c'est ce que vous voulez :)

Remarque: Contrairement à RCSI (qui, une fois activé, s'applique à toutes les transactions exécutées à la lecture validée), SI doit être explicitement demandé à l'aide de SET TRANSACTION ISOLATION SNAPSHOT;.

Les comportements subtils qui dépendent des lecteurs bloquant les écrivains (y compris dans le code de déclenchement!) Rendent les tests essentiels. Voir ma série d'articles liés et Books Online pour plus de détails. Si vous décidez d'utiliser RCSI, assurez-vous de consulter les modifications de données sous Lire l'isolement de l'instantané validé en particulier.

Enfin, vous devez vous assurer que votre instance est corrigée pour SQL Server 2008 Service Pack 4.

Paul White 9
la source
0

Humblement, je n'utiliserais pas la fusion. J'irais avec IF Exists (UPDATE) ELSE (INSERT) - vous avez une clé en cluster avec les deux colonnes que vous utilisez pour identifier les lignes, c'est donc un test facile.

Vous mentionnez des insertions MASSIVES et pourtant vous faites 1 par 1 ... vous avez pensé à regrouper les données dans une table intermédiaire et à utiliser le pouvoir de l'ensemble de données SQL POWER OVERWHELMING pour faire plus d'une mise à jour / insertion à la fois? Comme avoir un test de routine pour le contenu de la table de transfert et saisir le top 10000 à la fois au lieu de 1 à la fois ...

Je ferais quelque chose comme ça dans ma mise à jour

DECLARE @Set TABLE (StagingKey, ID,DATE)
INSERT INTO @Set
UPDATE Staging 
SET InProgress = 1
OUTPUT StagingKey, Staging.ID, Staging.Date
WHERE InProgress = 0
AND StagingID IN (SELECT TOP (100000) StagingKey FROM Staging WHERE inProgress = 0 ORDER BY StagingKey ASC ) --FIFO

DECLARE @Temp 
INSERT INTO @TEMP 
UPDATE [DEST] SET Value = Staging.Value [whatever]
OUTPUT INSERTED.ID, DATE [row identifiers]
FROM [DEST] 
JOIN [STAGING]
JOIN [@SET]; 
INSERT INTO @TEMP 
INSERT [DEST] 
SELECT
OUTPUT INSERT.ID, DATE [row identifiers] 
FROM [STAGING] 
JOIN [@SET] 
LEFT JOIN [DEST]

UPDATE Staging
SET inProgress = NULL
FROM Staging 
JOIN @set
ON @Set.Key = Staging.Key
JOIN @temp
ON @temp.id = @set.ID
AND @temp.date = @set.Date

Vous pourriez probablement exécuter plusieurs tâches en faisant apparaître les lots de mise à jour, et vous auriez besoin d'une tâche distincte exécutant une suppression lente

while exists (inProgress is null) 
delete top (100) from staging where inProgress is null 

pour nettoyer la table intermédiaire.

Alocyte
la source