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:
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
fonte
([TimeStamp] DESC, [InverterID] ASC)
parece uma escolha estranha para o índice agrupado. Eu quero dizer aDESC
parte.Respostas:
Primeiro, embora ligeiramente não relacionado à questão principal, sua
MERGE
declaraçã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: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
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
MERGE
instruçã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: 1
no 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.
fonte
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
Provavelmente, você pode executar vários trabalhos aparecendo os lotes de atualização e precisará de um trabalho separado executando uma exclusão lenta
para limpar a tabela de preparação.
fonte