CPU 100% com plano de execução incorreto


8

Eu tenho um problema enorme com picos de CPU de 100% devido a um plano de execução incorreto usado por uma consulta específica. Agora passo semanas resolvendo sozinho.

Meu banco de dados

Meu banco de dados de amostra contém 3 tabelas simplificadas.

[Data logger]

CREATE TABLE [model].[DataLogger](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [ProjectID] [bigint] NULL,
CONSTRAINT [PK_DataLogger] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

[Inversor]

CREATE TABLE [model].[Inverter](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [SerialNumber] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_Inverter] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY],
 CONSTRAINT [UK_Inverter] UNIQUE NONCLUSTERED 
(
    [DataLoggerID] ASC,
    [SerialNumber] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [model].[Inverter] WITH CHECK
ADD CONSTRAINT [FK_Inverter_DataLogger]
FOREIGN KEY([DataLoggerID])
REFERENCES [model].[DataLogger] ([ID])

[InverterData]

CREATE TABLE [data].[InverterData](
    [InverterID] [bigint] NOT NULL,
    [Timestamp] [datetime] NOT NULL,
    [DayYield] [decimal](18, 2) NULL,
 CONSTRAINT [PK_InverterData] PRIMARY KEY CLUSTERED 
(
    [InverterID] ASC,
    [Timestamp] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF)
)

Estatísticas e Manutenção

A [InverterData]tabela contém vários milhões de linhas (difere em várias instâncias PaaS) particionadas em lixo eletrônico mensal.

Todos os indexadores são desfragmentados e todas as estatísticas são reconstruídas / reorganizadas conforme necessário em um turno diário / semanal.

Minha consulta

A consulta é gerada pelo Entity Framework e também simples. Mas eu corro 1.000 vezes por minuto e o desempenho é essencial.

SELECT
[Extent1].[InverterID] AS [InverterID],
[Extent1].[DayYield] AS [DayYield]
FROM [data].[InverterDayData] AS [Extent1]
INNER JOIN [model].[Inverter] AS [Extent2] ON [Extent1].[InverterID] = [Extent2].[ID]
INNER JOIN [model].[DataLogger] AS [Extent3] ON [Extent2].[DataLoggerID] = [Extent3].[ID]
WHERE ([Extent3].[ProjectID] = @p__linq__0)
AND ([Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1)

A MAXDOP 1dica é para outro problema com um plano paralelo lento.

O plano "bom"

Nos 90% do tempo, o plano usado é extremamente rápido e fica assim:

plano rápido

O problema

Ao longo do dia, o plano bom mudou aleatoriamente para um plano ruim e lento.

O plano "ruim" é usado por 10 a 60 minutos e depois volta ao plano "bom". O plano "ruim" aumenta a CPU até 100% permanente.

Isto é o que parece:

plano lento

O que eu tento até agora

Meu primeiro pensamento foi o Hash Matché o menino mau. Então, modifiquei a consulta com uma nova dica.

...Extent1].[Date] = @p__linq__1) OPTION (MAXDOP 1, LOOP JOIN)

O LOOP JOINdeve forçar a usar Nested Loopinstante de Hash Match.

O resultado é que o plano de 90% se parece com antes. Mas o plano também mudou aleatoriamente para ruim.

O plano "ruim" agora se parece com isso (a ordem do loop da tabela foi alterada):

plano também lento

A CPU também espreita para 100% durante o plano "novo ruim".

Solução?

Penso em forçar o plano "bom". Mas não sei se é uma boa ideia.

Dentro do plano, é recomendado um índice que inclua todas as colunas. Mas isso dobrará a tabela completa e diminuirá o conhecimento, que é altamente frequente.

Por favor me ajude!


Atualização 1 - relacionada ao comentário do @James

Aqui estão os dois planos (alguns campos extras mostrados no plano porque são da tabela real):

Bom plano

Plano ruim 1 (Hash Match)

Plano incorreto 2 (loop aninhado)

Atualização 2 - relacionada a @David Fowler replyere

O plano ruim está entrando no valor do parâmetro aleatório. Então, normalmente eu @p__linq__1 ='2016-11-26 00:00:00.0000000' @p__linq__0 =20825no dia do buraco e que o plano ruim vem com o mesmo valor.

Conheço o problema de detecção de parâmetros nos procedimentos armazenados e como evitá-los dentro do SP. Você tem uma dica para mim como evitar esse problema na minha consulta?

