Налаштування продуктивності за запитом


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% від вартості. Я не думаю, що ти можеш допомогти цьому, не переписавши запити чи очистивши дані ...
Tony Hinkle

Я не розумію, чому пропозиція Евана не допомогла тобі в першу чергу, обидва відповіді однакові, за винятком пояснень. Також я не бачу, що ти повністю реалізував те, що обидва хлопці запропонували тобі. Я зробив це питання цікавим.
KumarHarsh

Відповіді:


11

Почнемо з розгляду порядку приєднання. У запиті у вас є три посилання на таблицю. Яке замовлення на приєднання може дати вам найкращу ефективність? Оптимізатор запитів вважає, що приєднання з DsJobStatдо 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таблиці є близько 200000 унікальних значень . На основі фактичної кількості рядків після приєднання до цієї таблиці ми можемо бачити, що майже всі 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

@Joe Obbish, я оновив свою публікацію. Дуже дякую.
Венді

так, Recursive CTE out виконує row_number () over (розділ за замовленням імені посади за іменем) rn за 1 хвилину. Але в той же час я не бачив додаткового посилення в Recursive 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. Я вже спробував обидва, приєднайтесь і не існує, перш ніж розмістити запитання. Не велика різниця.
Венді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.