INSERTs massivos bloqueando SELECTs

14

Estou com um problema com uma quantidade enorme de INSERTs que estão bloqueando minhas operações SELECT.

Esquema

Eu tenho uma tabela como esta:

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

Eu também tenho esse pequeno procedimento auxiliar, que me permite inserir ou atualizar (atualização em conflito) com o 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

Agora, executei instâncias de serviço em vários servidores que executam atualizações massivas chamando o [InsertOrUpdateInverterData]procedimento rapidamente.

Há também um site que faz consultas SELECT na [InverterData]tabela.

Problema

Se eu fizer consultas SELECT na [InverterData]tabela, elas serão realizadas em intervalos de tempo diferentes, dependendo do uso INSERT das minhas instâncias de serviço. Se eu pausar todas as instâncias de serviço, o SELECT é extremamente rápido, se a instância executar uma inserção rápida, os SELECTs ficarão muito lentos ou até um cancelamento de tempo limite.

Tentativas

Concluí alguns SELECTs na [sys.dm_tran_locks]tabela para encontrar processos de bloqueio, como este

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

Este é o resultado:

insira a descrição da imagem aqui

S = compartilhado. A sessão de espera tem acesso compartilhado ao recurso.

Questão

Por que os SELECTs estão bloqueados pelo [InsertOrUpdateInverterData]procedimento que está usando apenas os comandos MERGE?

Preciso usar algum tipo de transação com o modo de isolamento definido dentro de [InsertOrUpdateInverterData]?

Atualização 1 (relacionada à pergunta de @Paul)

Baseie-se nos relatórios internos do servidor MS-SQL sobre as [InsertOrUpdateInverterData]seguintes estatísticas:

  • Tempo médio da CPU: 0,12ms
  • Processos médios de leitura: 5,76 por / s
  • Processos médios de gravação: 0,4 por / s

Com base nisso, parece que o comando MERGE está ocupado principalmente com operações de leitura que bloquearão a tabela! (?)

Atualização 2 (relacionada à pergunta de @Paul)

A [InverterData]tabela possui as seguintes estatísticas de armazenamento:

  • Espaço de dados: 26.901,86 MB
  • Contagem de linhas: 131,827,749
  • Particionado: true
  • Contagem de partições: 62

Aqui está o conjunto de resultados sp_WhoIsActive ( máximo ) completo :

SELECT comando

  • dd hh: mm: ss.mss: 00 00: 01: 01.930
  • session_id: 73
  • wait_info: (12629ms) LCK_M_S
  • CPU: 198
  • blocking_session_id: 146
  • lê: 99.368
  • escreve: 0
  • status: suspenso
  • open_tran_count: 0

[InsertOrUpdateInverterData]Comando de bloqueio

  • dd hh: mm: ss.mss: 00 00: 00: 00.330
  • session_id: 146
  • wait_info: NULL
  • CPU: 3.972
  • blocking_session_id: NULL
  • leituras: 376,95
  • escreve: 126
  • status: dormindo
  • open_tran_count: 1
Steffen Mangold
fonte
Os ([TimeStamp] DESC, [InverterID] ASC)parece uma escolha estranha para o índice agrupado. Eu quero dizer a DESCparte.
precisa saber é o seguinte
Entendo o seu ponto: o índice agrupado DESC, inserindo dados, forçaria a reconstrução da tabela em vez de anexar ao final ... performance dog; trava a mesa enquanto a reconstrução acontece ... sim. por Jove, você tem. A estrutura é motivo para bloquear mais do que bloqueios.
Alocyte

Respostas:

12

Primeiro, embora ligeiramente não relacionado à questão principal, sua MERGEdeclaração está potencialmente em risco de erros devido a uma condição de corrida . O problema, em poucas palavras, é que é possível que vários threads simultâneos concluam que a linha de destino não existe, resultando em tentativas de inserção em colisão. A causa principal é que não é possível obter um bloqueio compartilhado ou de atualização em uma linha que não existe. A solução é adicionar uma dica:

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

A dica do nível de isolamento serializável garante que o intervalo de teclas para onde a linha iria esteja bloqueado. Você tem um índice exclusivo para suportar o bloqueio de faixa, portanto, essa dica não terá um efeito adverso sobre o bloqueio, você simplesmente obterá proteção contra essa condição de corrida em potencial.

Pergunta principal

Por que os SELECTsbloqueados pelo procedimento [InsertOrUpdateInverterData] que está usando apenas MERGEcomandos?

