O operador de spool ansioso é útil para esta exclusão de um columnstore clusterizado?

28

Estou testando a exclusão de dados de um índice columnstore clusterizado.

Notei que há um grande operador de spool no plano de execução:

insira a descrição da imagem aqui

Isso é concluído com as seguintes características:

  • 60 milhões de linhas excluídas
  • 1.9 GiB TempDB usado
  • 14 minutos de tempo de execução
  • Plano serial
  • 1 religação no carretel
  • Custo estimado da digitalização: 364.821

Se eu levar o estimador a subestimar, recebo um plano mais rápido que evita o uso do TempDB:

insira a descrição da imagem aqui

Custo estimado da digitalização: 56.901

(Este é um plano estimado, mas os números nos comentários estão corretos.)

Curiosamente, o spool desaparece novamente se eu liberar as lojas delta executando o seguinte:

ALTER INDEX IX_Clustered ON Fact.RecordedMetricsDetail REORGANIZE WITH (COMPRESS_ALL_ROW_GROUPS = ON);

O spool parece ser introduzido apenas quando há mais do que um limite de páginas nos armazenamentos delta.

Para verificar o tamanho das lojas delta, estou executando a seguinte consulta para verificar se há páginas em linha da tabela:

SELECT  
  SUM([in_row_used_page_count]) AS in_row_used_pages,
  SUM(in_row_data_page_count) AS in_row_data_pages
FROM sys.[dm_db_partition_stats] as pstats
JOIN sys.partitions AS p
ON pstats.partition_id = p.partition_id
WHERE p.[object_id] = OBJECT_ID('Fact.RecordedMetricsDetail');

Existe algum benefício plausível para o iterador de spool no primeiro plano? Devo assumir que ele pretende ser um aprimoramento de desempenho e não para a proteção do dia das bruxas, porque sua presença não é consistente.

Estou testando isso no 2016 CTP 3.1, mas vejo o mesmo comportamento no 2014 SP1 CU3.

Publiquei um script que gera esquema e dados e orienta você na demonstração do problema aqui .

A questão é principalmente por curiosidade sobre o comportamento do otimizador neste momento, pois eu tenho uma solução alternativa para o problema que levou à pergunta (um tempDB cheio de um spool grande). Agora estou excluindo usando a alternância de partições.

James L
fonte
2
Se eu tentar, OPTION (QUERYRULEOFF EnforceHPandAccCard)o carretel desaparece. Presumo que a HP possa ser "Proteção do Dia das Bruxas". No entanto, a tentativa de usar esse plano com uma USE PLANdica falha (assim como a tentativa de usar o plano OPTIMIZE FOR também)
Martin Smith
Obrigado @MartinSmith. Alguma idéia do que AccCardseria? Coluna ascendente cardinalidade cardinalidade talvez?
James L
1
@JamesLupolt Não, eu não conseguia pensar em algo particularmente convincente para mim. Talvez o Acc seja Acumulado ou Acesso?
Martin Smith

Respostas:

22

Existe algum benefício plausível para o iterador de spool no primeiro plano?

Isso depende do que você considera "plausível", mas a resposta de acordo com o modelo de custo é sim. Claro que isso é verdade, porque o otimizador sempre escolhe o plano mais barato que encontra.

A verdadeira questão é por que o modelo de custo considera o plano com o carretel muito mais barato que o plano sem. Considere os planos estimados criados para uma tabela nova (a partir do seu script) antes que quaisquer linhas tenham sido adicionadas ao repositório delta:

DELETE Fact.RecordedMetricsDetail
WHERE MeasurementTime < DATEADD(day,-1,GETUTCDATE())
OPTION (RECOMPILE);

O custo estimado para este plano é de 771.734 unidades :

Plano original

O custo está quase todo associado à Exclusão de Índice em Cluster, porque é esperado que as exclusões resultem em uma grande quantidade de E / S aleatória. Essa é apenas a lógica genérica que se aplica a todas as modificações de dados. Por exemplo, presume-se que um conjunto não-ordenado de modificações em um índice de árvore-b resulte em E / S amplamente aleatória, com um alto custo de E / S associado.

Os planos de alteração de dados podem apresentar uma Classificação para apresentar linhas em uma ordem que promoverá o acesso sequencial, exatamente por esses motivos de custo. O impacto é exacerbado neste caso porque a tabela está particionada. Muito particionado, de fato; seu script cria 15.000 deles. As atualizações aleatórias em uma tabela muito particionada têm um custo especialmente alto, já que o preço para alternar as partições (conjuntos de linhas) no meio do fluxo também recebe um alto custo.

