100% CPU с плохим планом исполнения

8

У меня огромная проблема со 100% -ными скачками ЦП из-за неверного плана выполнения, используемого конкретным запросом. Я провожу недели, теперь решаю сам.

Моя база данных

Моя примерная БД содержит 3 упрощенные таблицы.

[Регистратор данных]

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]

[Инвертор]

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)
)

Статистика и техническое обслуживание

[InverterData]Таблица содержит несколько миллионов строк (зависит от нескольких экземпляров PaaS) секционированных в ежемесячных джонки.

Все индексаторы дефрагментированы и все статистические данные перестраиваются / реорганизуются по мере необходимости на ежедневном / еженедельном ходу.

Мой запрос

Запрос генерируется Entity Framework, а также прост. Но я бегаю 1000 раз в минуту, и производительность очень важна.

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)

MAXDOP 1Намек на другую проблему с медленным планом Параллели.

«Хороший» план

В течение 90% времени используемый план молниеносен и выглядит следующим образом:

быстрый план

Проблема

За день хороший план случайно изменился на плохой и медленный.

«Плохой» план используется в течение 10-60 минут, а затем возвращается к «хорошему» плану. «Плохой» план поднимает процессор до 100%.

Вот как это выглядит:

медленный план

Что я пытаюсь до сих пор

Моей первой мыслью было, что Hash Matchэто плохой мальчик. Поэтому я изменил запрос с новой подсказкой.

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

LOOP JOINДолжны заставить использовать Nested Loopмгновение Hash Match.

В результате план на 90% выглядит как раньше. Но план также случайно изменился на плохой.

«Плохой» план теперь выглядит следующим образом (изменен порядок цикла таблицы):

также медленный план

Процессор также подсматривает до 100% во время «нового плохого» плана.

Решение?

Мне приходит в голову форсировать «хороший» план. Но я не знаю, хорошая ли это идея.

Внутри плана рекомендуется индекс, включающий все столбцы. Но это удвоит всю таблицу и замедлит вставку, что очень часто.

Пожалуйста, помогите мне!


Обновление 1 - связано с комментарием @James

Вот оба плана (некоторые дополнительные поля показаны в плане, потому что это из реальной таблицы):

Хороший план

Плохой план 1 (Hash Match)

Плохой план 2 (Nested Loop)

Обновление 2 - связано с ответом @David Fowler

Плохой план включает в себя случайное значение параметра. Так что обычно я дырявый @p__linq__1 ='2016-11-26 00:00:00.0000000' @p__linq__0 =20825день и плохой план, приходящий на одно и то же значение.

Я знаю проблему перехвата параметров из хранимых процедур и как избежать их внутри SP. У вас есть подсказка для меня, как избежать этой проблемы для моего запроса?

Создание рекомендуемого индекса будет включать все столбцы. Это удвоит всю таблицу и замедлит вставку, что очень часто. Это не "чувствует" право строить индекс, который просто клонирует таблицу. Также я имею в виду удвоить размер данных этой большой таблицы.

Обновление 3 - связано с комментарием Дэвида Фаулера

Это также не сработало, и я думаю, что не смогло. Для лучшего понимания я объясню вам, как называется запрос.

Предположим, у меня есть 3 сущности в [DataLogger]таблице. В течение дня я снова и снова звоню на одни и те же 3 запроса:

Базовый запрос: ...WHERE ([Extent3].[ProjectID] = @p__linq__0) AND ([Extent1].[Date] = @p__linq__1)

Параметр:

  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'

Параметр @p__linq__1всегда совпадает с датой. Но он случайно выбирает плохой план для запроса, который тысячи раз выполнялся с хорошим планом раньше. С таким же параметром!

Обновление 4 - связано с комментарием @Nic

Обслуживание выполняется каждую ночь и выглядит следующим образом.

Индекс

Если индекс фрагментирован более чем на 5%, он реорганизуется ...

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

Если индекс фрагментирован более чем на 30%, он перестраивается ...

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

Если индекс разделен на части, он будет защищен от фрагментации и изменен для каждого раздела ...

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

Статистика

Вся статистика будет обновляться, если modification_counterона выше 0 ...

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

или на разделенный ..

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

В обслуживание входит вся статистика, а также автоматически сгенерированная.

пример

Штеффен Мангольд
источник
Вы не упоминаете об этом, но обновили ли вы статистику по какой-либо из этих таблиц в последнее время?
Nic
Спасибо @Nic Я добавил обновление по этому вопросу. Пожалуйста, смотрите подробную информацию там.
Штеффен Мангольд

Ответы:

3

Посмотрите на планы, есть несколько различий между хорошим и плохим. Первое, на что следует обратить внимание, это то, что хороший план выполняет поиск по InverterDayData, где оба плохих плана выполняют сканирование. Почему, если вы проверите оценочные строки, вы увидите, что хороший план ожидает 1 строку, тогда как плохие планы ожидают 6661 и около 7000 строк.

Теперь взглянем на скомпилированные значения параметров,

Хороший план @ p__linq__1 = '2016-11-26 00: 00: 00.0000000' @ p__linq__0 = 20825

Плохие планы @ p__linq__1 = '2018-01-03 00: 00: 00.0000000' @ p__linq__0 = 20686

так что мне кажется, что это проблема с анализом параметров, какие значения параметров вы передаете в этот запрос, если он работает плохо?

В плохих планах по InverterDayData есть рекомендация по индексам, которая выглядит разумной, я бы попробовал выполнить ее и посмотреть, поможет ли она вам. Это может позволить SQL выполнить сканирование таблицы.

Дэвид Фаулер
источник
спасибо за ваш ответ! Я изменил вопрос для вас (в конце).
Штеффен Мангольд
Я не осознавал, что предлагаемый индекс охватывает всю таблицу, это не очень хорошая идея. Вы можете просто попробовать индекс по столбцу даты.
Дэвид Фаулер
использование подсказки OPTIMIZE FOR заставит план компилироваться с правильными значениями, OPTION (OPTIMIZE FOR (@ p__linq__1 = '2016-11-26 00: 00: 00.0000000', @ p__linq__0 20825)), но будьте осторожны с этим и проверьте чтобы убедиться, что это не вызовет у вас неожиданных болей в голове
Дэвид Фаулер
эй @ david я отправил обновление.
Штеффен Мангольд
Хорошо, это была проблема с анализом параметров! Из-за разного количества строк внутри секционированной таблицы выбран индекс SEEK или SCAN. Я исправил это с ...OPTION (OPTIMIZE FOR UNKNOWN)подсказкой.
Штеффен Мангольд