No nível de isolamento de confirmação confirmada de bloqueio de leitura, os bloqueios compartilhados (S) são obtidos ao ler dados e normalmente (embora nem sempre) são liberados logo após a conclusão da leitura. Alguns bloqueios compartilhados são mantidos até o final da instrução.

Uma MERGEinstrução modifica os dados, para adquirir bloqueios S ou atualizar (U) ao localizar os dados a serem alterados, que são convertidos em bloqueios exclusivos (X) imediatamente antes de realizar a modificação real. Os bloqueios U e X devem ser mantidos até o final da transação.

Isso é verdade em todos os níveis de isolamento, exceto no isolamento 'otimista' de instantâneo (SI), que não deve ser confundido com o controle de versão confirmado, também conhecido como isolamento de instantâneo confirmado por leitura (RCSI).

Nada na sua pergunta mostra uma sessão aguardando um bloqueio S ser bloqueado por uma sessão segurando um bloqueio U. Esses bloqueios são compatíveis . É quase certo que qualquer bloqueio seja causado pelo bloqueio de um bloqueio X retido. Isso pode ser um pouco complicado de capturar quando um grande número de bloqueios de curto prazo está sendo obtido, convertido e liberado em um curto intervalo de tempo.

O open_tran_count: 1no comando InsertOrUpdateInverterData vale a pena investigar. Embora o comando não esteja sendo executado por muito tempo, verifique se você não possui uma transação contendo (no aplicativo ou procedimento armazenado de nível superior) que é desnecessariamente longo. A melhor prática é manter as transações o mais curtas possível. Isso pode não ser nada, mas você deve definitivamente verificar.

Solução potencial

Como Kin sugeriu em um comentário, você pode procurar habilitar um nível de isolamento de versão de linha (RCSI ou SI) nesse banco de dados. O RCSI é o mais usado, pois geralmente não requer tantas alterações no aplicativo. Depois de ativado, o nível de isolamento confirmado por leitura padrão usa versões de linha em vez de usar bloqueios S para leituras, para que o bloqueio do SX seja reduzido ou eliminado. Algumas operações (por exemplo, verificações de chave estrangeira) ainda adquirem bloqueios S sob RCSI.

Esteja ciente de que as versões de linha consomem espaço tempdb, de maneira geral proporcional à taxa de atividade de alteração e à duração das transações. Você precisará testar sua implementação completamente sob carga para entender e planejar o impacto do RCSI (ou SI) no seu caso.

Se você deseja localizar o uso do controle de versão, em vez de ativá-lo para toda a carga de trabalho, o SI ainda pode ser uma escolha melhor. Ao usar o SI nas transações de leitura, você evitará a disputa entre leitores e gravadores, à custa dos leitores que veem a versão da linha antes do início de qualquer modificação simultânea (mais corretamente, a operação de leitura no SI sempre verá o estado comprometido de a linha no momento em que a transação do SI foi iniciada). Há pouco ou nenhum benefício em usar o SI nas transações de gravação, porque os bloqueios de gravação ainda serão executados e você precisará lidar com qualquer conflito de gravação. A menos que seja isso que você deseja :)

Nota: Ao contrário do RCSI (que uma vez ativado se aplica a todas as transações executadas na leitura confirmada), o SI deve ser solicitado explicitamente usando SET TRANSACTION ISOLATION SNAPSHOT;.

Comportamentos sutis que dependem de leitores bloqueando gravadores (inclusive no código de gatilho!) Tornam o teste essencial. Veja minha série de artigos vinculados e o Books Online para obter detalhes. Se você decidir sobre o RCSI, revise as Modificações de dados em Ler isolamento de instantâneo confirmado, em particular.

Por fim, verifique se sua instância está corrigida no SQL Server 2008 Service Pack 4.

Paul White 9
fonte
0

Humildemente, eu não usaria mesclagem. Eu iria com IF Exists (UPDATE) ELSE (INSERT) - você tem uma chave de cluster com as duas colunas que está usando para identificar as linhas, por isso é um teste fácil.

Você menciona inserções MASSIVE e ainda faz 1 por 1 ... pensou em agrupar os dados em uma tabela intermediária e em usar o conjunto de dados POWER POWER OVERWHELMING SQL para fazer mais de uma atualização / inserção por vez? Por exemplo, faça um teste de rotina para verificar o conteúdo na tabela de preparação e obtenha os 10000 melhores de cada vez, em vez de 1 por vez ...

Eu faria algo assim na minha atualização

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

Provavelmente, você pode executar vários trabalhos aparecendo os lotes de atualização e precisará de um trabalho separado executando uma exclusão lenta

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

para limpar a tabela de preparação.

Alócito
fonte