INSERTI voluminosi che bloccano le SELECT


14

Ho un problema con una grande quantità di INSERT che stanno bloccando le mie operazioni SELECT.

Schema

Ho un tavolo come questo:

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)
)

Ho anche questa piccola procedura di supporto, che mi consente di inserire o aggiornare (aggiornamento in caso di conflitto) con il comando 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

uso

Ora ho eseguito istanze di servizio su più server che eseguono enormi aggiornamenti chiamando [InsertOrUpdateInverterData]rapidamente la procedura.

Esiste anche un sito Web che esegue query SELECT sulla [InverterData]tabella.

Problema

Se eseguo SELEZIONA le query sul [InverterData]tavolo, vengono eseguite in diversi intervalli di tempo, a seconda dell'utilizzo di INSERT delle mie istanze del servizio. Se metto in pausa tutte le istanze del servizio, SELECT è estremamente veloce, se l'istanza esegue un inserimento rapido, le SELECT diventano molto lente o addirittura un annullamento del timeout.

tentativi

Ho fatto alcuni SELECT sul [sys.dm_tran_locks]tavolo per trovare processi di blocco, come questo

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

Questo è il risultato:

inserisci qui la descrizione dell'immagine

S = Condiviso. Alla sessione di mantenimento è concesso l'accesso condiviso alla risorsa.

Domanda

Perché i SELECT sono bloccati dalla [InsertOrUpdateInverterData]procedura che utilizza solo i comandi MERGE?

Devo usare una sorta di transazione con modalità di isolamento definita all'interno di [InsertOrUpdateInverterData]?

Aggiornamento 1 (correlato alla domanda di @Paul)

Basati sul reporting interno del server MS-SQL sulle [InsertOrUpdateInverterData]seguenti statistiche:

  • Tempo medio CPU: 0,12 ms
  • Processi di lettura medi: 5,76 per / s
  • Processi di scrittura medi: 0,4 per / s

Sulla base di questo sembra che il comando MERGE sia principalmente occupato con operazioni di lettura che bloccheranno la tabella! (?)

Aggiornamento 2 (correlato alla domanda di @Paul)

La [InverterData]tabella contiene le seguenti statistiche di archiviazione:

  • Spazio dati: 26.901,86 MB
  • Numero di righe: 131.827.749
  • Partizionato: vero
  • Conteggio partizioni: 62

Ecco il set di risultati sp_WhoIsActive (quasi tutti) completo :

SELECT comando

  • gg hh: mm: ss.mss: 00 00: 01: 01.930
  • session_id: 73
  • wait_info: (12629ms) LCK_M_S
  • CPU: 198
  • blocking_session_id: 146
  • legge: 99.368
  • scrive: 0
  • stato: sospeso
  • open_tran_count: 0

[InsertOrUpdateInverterData]Comando di blocco

  • gg hh: mm: ss.mss: 00 00: 00: 00.330
  • session_id: 146
  • wait_info: NULL
  • CPU: 3.972
  • blocking_session_id: NULL
  • legge: 376,95
  • scrive: 126
  • stato: dormire
  • open_tran_count: 1

I ([TimeStamp] DESC, [InverterID] ASC)si presenta come una scelta strana per l'indice cluster. Intendo la DESCparte.
ypercubeᵀᴹ

Capisco il tuo punto: l'indice cluster DESC che inserisce i dati forzerebbe la ricostruzione della tabella piuttosto aggiungendo alla fine ... performance dog; bloccherebbe il tavolo mentre la ricostruzione avviene ... sì. di Jove, ce l'hai. La struttura è causa di blocco più di serrature.
Alocyte,

Risposte:


12

In primo luogo, sebbene leggermente non correlato alla domanda principale, la tua MERGEdichiarazione è potenzialmente a rischio di errori a causa di una condizione di gara . Il problema, in breve, è che è possibile che più thread simultanei concludano che la riga di destinazione non esiste, causando tentativi di inserimento da scontrarsi. La causa principale è che non è possibile eseguire un blocco condiviso o aggiornato su una riga che non esiste. La soluzione è aggiungere un suggerimento:

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

Il suggerimento del livello di isolamento serializzabile assicura che l'intervallo di tasti in cui andrebbe la riga sia bloccato. Hai un indice univoco per supportare il blocco della distanza, quindi questo suggerimento non avrà un effetto negativo sul blocco, otterrai semplicemente protezione da questa potenziale condizione di gara.

Domanda principale

Perché vengono SELECTsbloccati dalla procedura [InsertOrUpdateInverterData] che utilizza solo i MERGEcomandi?

Sotto il livello di isolamento di commit della lettura di blocco predefinito, i blocchi condivisi (S) vengono acquisiti durante la lettura dei dati e in genere (sebbene non sempre) rilasciati subito dopo il completamento della lettura. Alcuni blocchi condivisi vengono mantenuti alla fine dell'istruzione.

