Ищу помощь для улучшения производительности этого запроса.
SQL Server 2008 R2 Enterprise , Макс. ОЗУ 16 ГБ, ЦП 40, Макс. Степень параллелизма 4.
SELECT DsJobStat.JobName AS JobName
, AJF.ApplGroup AS GroupName
, DsJobStat.JobStatus AS JobStatus
, AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
, AVG(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG
FROM DsJobStat, AJF
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo
AND DsJobStat.Odate=AJF.Odate
AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )
GROUP BY DsJobStat.JobName
, AJF.ApplGroup
, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;
Сообщение о выполнении,
(0 row(s) affected)
Table 'AJF'. Scan count 11, logical reads 45, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 2, logical reads 1926, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 1, logical reads 3831235, physical reads 85, read-ahead reads 3724396, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row(s) affected)
SQL Server Execution Times:
CPU time = 67268 ms, elapsed time = 90206 ms.
Структура столов:
-- 212271023 rows
CREATE TABLE [dbo].[DsJobStat](
[OrderID] [nvarchar](8) NOT NULL,
[JobNo] [int] NOT NULL,
[Odate] [datetime] NOT NULL,
[TaskType] [nvarchar](255) NULL,
[JobName] [nvarchar](255) NOT NULL,
[StartTime] [datetime] NULL,
[EndTime] [datetime] NULL,
[NodeID] [nvarchar](255) NULL,
[GroupName] [nvarchar](255) NULL,
[CompStat] [int] NULL,
[RerunCounter] [int] NOT NULL,
[JobStatus] [nvarchar](255) NULL,
[CpuMSec] [int] NULL,
[ElapsedSec] [int] NULL,
[StatusReason] [nvarchar](255) NULL,
[NumericOrderNo] [int] NULL,
CONSTRAINT [PK_DsJobStat] PRIMARY KEY CLUSTERED
( [OrderID] ASC,
[JobNo] ASC,
[Odate] ASC,
[JobName] ASC,
[RerunCounter] ASC
));
-- 48992126 rows
CREATE TABLE [dbo].[AJF](
[JobName] [nvarchar](255) NOT NULL,
[JobNo] [int] NOT NULL,
[OrderNo] [int] NOT NULL,
[Odate] [datetime] NOT NULL,
[SchedTab] [nvarchar](255) NULL,
[Application] [nvarchar](255) NULL,
[ApplGroup] [nvarchar](255) NULL,
[GroupName] [nvarchar](255) NULL,
[NodeID] [nvarchar](255) NULL,
[Memlib] [nvarchar](255) NULL,
[Memname] [nvarchar](255) NULL,
[CreationTime] [datetime] NULL,
CONSTRAINT [AJF$PrimaryKey] PRIMARY KEY CLUSTERED
( [JobName] ASC,
[JobNo] ASC,
[OrderNo] ASC,
[Odate] ASC
));
-- 413176 rows
CREATE TABLE [dbo].[DsAvg](
[JobName] [nvarchar](255) NULL,
[GroupName] [nvarchar](255) NULL,
[JobStatus] [nvarchar](255) NULL,
[ElapsedSecAVG] [float] NULL,
[CpuMSecAVG] [float] NULL
);
CREATE NONCLUSTERED INDEX [DJS_Dashboard_2] ON [dbo].[DsJobStat]
( [JobName] ASC,
[Odate] ASC,
[StartTime] ASC,
[EndTime] ASC
)
INCLUDE ( [OrderID],
[JobNo],
[NodeID],
[GroupName],
[JobStatus],
[CpuMSec],
[ElapsedSec],
[NumericOrderNo]) ;
CREATE NONCLUSTERED INDEX [Idx_Dashboard_AJF] ON [dbo].[AJF]
( [OrderNo] ASC,
[Odate] ASC
)
INCLUDE ( [SchedTab],
[Application],
[ApplGroup]) ;
CREATE NONCLUSTERED INDEX [DsAvg$JobName] ON [dbo].[DsAvg]
( [JobName] ASC
)
План выполнения:
https://www.brentozar.com/pastetheplan/?id=rkUVhMlXM
Обновить после получения ответа
Большое спасибо @Joe Obbish
Вы правы в вопросе этого запроса, который существует между DsJobStat и DsAvg. Это не много о том, как присоединиться и не использовать NOT IN.
Как вы уже догадались, стол действительно есть.
CREATE TABLE [dbo].[DSJobNames](
[JobName] [nvarchar](255) NOT NULL,
CONSTRAINT [DSJobNames$PrimaryKey] PRIMARY KEY CLUSTERED
( [JobName] ASC
) );
Я попробовал ваше предложение,
SELECT DsJobStat.JobName AS JobName
, AJF.ApplGroup AS GroupName
, DsJobStat.JobStatus AS JobStatus
, AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) AS ElapsedSecAVG
, Avg(CAST(DsJobStat.CpuMSec AS FLOAT)) AS CpuMSecAVG
FROM DsJobStat
INNER JOIN DSJobNames jn
ON jn.[JobName]= DsJobStat.[JobName]
INNER JOIN AJF
ON DsJobStat.Odate=AJF.Odate
AND DsJobStat.NumericOrderNo=AJF.OrderNo
WHERE NOT EXISTS ( SELECT 1 FROM [DsAvg] WHERE jn.JobName = [DsAvg].JobName )
GROUP BY DsJobStat.JobName, AJF.ApplGroup, DsJobStat.JobStatus
HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;
Сообщение о выполнении:
(0 row(s) affected)
Table 'DSJobNames'. Scan count 5, logical reads 1244, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsAvg'. Scan count 5, logical reads 2129, physical reads 0, read-ahead reads 24, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'DsJobStat'. Scan count 8, logical reads 84, physical reads 0, read-ahead reads 83, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AJF'. Scan count 5, logical reads 757999, physical reads 944, read-ahead reads 757311, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
(1 row(s) affected)
SQL Server Execution Times:
CPU time = 21776 ms, elapsed time = 33984 ms.
План выполнения: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f
Ответы:
Начнем с рассмотрения порядка соединения. У вас есть три ссылки на таблицы в запросе. Какой порядок соединения может дать вам лучшую производительность? Оптимизатор запросов полагает, что объединение из
DsJobStat
toDsAvg
исключит почти все строки (оценка количества элементов снизится с 212195000 до 1 строки). Фактический план показывает нам, что оценка довольно близка к реальности (11 рядов выдерживают объединение). Однако объединение реализовано как правильное соединение против полуслияния, поэтому все 212 миллионов строк вDsJobStat
таблице сканируются только для получения 11 строк. Это, безусловно, может способствовать увеличению времени выполнения запроса, но я не могу придумать лучшего физического или логического оператора для этого соединения, который был бы лучше. Я уверен, чтоDJS_Dashboard_2
Индекс используется для других запросов, но все дополнительные ключи и включенные столбцы просто потребуют больше ввода-вывода для этого запроса и замедляют работу. Таким образом, у вас потенциально есть проблема с доступом к таблице при сканировании индекса поDsJobStat
таблице.Я собираюсь предположить, что присоединение к
AJF
не очень избирательно. В настоящее время это не имеет отношения к проблемам производительности, которые вы видите в запросе, поэтому я буду игнорировать его до конца этого ответа. Это может измениться, если данные в таблице изменятся.Другая проблема, которая очевидна из плана - это оператор буфера подсчета строк. Это очень легкий оператор, но он выполняется более 200 миллионов раз. Оператор там, потому что запрос написан с
NOT IN
. Если в одной строке NULL,DsAvg
то все строки должны быть удалены. Катушка является реализацией этой проверки. Это, вероятно, не та логика, которую вы хотите, поэтому вам лучше написать эту часть для использованияNOT EXISTS
. Фактическая польза от этого переписывания будет зависеть от вашей системы и данных.Я смоделировал некоторые данные на основе плана запроса, чтобы протестировать несколько переписанных запросов. Мои определения таблиц значительно отличаются от ваших, потому что было бы слишком много усилий для макетирования данных для каждого отдельного столбца. Даже с сокращенными структурами данных я смог воспроизвести проблему производительности, с которой вы столкнулись.
Основываясь на плане запроса, мы видим, что
JobName
вDsAvg
таблице около 200 000 уникальных значений . Исходя из фактического количества строк после соединения с этой таблицей, мы можем видеть, что почти всеJobName
значенияDsJobStat
также находятся вDsAvg
таблице. Таким образом,DsJobStat
таблица имеет 200001 уникальных значений дляJobName
столбца и 1000 строк на значение.Я считаю, что этот запрос представляет проблему производительности:
Все остальное в вашем плане запросов (
GROUP BY
, соединение вHAVING
старинном стиле и т. Д.) Происходит после того, как результирующий набор был уменьшен до 11 строк. В настоящее время это не имеет значения с точки зрения производительности запросов, но могут быть и другие проблемы, которые могут быть обнаружены по измененным данным в ваших таблицах.Я тестирую в SQL Server 2017, но получаю ту же базовую форму плана, что и вы:
На моей машине этот запрос занимает 62219 мс процессорного времени и 65576 мс истекшего времени для выполнения. Если я переписать запрос для использования
NOT EXISTS
:Спул больше не выполняется 212 миллионов раз, и, вероятно, его поведение соответствует предполагаемому поведению поставщика. Теперь запрос выполняется за 34516 мс времени ЦП и 41132 мс истекшего времени. Большая часть времени уходит на сканирование 212 миллионов строк из индекса.
Это сканирование индекса очень неудачно для этого запроса. В среднем у нас есть 1000 строк на уникальное значение
JobName
, но после чтения первой строки мы узнаем, понадобятся ли нам предыдущие 1000 строк. Нам почти никогда не нужны эти строки, но мы все равно должны сканировать их. Если мы знаем, что строки не очень плотные в таблице и что почти все они будут исключены при объединении, мы можем представить, возможно, более эффективный шаблон ввода-вывода в индексе. Что, если SQL Server прочитал первую строку для каждого уникального значенияJobName
, проверил, было ли это значениеDsAvg
, и просто пропустил следующее значение,JobName
если оно было? Вместо сканирования 212 миллионов строк можно выполнить план поиска, требующий около 200 тыс. Выполнений.В основном это может быть достигнуто с помощью рекурсии вместе с техникой, которую впервые описал Пол Уайт, которая описана здесь . Мы можем использовать рекурсию, чтобы сделать шаблон ввода-вывода, который я описал выше:
Этот запрос очень интересен, поэтому я рекомендую внимательно изучить реальный план . Сначала мы выполняем поиск индекса 200002 по индексу,
DsJobStat
чтобы получить все уникальныеJobName
значения. Затем мы присоединяемсяDsAvg
и удаляем все строки, кроме одной. Для оставшейся строки присоединитесьDsJobStat
и получите все необходимые столбцы.Шаблон IO полностью меняется. Прежде чем мы получили это:
С помощью рекурсивного запроса мы получаем это:
На моей машине новый запрос выполняется всего за 6891 мс времени ЦП и 7107 мс времени. Обратите внимание, что необходимость использовать рекурсию таким образом предполагает, что что-то отсутствует в модели данных (или, возможно, это было просто не указано в опубликованном вопросе). Если есть сравнительно небольшая таблица, которая содержит все возможное,
JobNames
будет гораздо лучше использовать эту таблицу, а не рекурсию на большой таблице. Все сводится к тому, что если у вас есть набор результатов, содержащий все,JobNames
что вам нужно, вы можете использовать поиск по индексу, чтобы получить оставшиеся пропущенные столбцы. Однако вы не можете сделать это с набором результатов,JobNames
который вам не нужен.источник
NOT EXISTS
. Они уже ответили: «Я уже пробовал оба, присоединиться и не существует, прежде чем я разместил вопрос. Не большая разница».Посмотрите, что произойдет, если переписать условие,
к
Также подумайте о переписывании соединения SQL89, потому что этот стиль ужасен.
Вместо
Пытаться
Я также подозреваю, что это условие может быть написано лучше, но мы должны знать больше о том, что происходит
Вы действительно должны знать, что среднее не равно нулю, или просто один элемент группы не равен нулю?
источник