Настройка производительности по запросу

9

Ищу помощь для улучшения производительности этого запроса.

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

Венди
источник
Если это код поставщика, который вы не можете изменить, лучше всего открыть инцидент поддержки с поставщиком, каким бы болезненным он ни был, и обработать его запросом, требующим выполнения большого числа операций чтения. Предложение NOT IN, которое ссылается на значения в таблице с 413 тысячами строк, является неоптимальным. Сканирование индекса в DSJobStat возвращает 212 миллионов строк, что составляет до 212 миллионов вложенных циклов, и вы можете видеть, что число строк в 212 миллионов составляет 83% от стоимости. Я не думаю, что вы можете помочь этому без переписывания запроса или очистки данных ...
Тони Хинкл
Я не понимаю, почему предложение Эвана не помогло вам в первую очередь, оба ответа одинаковы, за исключением объяснения. Также я не вижу, чтобы вы полностью реализовали то, что предложили оба этих парня. Джо сделал этот вопрос интересным.
KumarHarsh

Ответы:

11

Начнем с рассмотрения порядка соединения. У вас есть три ссылки на таблицы в запросе. Какой порядок соединения может дать вам лучшую производительность? Оптимизатор запросов полагает, что объединение из DsJobStatto DsAvgисключит почти все строки (оценка количества элементов снизится с 212195000 до 1 строки). Фактический план показывает нам, что оценка довольно близка к реальности (11 рядов выдерживают объединение). Однако объединение реализовано как правильное соединение против полуслияния, поэтому все 212 миллионов строк в DsJobStatтаблице сканируются только для получения 11 строк. Это, безусловно, может способствовать увеличению времени выполнения запроса, но я не могу придумать лучшего физического или логического оператора для этого соединения, который был бы лучше. Я уверен, чтоDJS_Dashboard_2Индекс используется для других запросов, но все дополнительные ключи и включенные столбцы просто потребуют больше ввода-вывода для этого запроса и замедляют работу. Таким образом, у вас потенциально есть проблема с доступом к таблице при сканировании индекса по DsJobStatтаблице.

Я собираюсь предположить, что присоединение к AJFне очень избирательно. В настоящее время это не имеет отношения к проблемам производительности, которые вы видите в запросе, поэтому я буду игнорировать его до конца этого ответа. Это может измениться, если данные в таблице изменятся.

Другая проблема, которая очевидна из плана - это оператор буфера подсчета строк. Это очень легкий оператор, но он выполняется более 200 миллионов раз. Оператор там, потому что запрос написан с NOT IN. Если в одной строке NULL, DsAvgто все строки должны быть удалены. Катушка является реализацией этой проверки. Это, вероятно, не та логика, которую вы хотите, поэтому вам лучше написать эту часть для использования NOT EXISTS. Фактическая польза от этого переписывания будет зависеть от вашей системы и данных.

Я смоделировал некоторые данные на основе плана запроса, чтобы протестировать несколько переписанных запросов. Мои определения таблиц значительно отличаются от ваших, потому что было бы слишком много усилий для макетирования данных для каждого отдельного столбца. Даже с сокращенными структурами данных я смог воспроизвести проблему производительности, с которой вы столкнулись.

CREATE TABLE [dbo].[DsAvg](
    [JobName] [nvarchar](255) NULL
);

CREATE CLUSTERED INDEX CI_DsAvg ON [DsAvg] (JobName);

INSERT INTO [DsAvg] WITH (TABLOCK)
SELECT TOP (200000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE [dbo].[DsJobStat](
    [JobName] [nvarchar](255) NOT NULL,
    [JobStatus] [nvarchar](255) NULL,
);

CREATE CLUSTERED INDEX CI_JobStat ON DsJobStat (JobName)

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT [JobName], 'ACTIVE'
FROM [DsAvg] ds
CROSS JOIN (
SELECT TOP (1000) 1
FROM master..spt_values t1
) c (t);

INSERT INTO [DsJobStat] WITH (TABLOCK)
SELECT TOP (1000) '200001', 'ACTIVE'
FROM master..spt_values t1;

Основываясь на плане запроса, мы видим, что JobNameв DsAvgтаблице около 200 000 уникальных значений . Исходя из фактического количества строк после соединения с этой таблицей, мы можем видеть, что почти все JobNameзначения DsJobStatтакже находятся в DsAvgтаблице. Таким образом, DsJobStatтаблица имеет 200001 уникальных значений для JobNameстолбца и 1000 строк на значение.

Я считаю, что этот запрос представляет проблему производительности:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] );

Все остальное в вашем плане запросов ( GROUP BY, соединение в HAVINGстаринном стиле и т. Д.) Происходит после того, как результирующий набор был уменьшен до 11 строк. В настоящее время это не имеет значения с точки зрения производительности запросов, но могут быть и другие проблемы, которые могут быть обнаружены по измененным данным в ваших таблицах.

Я тестирую в SQL Server 2017, но получаю ту же базовую форму плана, что и вы:

раньше плана

На моей машине этот запрос занимает 62219 мс процессорного времени и 65576 мс истекшего времени для выполнения. Если я переписать запрос для использования NOT EXISTS:

SELECT DsJobStat.JobName AS JobName, DsJobStat.JobStatus AS JobStatus
FROM DsJobStat
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE DsJobStat.JobName = [DsAvg].JobName);

без катушки

Спул больше не выполняется 212 миллионов раз, и, вероятно, его поведение соответствует предполагаемому поведению поставщика. Теперь запрос выполняется за 34516 мс времени ЦП и 41132 мс истекшего времени. Большая часть времени уходит на сканирование 212 миллионов строк из индекса.

