No Sql Server, existe uma maneira de verificar se um grupo selecionado de linhas está bloqueado ou não?

21

Estamos tentando atualizar / excluir um grande número de registros em uma tabela de bilhões de linhas. Como essa é uma tabela popular, há muita atividade em diferentes seções desta tabela. Qualquer atividade grande de atualização / exclusão está sendo bloqueada por longos períodos de tempo (enquanto aguarda para obter bloqueios em todas as linhas ou bloqueio de página ou bloqueio de tabela), resultando em tempos limite ou em vários dias para concluir a tarefa.

Portanto, estamos mudando a abordagem para excluir um pequeno lote de linhas de cada vez. Mas queremos verificar se os selecionados (digamos 100 ou 1000 ou 2000 linhas) estão atualmente bloqueados por um processo diferente ou não.

  • Caso contrário, continue com a exclusão / atualização.
  • Se eles estiverem bloqueados, passe para o próximo grupo de registros.
  • No final, volte ao início e tente atualizar / excluir os excluídos.

Isso é factível?

Obrigado, ToC

ToC
fonte
2
Você analisou o READPAST como parte da instrução delete ou NOWAIT (para falhar em todo o grupo)? Um deles pode funcionar para você. msdn.microsoft.com/en-us/library/ms187373.aspx
Sean diz Remover Sara Chipps
@SeanGallardy Eu não considerei essa idéia, mas agora vou. Mas existe uma maneira mais fácil de verificar se uma linha específica está bloqueada ou não? Obrigado.
ToC
3
Você também pode procurar em LOCK_TIMEOUT ( msdn.microsoft.com/en-us/library/ms189470.aspx ). Por exemplo, é dessa maneira que o sp_whoisactive de Adam Machanic garante que o procedimento não espere muito tempo se estiver bloqueado ao tentar reunir um plano de execução. Você pode definir um tempo limite curto ou até usar um valor 0 ("0 significa não esperar e retornar uma mensagem assim que um bloqueio for encontrado.") Você pode combinar isso com uma tentativa / captura para capturar o erro 1222 ( "Período de tempo limite da solicitação de bloqueio excedido") e prossiga para o próximo lote.
Geoff Patterson
@gpatterson Abordagem interessante. Eu vou tentar isso também.
ToC
2
Para responder, não, não há uma maneira mais fácil de ver se as linhas estão bloqueadas, a menos que algo seja feito especificamente no aplicativo. Basicamente, você pode primeiro fazer uma seleção com HOLDLOCK e XLOCK com um conjunto lock_timeout (que é o que NOWAIT no meu comentário original trata, definindo o tempo limite como 0). Se você não conseguir, você sabe que algo está bloqueado. Não há nada facilmente disponível para dizer "A linha X na Tabela Y usando o Índice Z está bloqueada por alguma coisa". Podemos ver se a tabela possui bloqueios ou se todas as páginas / linhas / chaves / etc têm bloqueios, mas traduzir isso em linhas específicas de uma consulta não seria fácil.
Sean diz Remover Sara Chipps

Respostas:

10

Se eu entendi a solicitação corretamente, o objetivo é excluir lotes de linhas, enquanto, ao mesmo tempo, operações DML estão ocorrendo nas linhas da tabela. O objetivo é excluir um lote; no entanto, se alguma linha subjacente contida no intervalo definido pelo referido lote estiver bloqueada, devemos pular esse lote e passar para o próximo lote. Devemos então retornar a qualquer lote que não tenha sido excluído anteriormente e tentar novamente nossa lógica de exclusão original. Devemos repetir esse ciclo até que todos os lotes necessários de linhas sejam excluídos.

Como foi mencionado, é razoável usar uma dica READPAST e o nível de isolamento READ COMMITTED (padrão), para ignorar os intervalos anteriores que podem conter linhas bloqueadas. Vou dar um passo adiante e recomendo o uso do nível de isolamento SERIALIZABLE e exclusões de mordiscar.

O SQL Server usa bloqueios de intervalo de chave para proteger um intervalo de linhas implicitamente incluídas em um conjunto de registros que está sendo lido por uma instrução Transact-SQL ao usar o nível de isolamento de transação serializável ... saiba mais aqui: https://technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx

Com exclusões de mordiscar, nosso objetivo é isolar um intervalo de linhas e garantir que nenhuma alteração ocorra nessas linhas enquanto as excluirmos, ou seja, não queremos leituras ou inserções fantasmas. O nível de isolamento serializável visa solucionar esse problema.

