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:
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
la source
([TimeStamp] DESC, [InverterID] ASC)
ressemble à un choix étrange pour l'index clusterisé. Je veux dire laDESC
partie.Réponses:
Tout d'abord, bien que légèrement sans rapport avec la question principale, votre
MERGE
dé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: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
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
MERGE
instruction 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: 1
commande 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.
la source
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
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
pour nettoyer la table intermédiaire.
la source