A criação do índice recomendado incluirá todas as colunas. Isso dobrará a tabela completa e diminuirá o desempenho, que são altamente frequentes. Isso não "parece" certo para criar um índice que simplesmente clone a tabela. Também quero dobrar o tamanho dos dados dessa grande tabela.

Atualização 3 - relacionada ao comentário de @David Fowler

Também não funcionou e acho que não. Para uma melhor compreensão, explicarei como a consulta é chamada.

Vamos supor que eu tenha 3 entidades na [DataLogger]tabela. Durante o dia, chamo as mesmas 3 consultas em uma viagem de ida e volta:

Consulta base: ...WHERE ([Extent3].[ProjectID] = @p__linq__0) AND ([Extent1].[Date] = @p__linq__1)

Parâmetro:

  1. @p__linq__0 = 1; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  2. @p__linq__0 = 2; @p__linq__1 = '2018-01-05 00:00:00.0000000'
  3. @p__linq__0 = 3; @p__linq__1 = '2018-01-05 00:00:00.0000000'

O parâmetro @p__linq__1é sempre a mesma data. Mas escolhe aleatoriamente o plano ruim em uma consulta que executa milhares de vezes com um bom plano antes. Com o mesmo parâmetro!

Atualização 4 - relacionada ao comentário @Nic

A manutenção é executada todas as noites e fica assim.

Índice

Se um índice é fragmentado mais de 5%, é reorganizado ...

ALTER INDEX [{index}] ON [{table}] REORGANIZE

Se um índice é fragmentado mais de 30%, é reconstruído ...

ALTER INDEX [{index}] ON [{table}] REBUILD WITH (ONLINE=ON, MAXDOP=1)

Se o Índice for particionado, ele será testado quanto à fragmentação e alterado por partição ...

ALTER INDEX [{index}] ON [{table}] REBUILD PARTITION = {partitionNr} WITH (ONLINE=ON, MAXDOP=1)

Estatisticas

Todas as estatísticas serão atualizadas se modification_counterfor maior que 0 ...

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH FULLSCAN

ou em particionado ..

UPDATE STATISTICS [{schema}].[{object}] ([{stats}]) WITH RESAMPLE ON PARTITIONS({partitionNr})

A manutenção inclui todas as estatísticas, também a gerada automaticamente.

Exemplo


Você não mencionou isso, mas atualizou as estatísticas de alguma dessas tabelas recentemente?
Nic

Thx @Nic Adicionei uma atualização sobre a questão. Por favor, veja informações detalhadas lá.
Steffen Mangold

Respostas:


3

Veja os planos, existem algumas diferenças entre as boas e as ruins. A primeira coisa a notar é que o plano bom realiza uma busca no InverterDayData, onde os dois planos ruins executam uma verificação. Por que é isso, se você verificar as linhas estimadas, verá que o plano bom está esperando uma linha, enquanto os planos ruins estão esperando 6661 e cerca de 7000 linhas.

Agora, veja os valores dos parâmetros compilados,

Bom plano @ p__linq__1 = '2016-11-26 00: 00: 00.0000000' @ p__linq__0 = 20825

O que você precisa saber para começar o dia bem-informado

então, para mim, parece um problema de detecção de parâmetros, que valores de parâmetro você está passando para essa consulta quando está com um desempenho ruim?

Há uma recomendação de índice nos planos ruins no InverterDayData que parece sensata, eu tentaria executá-lo e ver se isso ajuda você. Pode permitir que o SQL execute uma varredura na tabela.


obrigado pela sua resposta! Eu modifiquei a pergunta para você (no final).
Steffen Mangold

Não percebi que o índice sugerido cobria toda a tabela, não é uma boa ideia. Você pode apenas tentar um índice na coluna da data.
David Fowler

usar a dica OPTIMIZE FOR forçará o plano a compilar com os bons valores, OPTION (OPTIMIZE FOR (@ p__linq__1 = '2016-11-26 00: 00: 00.0000000', @ p__linq__0 20825)), mas tenha cuidado com isso e teste -lo para se certificar de que isso não vai lhe causar dores de cabeça inesperadas
David Fowler

hey @david Postei uma atualização.
Steffen Mangold

Ok, era uma questão de cheirar parâmetros! Por causa da contagem de linhas diferentes na tabela particionada, o Índice SEEK ou SCAN é escolhido. Eu consertei com uma ...OPTION (OPTIMIZE FOR UNKNOWN)dica.
Steffen Mangold
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.