A MERGEdati modifica i economico, così acquisirà S o aggiornamento (U) blocca quando si individua i dati da modificare, che vengono convertiti in esclusivo (X) serrature poco prima di eseguire la modifica effettiva. I blocchi U e X devono essere mantenuti fino alla fine della transazione.

Ciò è vero sotto tutti i livelli di isolamento, ad eccezione dell'isolamento "ottimistico" dello snapshot (SI), da non confondere con il versioning read commit, noto anche come read commit snapshot isolation (RCSI).

Nulla nella tua domanda mostra una sessione in attesa che un blocco S sia bloccato da una sessione con un blocco U. Questi blocchi sono compatibili . Qualsiasi blocco è quasi certamente causato dal blocco su un blocco X bloccato. Questo può essere un po 'complicato da catturare quando un gran numero di blocchi a breve termine viene acquisito, convertito e rilasciato in un breve intervallo di tempo.

Il open_tran_count: 1sul comando InsertOrUpdateInverterData vale la pena indagare. Sebbene il comando non sia stato eseguito da molto tempo, è necessario verificare che non si disponga di una transazione contenente (nell'applicazione o nella procedura memorizzata di livello superiore) inutilmente lunga. La migliore pratica è di mantenere le transazioni il più brevi possibile. Questo potrebbe non essere nulla, ma dovresti assolutamente verificare.

Soluzione potenziale

Come Kin ha suggerito in un commento, è possibile cercare di abilitare un livello di isolamento di versione delle righe (RCSI o SI) su questo database. RCSI è il più utilizzato, poiché in genere non richiede altrettante modifiche alle applicazioni. Una volta abilitato, il livello di isolamento di commit della lettura predefinito utilizza le versioni di riga anziché prendere i blocchi S per le letture, quindi il blocco SX viene ridotto o eliminato. Alcune operazioni (ad es. Controlli di chiave esterna) acquisiscono ancora i blocchi S in RCSI.

Tenere presente, tuttavia, che le versioni delle righe consumano spazio tempdb, in linea di massima proporzionale al tasso di attività di modifica e alla lunghezza delle transazioni. Dovrai testare accuratamente l' implementazione sotto carico per capire e pianificare l'impatto di RCSI (o SI) nel tuo caso.

Se si desidera localizzare l'utilizzo del controllo delle versioni, anziché abilitarlo per l'intero carico di lavoro, SI potrebbe comunque essere una scelta migliore. Utilizzando SI per le transazioni di lettura, si eviterà la contesa tra lettori e scrittori, a spese dei lettori che vedono la versione della riga prima dell'inizio di qualsiasi modifica simultanea (più correttamente, l'operazione di lettura in SI vedrà sempre lo stato di commit di la riga al momento dell'inizio della transazione SI). L'utilizzo di SI per le transazioni di scrittura comporta scarsi o nulli vantaggi, poiché i blocchi di scrittura verranno comunque adottati e sarà necessario gestire eventuali conflitti di scrittura. A meno che non sia quello che vuoi :)

Nota: a differenza di RCSI (che una volta abilitato si applica a tutte le transazioni in esecuzione in commit in lettura), SI deve essere esplicitamente richiesto utilizzando SET TRANSACTION ISOLATION SNAPSHOT;.

I comportamenti impercettibili che dipendono dai lettori che bloccano gli scrittori (incluso nel codice trigger!) Rendono i test essenziali. Vedi la mia serie di articoli collegati e Libri online per i dettagli. Se decidi su RCSI, assicurati di rivedere le modifiche ai dati in particolare in Leggi isolamento dello snapshot in lettura .

Infine, è necessario assicurarsi che l'istanza sia patchata con SQL Server 2008 Service Pack 4.


0

Umilmente, non userei l'unione. Andrei con IF Exists (UPDATE) ELSE (INSERT) - hai una chiave cluster con le due colonne che stai usando per identificare le righe, quindi è un test facile.

Hai citato inserti MASSIVI eppure fai 1 per 1 ... hai pensato di raggruppare i dati in una tabella di gestione temporanea e di utilizzare i dati SQL POWER OVERWHELMING per impostare più di 1 aggiornamento / inserimento alla volta? Come fare un test di routine per il contenuto nella tabella di gestione temporanea e afferrare i primi 10000 alla volta invece di 1 alla volta ...

Farei una cosa del genere nel mio aggiornamento

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

Probabilmente potresti eseguire più lavori facendo scoppiare i batch di aggiornamento e avresti bisogno di un lavoro separato che esegua una cancellazione di mantenimento

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

per pulire la tabella di gestione temporanea.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.