Esta pergunta está relacionada a este tópico do fórum .
Executando o SQL Server 2008 Developer Edition em minha estação de trabalho e em um cluster de máquina virtual de dois nós do Enterprise Edition, onde me refiro ao "cluster alfa".
O tempo necessário para excluir linhas com uma coluna varbinária (máx) está diretamente relacionado ao comprimento dos dados nessa coluna. Isso pode parecer intuitivo no começo, mas após a investigação, entra em conflito com meu entendimento de como o SQL Server realmente exclui linhas em geral e lida com esse tipo de dados.
O problema decorre de um problema de tempo limite de exclusão (> 30 segundos) que estamos vendo em nosso aplicativo Web .NET, mas eu o simplifiquei para o propósito desta discussão.
Quando um registro é excluído, o SQL Server o marca como um fantasma a ser limpo por uma Tarefa de Limpeza de Fantasma posteriormente, depois que a transação é confirmada (consulte o blog de Paul Randal ). Em um teste para excluir três linhas com dados de 16 KB, 4 MB e 50 MB em uma coluna varbinária (máx), respectivamente, vejo isso acontecendo na página com a parte dos dados em linha, bem como na transação registro.
O que me parece estranho é que os bloqueios X são colocados em todas as páginas de dados LOB durante a exclusão e as páginas são desalocadas no PFS. Eu vejo isso no log de transações, bem como com sp_lock
e os resultados do dm_db_index_operational_stats
DMV ( page_lock_count
).
Isso cria um gargalo de E / S na minha estação de trabalho e em nosso cluster alfa, se essas páginas ainda não estiverem no cache do buffer. De fato, a page_io_latch_wait_in_ms
mesma DMV é praticamente toda a duração da exclusão e page_io_latch_wait_count
corresponde ao número de páginas bloqueadas. Para o arquivo de 50 MB na minha estação de trabalho, isso se traduz em mais de 3 segundos ao iniciar com um cache de buffer vazio ( checkpoint
/ dbcc dropcleanbuffers
), e não tenho dúvida de que seria mais demorado para fragmentação pesada e sob carga.
Tentei me certificar de que não estava apenas alocando espaço no cache, ocupando esse tempo. Li 2 GB de dados de outras linhas antes de executar a exclusão em vez do checkpoint
método, que é mais do que o que é alocado no processo do SQL Server. Não tenho certeza se esse é um teste válido ou não, pois não sei como o SQL Server embaralha os dados. Eu supus que sempre empurraria o velho a favor do novo.
Além disso, ele nem modifica as páginas. Isso eu posso ver com dm_os_buffer_descriptors
. As páginas são limpas após a exclusão, enquanto o número de páginas modificadas é menor que 20 para todas as três exclusões pequenas, médias e grandes. Também comparei a saída de DBCC PAGE
para uma amostra das páginas consultadas e não houve alterações (apenas o ALLOCATED
bit foi removido do PFS). Apenas os desaloca.
Para provar ainda mais que as pesquisas / desalocações da página estão causando o problema, tentei o mesmo teste usando uma coluna de fluxo de arquivos em vez de vanilla varbinary (max). As exclusões eram de tempo constante, independentemente do tamanho do LOB.
Então, primeiro minhas perguntas acadêmicas:
- Por que o SQL Server precisa procurar todas as páginas de dados LOB para bloquear X? Isso é apenas um detalhe de como os bloqueios são representados na memória (armazenados de alguma forma na página)? Isso faz com que o impacto de E / S dependa fortemente do tamanho dos dados, se não for completamente armazenado em cache.
- Por que o X bloqueia, apenas para desalocá-los? Não é suficiente bloquear apenas a folha de índice com a parte em linha, pois a desalocação não precisa modificar as páginas? Existe alguma outra maneira de obter os dados LOB contra os quais o bloqueio protege?
- Por que desalocar as páginas de antemão, já que já existe uma tarefa em segundo plano dedicada a esse tipo de trabalho?
E talvez mais importante, minha pergunta prática:
- Existe alguma maneira de fazer exclusões operar de maneira diferente? Meu objetivo é que o tempo seja excluído independentemente do tamanho, semelhante ao fluxo de arquivos, onde qualquer limpeza ocorre em segundo plano após o fato. É uma coisa de configuração? Estou armazenando coisas estranhamente?
Aqui está como reproduzir o teste descrito (executado através da janela de consulta do SSMS):
CREATE TABLE [T] (
[ID] [uniqueidentifier] NOT NULL PRIMARY KEY,
[Data] [varbinary](max) NULL
)
DECLARE @SmallID uniqueidentifier
DECLARE @MediumID uniqueidentifier
DECLARE @LargeID uniqueidentifier
SELECT @SmallID = NEWID(), @MediumID = NEWID(), @LargeID = NEWID()
-- May want to keep these IDs somewhere so you can use them in the deletes without var declaration
INSERT INTO [T] VALUES (@SmallID, CAST(REPLICATE(CAST('a' AS varchar(max)), 16 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@MediumID, CAST(REPLICATE(CAST('a' AS varchar(max)), 4 * 1024 * 1024) AS varbinary(max)))
INSERT INTO [T] VALUES (@LargeID, CAST(REPLICATE(CAST('a' AS varchar(max)), 50 * 1024 * 1024) AS varbinary(max)))
-- Do this before test
CHECKPOINT
DBCC DROPCLEANBUFFERS
BEGIN TRAN
-- Do one of these deletes to measure results or profile
DELETE FROM [T] WHERE ID = @SmallID
DELETE FROM [T] WHERE ID = @MediumID
DELETE FROM [T] WHERE ID = @LargeID
-- Do this after test
ROLLBACK
Aqui estão alguns resultados da criação de perfil das exclusões na minha estação de trabalho:
| Tipo de coluna | Excluir tamanho | Duração (ms) | Lê | Escreve | CPU -------------------------------------------------- ------------------ | VarBinary | 16 KB | 40 13 2 0 | VarBinary | 4 MB | 952 2318 2 0 | VarBinary | 50 MB | 2976 28594 1 | 62 -------------------------------------------------- ------------------ | FileStream | 16 KB | 1 | 12 1 | 0 | FileStream | 4 MB | 0 9 0 0 | FileStream | 50 MB | 1 | 9 0 0
Em vez disso, não podemos apenas usar o filtro de arquivos porque:
- Nossa distribuição de tamanho de dados não garante isso.
- Na prática, adicionamos dados em vários blocos e o fluxo de arquivos não suporta atualizações parciais. Nós precisaríamos projetar em torno disso.
Atualização 1
Testou uma teoria de que os dados estão sendo gravados no log de transações como parte da exclusão, e isso não parece ser o caso. Estou testando isso incorretamente? Ver abaixo.
SELECT MAX([Current LSN]) FROM fn_dblog(NULL, NULL)
--0000002f:000001d9:0001
BEGIN TRAN
DELETE FROM [T] WHERE ID = @ID
SELECT
SUM(
DATALENGTH([RowLog Contents 0]) +
DATALENGTH([RowLog Contents 1]) +
DATALENGTH([RowLog Contents 3]) +
DATALENGTH([RowLog Contents 4])
) [RowLog Contents Total],
SUM(
DATALENGTH([Log Record])
) [Log Record Total]
FROM fn_dblog(NULL, NULL)
WHERE [Current LSN] > '0000002f:000001d9:0001'
Para um arquivo com mais de 5 MB, isso retornou 1651 | 171860
.
Além disso, eu esperaria que as páginas estivessem sujas se os dados fossem gravados no log. Apenas as desalocações parecem estar registradas, o que corresponde ao que está sujo após a exclusão.
Atualização 2
Recebi uma resposta de Paul Randal. Ele afirmou que precisa ler todas as páginas para percorrer a árvore e encontrar quais páginas desalocar e afirmou que não há outra maneira de procurar quais páginas. Esta é uma meia resposta para 1 e 2 (embora não explique a necessidade de bloqueios em dados fora de linha, mas isso é pequeno).
A pergunta 3 ainda está aberta: por que desalocar as páginas com antecedência se já existe uma tarefa em segundo plano para limpar as exclusões?
E, claro, a questão mais importante: existe uma maneira de mitigar diretamente (ou seja, não contornar) esse comportamento de exclusão dependente do tamanho? Eu acho que esse seria um problema mais comum, a menos que realmente sejamos os únicos a armazenar e excluir linhas de 50 MB no SQL Server? Todo mundo lá fora resolve isso com algum tipo de trabalho de coleta de lixo?