Antes de demonstrar minha solução, gostaria de acrescentar que não estou recomendando mudar o nível de isolamento padrão do seu banco de dados para SERIALIZABLE nem estou recomendando que minha solução seja a melhor. Eu apenas desejo apresentá-lo e ver para onde podemos ir daqui.

Algumas notas de manutenção:

  1. A versão do SQL Server que estou usando é o Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
  2. Meu banco de dados de teste está usando o modelo de recuperação COMPLETO

Para iniciar meu experimento, configurarei um banco de dados de teste, uma tabela de amostra e preencherei a tabela com 2.000.000 de linhas.


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

Nesse ponto, precisaremos de um ou mais índices nos quais os mecanismos de bloqueio do nível de isolamento SERIALIZABLE possam atuar.


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

Agora, vamos verificar se nossas 2.000.000 de linhas foram criadas


SELECT
    COUNT(*)
FROM
    tbl;

insira a descrição da imagem aqui

Portanto, temos nosso banco de dados, tabela, índices e linhas. Então, vamos configurar o experimento para excluir petiscos. Primeiro, devemos decidir qual a melhor maneira de criar um mecanismo típico de exclusão.


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;

Como você pode ver, coloquei a transação explícita dentro do loop while. Se você deseja limitar as liberações de log, sinta-se à vontade para colocá-lo fora do loop. Além disso, como estamos no modelo de recuperação COMPLETO, convém criar backups de log de transações com mais frequência ao executar suas operações de exclusão, a fim de garantir que seu log de transações possa ser impedido de crescer escandalosamente.

Então, eu tenho alguns objetivos com essa configuração. Primeiro, quero meus bloqueios no intervalo de teclas; então, tento manter os lotes o menor possível. Eu também não quero impactar negativamente a concorrência na minha mesa "gigantesca"; então, quero pegar meus bloqueios e deixá-los o mais rápido possível. Portanto, recomendo que você diminua o tamanho do lote.

Agora, quero fornecer um exemplo muito curto dessa rotina de exclusão em ação. Devemos abrir uma nova janela no SSMS e excluir uma linha da nossa tabela. Farei isso dentro de uma transação implícita usando o nível de isolamento READ COMMITTED padrão.


DELETE FROM
    dbo.tbl
WHERE
    c1 = 20005;

Esta linha foi realmente excluída?


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

Sim, foi excluído.

Prova de linha excluída

Agora, para ver nossos bloqueios, vamos abrir uma nova janela no SSMS e adicionar um trecho de código ou dois. Estou usando o sp_whoisactive do Adam Mechanic, que pode ser encontrado aqui: 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;

Agora, estamos prontos para começar. Em uma nova janela do SSMS, vamos começar uma transação explícita que tentará reinserir a linha que excluímos. Ao mesmo tempo, iniciaremos nossa operação de exclusão mordisada.

O código de inserção:


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;

Vamos iniciar as duas operações começando com a inserção e seguidas pelas nossas exclusões. Podemos ver os bloqueios de faixa de chave e bloqueios exclusivos.

Fechaduras de gama e eXclusive

A inserção gerou esses bloqueios:

Fechaduras de inserção

A exclusão / seleção mordisca está mantendo estes bloqueios:

insira a descrição da imagem aqui

Nossa inserção está bloqueando nossa exclusão conforme o esperado:

Inserir blocos Excluir

Agora, vamos confirmar a transação de inserção e ver o que está acontecendo.

Confirmar a exclusão

E, como esperado, todas as transações são concluídas. Agora, devemos verificar se a inserção era um fantasma ou se a operação de exclusão também a removeu.


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

De fato, a inserção foi excluída; portanto, nenhuma inserção fantasma foi permitida.

Nenhuma inserção fantasma

Portanto, concluindo, acho que a verdadeira intenção deste exercício não é tentar rastrear todas as linhas, páginas ou bloqueios em nível de tabela e tentar determinar se um elemento de um lote está bloqueado e, portanto, exigiria que nossa operação de exclusão fosse executada. esperar. Essa pode ter sido a intenção dos questionadores; no entanto, essa tarefa é hercúlea e basicamente impraticável, se não impossível. O objetivo real é garantir que nenhum fenômeno indesejado ocorra depois que isolarmos o intervalo do nosso lote com bloqueios próprios e, em seguida, precedermos a exclusão do lote. O nível de isolamento SERIALIZABLE atinge esse objetivo. A chave é manter pequenos petiscos, controlar o log de transações e eliminar fenômenos indesejados.

Se você deseja velocidade, não crie tabelas gigantes profundas que não possam ser particionadas e, portanto, não consiga usar a alternância de partições para obter os resultados mais rápidos. A chave da velocidade é particionamento e paralelismo; a chave do sofrimento é mordiscar e travar a vida.