O último fator importante a ser considerado é que a consulta de atualização simples acima (onde 'atualização' significa qualquer operação de alteração de dados, incluindo uma exclusão) se qualifica para uma otimização chamada "compartilhamento de conjunto de linhas", onde o mesmo conjunto de linhas interno é usado para varredura e atualizando a tabela. O plano de execução ainda mostra dois operadores separados, mas, no entanto, há apenas um conjunto de linhas usado.

Menciono isso porque ser capaz de aplicar essa otimização significa que o otimizador adota um caminho de código que simplesmente não considera os benefícios potenciais da classificação explícita para reduzir o custo de E / S aleatória. Onde a tabela é uma árvore b, isso faz sentido, porque a estrutura é inerentemente ordenada; portanto, o compartilhamento do conjunto de linhas fornece todos os benefícios potenciais automaticamente.

A consequência importante é que a lógica de custo para o operador de atualização não considera esse benefício de pedido (promoção de E / S sequenciais ou outras otimizações) em que o objeto subjacente é o armazenamento de colunas. Isso ocorre porque as modificações do armazenamento de coluna não são executadas no local; eles usam uma loja delta. O modelo de custo está, portanto, refletindo a diferença entre atualizações de conjuntos de linhas compartilhadas em b-trees e colunas.

No entanto, no caso especial de um columnstore particionado (muito!), Ainda pode haver um benefício no pedido preservado, pois executar todas as atualizações em uma partição antes de passar para a próxima ainda pode ser vantajoso do ponto de vista de E / S .

A lógica de custo padrão é reutilizada para armazenamentos de colunas aqui, portanto, um plano que preserva a ordem das partições (embora não seja dentro de cada partição) tem um custo menor. Podemos ver isso na consulta de teste usando o sinalizador de rastreamento não documentado 2332 para exigir entrada classificada para o operador de atualização. Isso define a DMLRequestSortpropriedade como true na atualização e força o otimizador a produzir um plano que forneça todas as linhas para uma partição antes de passar para a próxima:

DELETE Fact.RecordedMetricsDetail
WHERE MeasurementTime < DATEADD(day,-1,GETUTCDATE())
OPTION (RECOMPILE, QUERYTRACEON 2332);

O custo estimado para este plano é muito menor, em 52.5174 unidades:

DMLRequestSort = plano verdadeiro

Essa redução no custo deve-se ao menor custo estimado de E / S na atualização. O spool introduzido não executa nenhuma função útil, exceto para garantir a saída na ordem da partição, conforme exigido pela atualização DMLRequestSort = true(a varredura serial de um índice de armazenamento de coluna não pode fornecer essa garantia). O custo do spool em si é considerado relativamente baixo, especialmente em comparação com a redução (provavelmente irrealista) no custo na atualização.

A decisão sobre a necessidade de solicitar entrada ordenada ao operador de atualização é tomada muito cedo na otimização da consulta. As heurísticas usadas nesta decisão nunca foram documentadas, mas podem ser determinadas por tentativa e erro. Parece que o tamanho de qualquer loja delta é uma entrada para essa decisão. Uma vez feita, a escolha é permanente para a compilação da consulta. Nenhuma USE PLANdica será bem-sucedida: o objetivo do plano solicitou entrada para a atualização ou não.

Há outra maneira de obter um plano de baixo custo para essa consulta sem limitar artificialmente a estimativa de cardinalidade. Uma estimativa suficientemente baixa para evitar o spool provavelmente resultará em DMLRequestSort sendo falso, resultando em um custo planejado muito alto devido à E / S aleatória esperada. Uma alternativa é usar o sinalizador de rastreamento 8649 (plano paralelo) em conjunto com 2332 (DMLRequestSort = true):

DELETE Fact.RecordedMetricsDetail
WHERE MeasurementTime < DATEADD(day,-1,GETUTCDATE())
OPTION (RECOMPILE, QUERYTRACEON 2332, QUERYTRACEON 8649);

Isso resulta em um plano que usa a varredura paralela no modo de lote por partição e uma troca Gather Streams que preserva a ordem (mesclando):

Exclusão ordenada

Dependendo da eficácia em tempo de execução do pedido de partição no seu hardware, isso pode ter um desempenho melhor dos três. Dito isso, grandes modificações não são uma boa idéia no armazenamento de colunas, portanto a idéia de troca de partição é quase certamente melhor. Se você conseguir lidar com os longos tempos de compilação e as opções de plano peculiar, geralmente vistas com objetos particionados - especialmente quando o número de partições é grande.

Combinar muitos recursos relativamente novos, especialmente perto de seus limites, é uma ótima maneira de obter planos de execução ruins. A profundidade do suporte ao otimizador tende a melhorar com o tempo, mas o uso de 15.000 partições do armazenamento de colunas provavelmente sempre significará que você vive em momentos interessantes.

Paul White diz que a GoFundMonica
fonte