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 1
dica é para outro problema com um plano paralelo lento.
O plano "bom"
Nos 90% do tempo, o plano usado é extremamente rápido e fica assim:
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:
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 JOIN
deve forçar a usar Nested Loop
instante 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):
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):
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 =20825
no 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:
@p__linq__0 = 1; @p__linq__1 = '2018-01-05 00:00:00.0000000'
@p__linq__0 = 2; @p__linq__1 = '2018-01-05 00:00:00.0000000'
@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_counter
for 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.