Это сканирование индекса очень неудачно для этого запроса. В среднем у нас есть 1000 строк на уникальное значение JobName, но после чтения первой строки мы узнаем, понадобятся ли нам предыдущие 1000 строк. Нам почти никогда не нужны эти строки, но мы все равно должны сканировать их. Если мы знаем, что строки не очень плотные в таблице и что почти все они будут исключены при объединении, мы можем представить, возможно, более эффективный шаблон ввода-вывода в индексе. Что, если SQL Server прочитал первую строку для каждого уникального значения JobName, проверил, было ли это значение DsAvg, и просто пропустил следующее значение, JobNameесли оно было? Вместо сканирования 212 миллионов строк можно выполнить план поиска, требующий около 200 тыс. Выполнений.

В основном это может быть достигнуто с помощью рекурсии вместе с техникой, которую впервые описал Пол Уайт, которая описана здесь . Мы можем использовать рекурсию, чтобы сделать шаблон ввода-вывода, который я описал выше:

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        [JobName]
    FROM dbo.DsJobStat AS T
    ORDER BY
        T.[JobName]

    UNION ALL

    -- Recursive
    SELECT R.[JobName]
    FROM
    (
        -- Number the rows
        SELECT 
            T.[JobName],
            rn = ROW_NUMBER() OVER (
                ORDER BY T.[JobName])
        FROM dbo.DsJobStat AS T
        JOIN RecursiveCTE AS R
            ON R.[JobName] < T.[JobName]
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT js.*
FROM RecursiveCTE
INNER JOIN dbo.DsJobStat js ON RecursiveCTE.[JobName]= js.[JobName]
WHERE NOT EXISTS (SELECT 1 FROM [DsAvg] WHERE RecursiveCTE.JobName = [DsAvg].JobName)
OPTION (MAXRECURSION 0);

Этот запрос очень интересен, поэтому я рекомендую внимательно изучить реальный план . Сначала мы выполняем поиск индекса 200002 по индексу, DsJobStatчтобы получить все уникальные JobNameзначения. Затем мы присоединяемся DsAvgи удаляем все строки, кроме одной. Для оставшейся строки присоединитесь DsJobStatи получите все необходимые столбцы.

Шаблон IO полностью меняется. Прежде чем мы получили это:

Таблица «DsJobStat». Сканирование 1, логическое чтение 1091651, физическое чтение 13836, чтение с опережением 181966

С помощью рекурсивного запроса мы получаем это:

Таблица «DsJobStat». Сканирование 200003, логическое чтение 1398000, физическое чтение 1, чтение с опережением 7345

На моей машине новый запрос выполняется всего за 6891 мс времени ЦП и 7107 мс времени. Обратите внимание, что необходимость использовать рекурсию таким образом предполагает, что что-то отсутствует в модели данных (или, возможно, это было просто не указано в опубликованном вопросе). Если есть сравнительно небольшая таблица, которая содержит все возможное, JobNamesбудет гораздо лучше использовать эту таблицу, а не рекурсию на большой таблице. Все сводится к тому, что если у вас есть набор результатов, содержащий все, JobNamesчто вам нужно, вы можете использовать поиск по индексу, чтобы получить оставшиеся пропущенные столбцы. Однако вы не можете сделать это с набором результатов, JobNamesкоторый вам не нужен.

Джо Оббиш
источник
Я предложил NOT EXISTS. Они уже ответили: «Я уже пробовал оба, присоединиться и не существует, прежде чем я разместил вопрос. Не большая разница».
Эван Кэрролл
1
Мне было бы интересно узнать, работает ли рекурсивная идея, но это ужасно.
Эван Кэрролл
я думаю, что наличие предложения не обязательно. "ElapsedSec не является нулевым" в том, где будет делать предложение. Также я думаю, что рекурсивный CTE не требуется. Вы можете использовать row_number () over (разделение по порядку имен заданий по имени) rn, где не существует (выберите вопрос). Что вы можете сказать о моей идее?
KumarHarsh
@ Джоэ Оббиш, я обновил свой пост. Большое спасибо.
Венди
да, рекурсивный CTE out выполняет row_number () over (разбиение по порядку имен заданий по имени) на 1 минуту. Но в то же время я не видел никакого дополнительного усиления в рекурсивном CTE с использованием ваших данных выборки.
KumarHarsh
0

Посмотрите, что произойдет, если переписать условие,

AND DsJobStat.JobName NOT IN( SELECT [DsAvg].JobName FROM [DsAvg] )         

к

AND NOT EXISTS ( SELECT 1 FROM [DsAvg] AS d WHERE d.JobName = DsJobStat.JobName )

Также подумайте о переписывании соединения SQL89, потому что этот стиль ужасен.

Вместо

FROM DsJobStat, AJF 
WHERE DsJobStat.NumericOrderNo=AJF.OrderNo 
AND DsJobStat.Odate=AJF.Odate 

Пытаться

FROM DsJobStat
INNER JOIN AJF ON (
  DsJobStat.NumericOrderNo=AJF.OrderNo 
  AND DsJobStat.Odate=AJF.Odate
)

Я также подозреваю, что это условие может быть написано лучше, но мы должны знать больше о том, что происходит

HAVING AVG(CAST(DsJobStat.ElapsedSec AS FLOAT)) <> 0;

Вы действительно должны знать, что среднее не равно нулю, или просто один элемент группы не равен нулю?

Эван Кэрролл
источник
@EvanCarroll. Я уже пробовал оба, присоединиться и не существует, прежде чем я разместил вопрос. Не большая разница.
Венди