Por favor, deixe-me saber o que você pensa.

Criei mais alguns exemplos do nível de isolamento SERIALIZABLE em ação. Eles devem estar disponíveis nos links abaixo.

Excluir operação

Inserir operação

Operações de igualdade - bloqueios de intervalo de chave nos próximos valores-chave

Operações de Igualdade - Busca Singleton de Dados Existentes

Operações de igualdade - busca singleton de dados inexistentes

Operações de desigualdade - bloqueios de intervalo de chave no intervalo e nos próximos valores-chave

ooutwire
fonte
9

Portanto, estamos mudando a abordagem para excluir um pequeno lote de linhas de cada vez.

É realmente uma boa idéia excluir em pequenos lotes ou pedaços cuidadosos . Eu adicionaria um pequeno waitfor delay '00:00:05'e, dependendo do modelo de recuperação do banco de dados - se FULL, faça um log backupe se SIMPLEfizer um manual CHECKPOINTpara evitar inchaço no log de transações - entre lotes.

Mas queremos verificar se os selecionados (digamos 100 ou 1000 ou 2000 linhas) estão atualmente bloqueados por um processo diferente ou não.

O que você está dizendo não é totalmente possível fora da caixa (tendo em mente seus três pontos). Se a sugestão acima - small batches + waitfor delaynão funcionar (desde que você faça o teste adequado), poderá usar o query HINT.

Não use NOLOCK- consulte kb / 308886 , Problemas de consistência de leitura do SQL Server por Itzik Ben-Gan , colocando o NOLOCK em toda parte - Por Aaron Bertrand e NOLOCK Hint do SQL Server e outras idéias ruins .

READPASTdica vai ajudar no seu cenário. A essência da READPASTdica é - se houver um bloqueio no nível da linha, o SQL Server não o lerá.

Especifica que o Mecanismo de Banco de Dados não lê linhas bloqueadas por outras transações. Quando READPASTé especificado, os bloqueios no nível da linha são ignorados. Ou seja, o Mecanismo de Banco de Dados ignora as linhas em vez de bloquear a transação atual até que os bloqueios sejam liberados.

Durante meus testes limitados, achei uma taxa de transferência realmente boa ao usar DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)e definir o nível de isolamento da sessão de consulta para READ COMMITTEDusar SET TRANSACTION ISOLATION LEVEL READ COMMITTEDqual é o nível de isolamento padrão de qualquer maneira.

Kin Shah
fonte
2

Resumindo outras abordagens originalmente oferecidas nos comentários à pergunta.


  1. Use NOWAITse o comportamento desejado falhar em todo o bloco assim que um bloqueio incompatível for encontrado.

    A partir da NOWAITdocumentação :

    Instrui o Mecanismo de Banco de Dados a retornar uma mensagem assim que um bloqueio for encontrado na tabela. NOWAITé equivalente a especificar SET LOCK_TIMEOUT 0para uma tabela específica. A NOWAITdica não funciona quando a TABLOCKdica também está incluída. Para encerrar uma consulta sem aguardar ao usar a TABLOCKdica, anteceda a consulta com SETLOCK_TIMEOUT 0;.

  2. Use SET LOCK_TIMEOUTpara obter um resultado semelhante, mas com um tempo limite configurável:

    A partir da SET LOCK_TIMEOUTdocumentação

    Especifica o número de milissegundos que uma instrução aguarda até que um bloqueio seja liberado.

    Quando uma espera por um bloqueio excede o valor do tempo limite, um erro é retornado. Um valor 0 significa não esperar e retornar uma mensagem assim que um bloqueio for encontrado.

Paul White diz que a GoFundMonica
fonte
0

Para assumir que temos duas consultas paralelas:

conectar / sessão 1: bloqueará a linha = 777

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

connect / session 2: ignorará a linha bloqueada = 777

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

OU conectar / sessão 2: lançará uma exceção

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;
Lebnik
fonte
-1

Tente filtrar algo assim - pode ficar complicado se você quiser ser realmente, realmente específico. Procure na BOL a descrição de 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
rottengeek
fonte
apenas curioso - por que o voto negativo?
Rottengeek # 11/15
-11

Você pode usar o NoLOCK enquanto estiver excluindo e, se as linhas estiverem bloqueadas, elas não serão excluídas. Não é o ideal, mas pode fazer o truque para você.

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True
mouliin
fonte
7
Se eu tentar isso na minha máquina local 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., ficar obsoleto desde 2005
Tom V - Team Monica