In SQL Server, c'è un modo per verificare se un gruppo selezionato di righe è bloccato o no?


21

Stiamo tentando di aggiornare / eliminare un numero elevato di record in una tabella con più miliardi di righe. Poiché questa è una tabella popolare, c'è molta attività in diverse sezioni di questa tabella. Qualsiasi attività di aggiornamento / eliminazione di grandi dimensioni viene bloccata per lunghi periodi di tempo (poiché è in attesa di ottenere blocchi su tutte le righe o blocco pagina o blocco tabella) con conseguenti timeout o impiegando più giorni per completare l'attività.

Quindi, stiamo cambiando l'approccio per eliminare piccoli gruppi di righe alla volta. Ma vogliamo verificare se le selezionate (diciamo 100 o 1000 o 2000 righe) sono attualmente bloccate da un processo diverso o meno.

  • In caso contrario, procedere con elimina / aggiorna.
  • Se sono bloccati, passa al gruppo successivo di record.
  • Alla fine, torna all'inizio e prova ad aggiornare / eliminare quelli lasciati fuori.

È fattibile?

Grazie, ToC


2
Hai esaminato READPAST come parte della dichiarazione di eliminazione o NOWAIT (per fallire l'intero gruppo)? Uno di questi potrebbe funzionare per te. msdn.microsoft.com/en-us/library/ms187373.aspx
Sean dice Rimuovi Sara Chipps

@SeanGallardy Non ho preso in considerazione quell'idea, ma ora lo farò. Ma c'è un modo più semplice per verificare se una determinata riga è bloccata o no? Grazie.
ToC,

3
Si potrebbe anche prendere in considerazione LOCK_TIMEOUT ( msdn.microsoft.com/en-us/library/ms189470.aspx ). Ad esempio, questo è il modo in cui sp_whoisactive di Adam Machanic garantisce che la procedura non aspetti troppo a lungo se viene bloccata quando si tenta di raccogliere un piano di esecuzione. È possibile impostare un timeout breve o persino utilizzare un valore pari a 0 ("0 significa non attendere affatto e restituire un messaggio non appena viene rilevato un blocco.") È possibile combinare questo con un TRY / CATCH per rilevare l'errore 1222 ( "Periodo di timeout della richiesta di blocco superato") e passare al batch successivo.
Geoff Patterson,

@gpatterson Approccio interessante. Ci proverò anche io.
ToC,

2
Per rispondere, no non esiste un modo più semplice per vedere se le righe sono bloccate a meno che non sia stato fatto qualcosa di specifico nell'applicazione. Fondamentalmente potresti prima fare una selezione con HOLDLOCK e XLOCK con un set di lock_timeout (che è ciò di cui parla NOWAIT nel mio commento originale, impostando il timeout su 0). Se non lo capisci, sai che qualcosa è bloccato. Non c'è niente di facilmente disponibile da dire "La riga X nella tabella Y usa l'indice Z bloccato da qualcosa". Possiamo vedere se la tabella ha dei blocchi o se pagine / righe / chiavi / ecc hanno dei blocchi, ma tradurlo in righe specifiche in una query non sarebbe facile.
Sean dice Rimuovi Sara Chipps

Risposte:


10

Se capisco correttamente la richiesta, l'obiettivo è eliminare i batch di righe, mentre allo stesso tempo, le operazioni DML si verificano su righe in tutta la tabella. L'obiettivo è eliminare un batch; tuttavia, se tutte le righe sottostanti contenute nell'intervallo definito da detto batch sono bloccate, allora dobbiamo saltare quel batch e passare al batch successivo. Dobbiamo quindi tornare a tutti i batch che non sono stati precedentemente eliminati e riprovare la nostra logica di eliminazione originale. Dobbiamo ripetere questo ciclo fino all'eliminazione di tutti i lotti di righe richiesti.

Come è stato menzionato, è ragionevole utilizzare un suggerimento READPAST e il livello di isolamento READ COMMITTED (predefinito), al fine di saltare gli intervalli che possono contenere righe bloccate. Farò un ulteriore passo e consiglierò di utilizzare il livello di isolamento SERIALIZZABILE e le eliminazioni rosicchianti.

SQL Server utilizza i blocchi di intervallo di chiavi per proteggere un intervallo di righe implicitamente incluse in un set di record letto da un'istruzione Transact-SQL durante l'utilizzo del livello di isolamento delle transazioni serializzabile ... Maggiori informazioni qui: https://technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx

Con le eliminazioni di roditura, il nostro obiettivo è isolare un intervallo di righe e garantire che non si verifichino cambiamenti a tali righe mentre le stiamo eliminando, vale a dire che non vogliamo letture o inserimenti fantasma. Il livello di isolamento serializzabile ha lo scopo di risolvere questo problema.

Prima di dimostrare la mia soluzione, vorrei aggiungere che non sto nemmeno raccomandando di cambiare il livello di isolamento predefinito del database in SERIALIZZABILE, né sto raccomandando che la mia soluzione sia la migliore. Desidero semplicemente presentarlo e vedere dove possiamo andare da qui.

Alcune note di pulizia:

  1. La versione di SQL Server che sto utilizzando è Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
  2. Il mio database di test utilizza il modello di recupero COMPLETO

Per iniziare il mio esperimento, creerò un database di test, una tabella di esempio e riempirò la tabella con 2.000.000 di righe.


USE [master];
GO

SET NOCOUNT ON;

IF DATABASEPROPERTYEX (N'test', N'Version') > 0
BEGIN
    ALTER DATABASE [test] SET SINGLE_USER
        WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [test];
END
GO

-- Create the test database
CREATE DATABASE [test];
GO

-- Set the recovery model to FULL
ALTER DATABASE [test] SET RECOVERY FULL;

-- Create a FULL database backup
-- in order to ensure we are in fact using 
-- the FULL recovery model
-- I pipe it to dev null for simplicity
BACKUP DATABASE [test]
TO DISK = N'nul';
GO

USE [test];
GO

-- Create our table
IF OBJECT_ID('dbo.tbl','U') IS NOT NULL
BEGIN
    DROP TABLE dbo.tbl;
END;
CREATE TABLE dbo.tbl
(
      c1 BIGINT IDENTITY (1,1) NOT NULL
    , c2 INT NOT NULL
) ON [PRIMARY];
GO

-- Insert 2,000,000 rows 
INSERT INTO dbo.tbl
    SELECT TOP 2000
        number
    FROM
        master..spt_values
    ORDER BY 
        number
GO 1000

A questo punto, avremo bisogno di uno o più indici su cui possano agire i meccanismi di blocco del livello di isolamento SERIALIZZABILE.


-- Add a clustered index
CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1
    ON dbo.tbl (c1);
GO

-- Add a non-clustered index
CREATE NONCLUSTERED INDEX IX_tbl_c2 
    ON dbo.tbl (c2);
GO

Ora, controlliamo per vedere che sono state create le nostre 2.000.000 di righe


SELECT
    COUNT(*)
FROM
    tbl;

inserisci qui la descrizione dell'immagine

Quindi, abbiamo il nostro database, tabella, indici e righe. Quindi, impostiamo l'esperimento per le eliminazioni rosicchianti. Innanzitutto, dobbiamo decidere il modo migliore per creare un tipico meccanismo di eliminazione rodibile.


DECLARE
      @BatchSize        INT    = 100
    , @LowestValue      BIGINT = 20000
    , @HighestValue     BIGINT = 20010
    , @DeletedRowsCount BIGINT = 0
    , @RowCount         BIGINT = 1;

SET NOCOUNT ON;
GO

WHILE  @DeletedRowsCount <  ( @HighestValue - @LowestValue ) 
BEGIN

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION

        DELETE 
        FROM
            dbo.tbl 
        WHERE
            c1 IN ( 
                    SELECT TOP (@BatchSize)
                        c1
                    FROM
                        dbo.tbl 
                    WHERE 
                        c1 BETWEEN @LowestValue AND @HighestValue
                    ORDER BY 
                        c1
                  );

        SET @RowCount = ROWCOUNT_BIG();

    COMMIT TRANSACTION;

    SET @DeletedRowsCount += @RowCount;
    WAITFOR DELAY '000:00:00.025';
    CHECKPOINT;

END;

Come puoi vedere, ho inserito la transazione esplicita all'interno del ciclo while. Se desideri limitare i flush dei log, sentiti libero di posizionarlo al di fuori del loop. Inoltre, poiché siamo nel modello di recupero COMPLETO, potresti voler creare backup del registro delle transazioni più spesso durante l'esecuzione delle tue operazioni di eliminazione di roditura, al fine di garantire che il registro delle transazioni possa essere impedito di crescere in modo oltraggioso.

Quindi, ho un paio di obiettivi con questa configurazione. In primo luogo, voglio i miei blocchi di intervallo di chiavi; quindi, provo a mantenere i lotti il ​​più piccoli possibile. Inoltre, non voglio influire negativamente sulla concorrenza sul mio tavolo "gigantesco"; quindi, voglio prendere i miei lucchetti e lasciarli il più velocemente possibile. Quindi, ti consiglio di ridurre le dimensioni dei lotti.

Ora, voglio fornire un breve esempio di questa routine di cancellazione in azione. Dobbiamo aprire una nuova finestra in SSMS ed eliminare una riga dalla nostra tabella. Lo farò all'interno di una transazione implicita utilizzando il livello di isolamento READ COMMITTED predefinito.


DELETE FROM
    dbo.tbl
WHERE
    c1 = 20005;

Questa riga è stata effettivamente eliminata?


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20010;

Sì, è stato cancellato.

Prova di riga eliminata

Ora, per vedere i nostri blocchi, apriamo una nuova finestra in SSMS e aggiungiamo uno o due frammenti di codice. Sto usando sp_whoisactive di Adam Mechanic, che può essere trovato qui: sp_whoisactive


SELECT
    DB_NAME(resource_database_id) AS DatabaseName
  , resource_type
  , request_mode
FROM
    sys.dm_tran_locks
WHERE
    DB_NAME(resource_database_id) = 'test'
    AND resource_type = 'KEY'
ORDER BY
    request_mode;

-- Our insert
sp_lock 55;

-- Our deletions
sp_lock 52;

-- Our active sessions
sp_whoisactive;

Ora siamo pronti per iniziare. In una nuova finestra SSMS, iniziamo una transazione esplicita che tenterà di reinserire una riga che abbiamo eliminato. Allo stesso tempo, spegneremo la nostra operazione di eliminazione sgranocchiante.

Il codice di inserimento:


BEGIN TRANSACTION

    SET IDENTITY_INSERT dbo.tbl ON;

    INSERT  INTO dbo.tbl
            ( c1 , c2 )
    VALUES
            ( 20005 , 1 );

    SET IDENTITY_INSERT dbo.tbl OFF;

--COMMIT TRANSACTION;

Iniziamo entrambe le operazioni a partire dall'inserimento e seguite dalle nostre eliminazioni. Possiamo vedere i blocchi della gamma di chiavi e i blocchi esclusivi.

Gamma e serrature esclusive

L'inserto ha generato questi blocchi:

Inserire le serrature

Il roditore elimina / seleziona tiene questi blocchi:

inserisci qui la descrizione dell'immagine

Il nostro inserto sta bloccando la nostra eliminazione come previsto:

Inserisci blocchi Elimina

Ora, commettiamo la transazione di inserimento e vediamo cosa succede.

Conferma l'eliminazione

E come previsto, tutte le transazioni vengono completate. Ora, dobbiamo verificare se l'inserto era un fantasma o se anche l'operazione di eliminazione lo ha rimosso.


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20015;

In effetti, l'inserto è stato eliminato; quindi, non è stato permesso l'inserimento di alcun fantasma.

Nessun inserto fantasma

Quindi, in conclusione, penso che la vera intenzione di questo esercizio non sia quella di provare a tracciare ogni singola riga, pagina o blocco a livello di tabella e provare a determinare se un elemento di un batch è bloccato e richiederebbe quindi la nostra operazione di cancellazione per aspettare. Questo potrebbe essere stato l'intento degli interroganti; tuttavia, tale compito è erculeo e sostanzialmente impraticabile se non impossibile. Il vero obiettivo è garantire che non si verifichino fenomeni indesiderati una volta che abbiamo isolato la gamma del nostro batch con blocchi di nostra proprietà e quindi precediamo l'eliminazione del batch. Il livello di isolamento SERIALIZZABILE raggiunge questo obiettivo. La chiave è mantenere piccoli gli stuzzichini, controllare il registro delle transazioni ed eliminare i fenomeni indesiderati.

Se vuoi la velocità, non creare tabelle gigantesche profonde che non possono essere partizionate e quindi non puoi usare il cambio di partizione per i risultati più veloci. La chiave per accelerare è il partizionamento e il parallelismo; la chiave della sofferenza sono stuzzichini e live-lock.

Per favore fatemi sapere cosa ne pensate.

Ho creato alcuni ulteriori esempi del livello di isolamento SERIALIZZABILE in azione. Dovrebbero essere disponibili ai link seguenti.

Elimina operazione

Inserisci operazione

Operazioni di uguaglianza: blocchi dell'intervallo di chiavi sui valori chiave successivi

Operazioni sull'uguaglianza - Singleton Fetch of Existent Data

Operazioni sull'uguaglianza - Singleton Fetch di dati inesistenti

Operazioni di disuguaglianza - Blocchi intervallo di chiavi su valori chiave intervallo e successivo


9

Quindi, stiamo cambiando l'approccio per eliminare piccoli gruppi di righe alla volta.

Questa è davvero una buona idea da eliminare in piccoli lotti o blocchi accurati . Vorrei aggiungere un piccolo waitfor delay '00:00:05'e a seconda del modello di recupero del database - se FULL, quindi fare un log backupe se SIMPLEquindi fare un manual CHECKPOINTper evitare gonfiore del registro delle transazioni - tra i batch.

Ma vogliamo verificare se le selezionate (diciamo 100 o 1000 o 2000 righe) sono attualmente bloccate da un processo diverso o meno.

Quello che stai dicendo non è del tutto possibile fuori dalla scatola (tenendo presente i tuoi 3 punti elenco). Se il suggerimento di cui sopra - small batches + waitfor delaynon funziona (purché si eseguano test adeguati), è possibile utilizzare il query HINT.

Non usare NOLOCK- vedi kb / 308886 , Problemi di coerenza di lettura di SQL Server di Itzik Ben-Gan , Mettere NOLOCK ovunque - Di Aaron Bertrand e SQL Server NOLOCK Suggerimento e altre idee sbagliate .

READPASTsuggerimento aiuterà nel tuo scenario. L'essenza del READPASTsuggerimento è: se esiste un blocco a livello di riga, il server SQL non lo leggerà.

Specifica che Motore di database non legge le righe bloccate da altre transazioni. Quando READPASTviene specificato, i blocchi a livello di riga vengono ignorati. Cioè, Motore di database salta le righe invece di bloccare la transazione corrente fino al rilascio dei blocchi.

Durante il mio numero limitato di test, ho trovato davvero un buon rendimento durante l'uso DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)e l'impostazione del livello di isolamento sessione di query a READ COMMITTEDusando SET TRANSACTION ISOLATION LEVEL READ COMMITTEDche è livello di isolamento predefinito in ogni caso.


2

Riassumendo altri approcci originariamente offerti nei commenti alla domanda.


  1. Utilizzare NOWAITse il comportamento desiderato è quello di fallire l'intero blocco non appena si incontra un blocco incompatibile.

    Dalla NOWAITdocumentazione :

    Indica al Motore di database di restituire un messaggio non appena viene rilevato un blocco sulla tabella. NOWAITequivale a specificare SET LOCK_TIMEOUT 0una tabella specifica. Il NOWAITsuggerimento non funziona quando TABLOCKè incluso anche il suggerimento. Per terminare una query senza attendere quando si utilizza il TABLOCKsuggerimento, utilizzare invece la prefazione SETLOCK_TIMEOUT 0;.

  2. Utilizzare SET LOCK_TIMEOUTper ottenere un risultato simile, ma con un timeout configurabile:

    Dalla SET LOCK_TIMEOUTdocumentazione

    Specifica il numero di millisecondi in cui un'istruzione attende il rilascio di un blocco.

    Quando un'attesa per un blocco supera il valore di timeout, viene restituito un errore. Un valore pari a 0 significa non attendere affatto e restituire un messaggio non appena viene rilevato un blocco.


0

Supponiamo di avere 2 query in parallelo:

connect / session 1: bloccherà la riga = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

connect / session 2: ignora la riga bloccata = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

OPPURE connect / session 2: genererà un'eccezione

DECLARE @id integer;
SELECT @id = id FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777;
IF @id is NULL
  THROW 51000, 'Hi, a record is locked or does not exist.', 1;

-1

Prova a filtrare su qualcosa del genere: può diventare complicato se vuoi essere davvero molto specifico. Cerca in BOL la descrizione di sys.dm_tran_locks

SELECT 
tl.request_session_id,
tl.resource_type,
tl.resource_associated_entity_id,
db_name(tl.resource_database_id) 'Database',
CASE 
    WHEN tl.resource_type = 'object' THEN object_name(tl.resource_associated_entity_id, tl.resource_database_id)
    ELSE NULL
END 'LockedObject',
tl.resource_database_id,
tl.resource_description,
tl.request_mode,
tl.request_type,
tl.request_status FROM [sys].[dm_tran_locks] tl WHERE resource_database_id <> 2order by tl.request_session_id

solo curioso - perché il downvote?
rottengeek,

-11

È possibile utilizzare NoLOCK durante l'eliminazione e se le righe sono bloccate, non verranno eliminate. Non è l'ideale ma può fare al caso tuo.

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True

7
Se lo provo sulla mia macchina locale Msg 1065, Level 15, State 1, Line 15 The NOLOCK and READUNCOMMITTED lock hints are not allowed for target tables of INSERT, UPDATE, DELETE or MERGE statements., divento obsoleto dal 2005
Tom V - Team Monica
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.