Điều chỉnh hiệu suất trên một truy vấn


9

Tìm kiếm sự giúp đỡ để cải thiện hiệu suất truy vấn này.

SQL Server 2008 R2 Enterprise , RAM tối đa 16 GB, CPU 40, Mức độ song song tối đa 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;

Thông báo thực thi,

(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.

Cấu trúc của bảng:

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

Kế hoạch thực hiện:

https://www.brentozar.com/pastetheplan/?id=rkUVhMlXM


Cập nhật sau khi nhận được câu trả lời

Cảm ơn bạn rất nhiều @Joe Obbish

Bạn nói đúng về vấn đề của truy vấn này nằm giữa DsJobStat và DsAvg. Đó không phải là nhiều về cách THAM GIA và không sử dụng KHÔNG VÀO.

Thực sự có một bảng như bạn đoán.

CREATE TABLE [dbo].[DSJobNames](
    [JobName] [nvarchar](255) NOT NULL,
 CONSTRAINT [DSJobNames$PrimaryKey] PRIMARY KEY CLUSTERED 
(   [JobName] ASC
) ); 

Tôi đã thử đề nghị của bạn,

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;   

Thông báo thực hiện:

(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.

Kế hoạch thực hiện: https://www.brentozar.com/pastetheplan/?id=rJVkLSZ7f


Nếu đó là mã nhà cung cấp mà bạn không thể thay đổi, điều tốt nhất nên làm là mở một sự cố hỗ trợ với nhà cung cấp, đau đớn nhất có thể, và đánh bại họ vì có một truy vấn yêu cầu nhiều người đọc phải thực hiện. Mệnh đề NOT IN đề cập đến các giá trị trong bảng có 413 nghìn hàng là, tối ưu phụ. Quá trình quét chỉ mục trên DSJobStat đang trả về 212 triệu hàng, bong bóng lên tới 212 triệu vòng lặp lồng nhau và bạn có thể thấy số lượng hàng triệu triệu là 83% chi phí. Tôi không nghĩ bạn có thể giúp điều này mà không cần viết lại truy vấn hoặc xóa dữ liệu ...
Tony Hinkle

Tôi không hiểu, làm thế nào mà Evan đề nghị không giúp bạn ở nơi đầu tiên, cả hai câu trả lời đều giống nhau ngoại trừ lời giải thích. Ngoài ra tôi không thấy rằng bạn thực hiện đầy đủ những gì cả hai người này gợi ý cho bạn.
KumarHarsh

Câu trả lời:


11

Hãy bắt đầu bằng cách xem xét thứ tự tham gia. Bạn có ba tham chiếu bảng trong truy vấn. Thứ tự tham gia nào có thể cung cấp cho bạn hiệu suất tốt nhất? Trình tối ưu hóa truy vấn cho rằng việc nối từ DsJobStatđến DsAvgsẽ loại bỏ gần như tất cả các hàng (ước tính cardinality giảm từ 212195000 xuống 1 hàng). Kế hoạch thực tế cho chúng ta thấy rằng ước tính này khá gần với thực tế (11 hàng tồn tại khi tham gia). Tuy nhiên, phép nối được triển khai như một phép nối chống bán hợp nhất, vì vậy tất cả 212 triệu hàng từ DsJobStatbảng được quét chỉ để tạo ra 11 hàng. Điều đó chắc chắn có thể góp phần vào thời gian thực hiện truy vấn dài, nhưng tôi không thể nghĩ ra một toán tử vật lý hoặc logic tốt hơn cho phép nối đó sẽ tốt hơn. Tôi chắc chắn rằngDJS_Dashboard_2chỉ mục được sử dụng cho các truy vấn khác, nhưng tất cả các cột bổ sung và các cột được bao gồm sẽ chỉ yêu cầu nhiều IO hơn cho truy vấn này và làm bạn chậm lại. Vì vậy, bạn có khả năng có một vấn đề truy cập bảng với quét chỉ mục trên DsJobStatbảng.

Tôi sẽ giả định rằng việc tham gia AJFkhông quá chọn lọc. Hiện tại nó không liên quan đến các vấn đề về hiệu suất mà bạn đang gặp trong truy vấn, vì vậy tôi sẽ bỏ qua nó cho phần còn lại của câu trả lời này. Điều đó có thể thay đổi nếu dữ liệu trong bảng thay đổi.

Vấn đề khác rõ ràng từ kế hoạch là toán tử bộ đệm đếm hàng. Đây là một nhà điều hành rất nhẹ nhưng nó thực hiện hơn 200 triệu lần. Toán tử ở đó vì truy vấn được viết bằng NOT IN. Nếu có một hàng NULL duy nhất DsAvgthì tất cả các hàng phải được loại bỏ. Các ống chỉ là việc thực hiện kiểm tra đó. Đó có lẽ không phải là logic mà bạn muốn, vì vậy bạn nên sử dụng phần đó để sử dụng NOT EXISTS. Lợi ích thực tế của việc viết lại đó sẽ phụ thuộc vào hệ thống và dữ liệu của bạn.

Tôi đã mô phỏng một số dữ liệu dựa trên kế hoạch truy vấn để kiểm tra một vài lần viết lại truy vấn. Các định nghĩa bảng của tôi khác biệt đáng kể so với các bảng của bạn vì sẽ có quá nhiều nỗ lực để giả lập dữ liệu cho mỗi cột. Ngay cả với các cấu trúc dữ liệu viết tắt, tôi có thể tái tạo vấn đề hiệu suất mà bạn đang gặp phải.

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;

Dựa trên kế hoạch truy vấn, chúng ta có thể thấy rằng có khoảng 200000 JobNamegiá trị duy nhất trong DsAvgbảng. Dựa trên số lượng hàng thực tế sau khi tham gia vào bảng đó, chúng ta có thể thấy rằng hầu như tất cả các JobNamegiá trị trong DsJobStatcũng nằm trong DsAvgbảng. Do đó, DsJobStatbảng có 200001 giá trị duy nhất cho JobNamecột và 1000 hàng trên mỗi giá trị.

Tôi tin rằng truy vấn này đại diện cho vấn đề hiệu suất:

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

Tất cả những thứ khác trong kế hoạch truy vấn của bạn ( GROUP BY,, HAVINGtham gia kiểu cổ, v.v.) xảy ra sau khi tập kết quả đã giảm xuống còn 11 hàng. Hiện tại không có vấn đề gì từ quan điểm hiệu năng truy vấn, nhưng có thể có những lo ngại khác có thể được tiết lộ bởi dữ liệu thay đổi trong bảng của bạn.

Tôi đang thử nghiệm trong SQL Server 2017, nhưng tôi có hình dạng kế hoạch cơ bản giống như bạn:

trước kế hoạch

Trên máy tính của tôi, truy vấn đó mất tới 6219 ms thời gian CPU và 65576 ms thời gian đã trôi qua để thực thi. Nếu tôi viết lại truy vấn để sử dụng 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);

không có ống chỉ

Các ống chỉ không còn được thực hiện 212 triệu lần và nó có thể có hành vi dự định từ nhà cung cấp. Bây giờ truy vấn thực hiện trong 34516 ms thời gian CPU và 41132 ms thời gian đã trôi qua. Phần lớn thời gian được dành để quét 212 triệu hàng từ chỉ mục.

Quét chỉ mục đó là rất đáng tiếc cho truy vấn đó. Trung bình chúng tôi có 1000 hàng cho mỗi giá trị duy nhất JobName, nhưng chúng tôi biết sau khi đọc hàng đầu tiên nếu chúng tôi cần 1000 hàng trước. Chúng tôi gần như không bao giờ cần những hàng đó, nhưng dù sao chúng tôi vẫn cần quét chúng. Nếu chúng ta biết rằng các hàng không quá dày đặc trong bảng và gần như tất cả chúng sẽ bị loại bỏ bởi phép nối, chúng ta có thể tưởng tượng một mẫu IO có thể hiệu quả hơn trên chỉ mục. Điều gì sẽ xảy ra nếu SQL Server đọc hàng đầu tiên trên mỗi giá trị duy nhất của JobName, kiểm tra xem giá trị đó có nằm trong không DsAvgvà chỉ cần bỏ qua giá trị tiếp theo JobNamenếu nó là? Thay vì quét 212 triệu hàng, kế hoạch tìm kiếm cần khoảng 200 nghìn lần thực hiện có thể được thực hiện thay thế.

Điều này chủ yếu có thể được thực hiện bằng cách sử dụng đệ quy cùng với một kỹ thuật mà Paul White tiên phong đã được mô tả ở đây . Chúng ta có thể sử dụng đệ quy để thực hiện mẫu IO mà tôi đã mô tả ở trên:

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

Truy vấn đó là rất nhiều để xem xét vì vậy tôi khuyên bạn nên kiểm tra cẩn thận kế hoạch thực tế . Đầu tiên chúng tôi thực hiện 200002 chỉ mục tìm kiếm so với chỉ mục trên DsJobStatđể có được tất cả các JobNamegiá trị duy nhất . Sau đó, chúng tôi tham gia DsAvgvà loại bỏ tất cả các hàng trừ một. Đối với hàng còn lại, tham gia trở lại DsJobStatvà nhận tất cả các cột cần thiết.

Mô hình IO hoàn toàn thay đổi. Trước khi chúng tôi nhận được điều này:

Bảng 'DsJobStat'. Quét số 1, đọc logic 1091651, đọc vật lý 13836, đọc trước 181966

Với truy vấn đệ quy, chúng tôi nhận được điều này:

Bảng 'DsJobStat'. Số lượng quét 200003, đọc logic 1398000, đọc vật lý 1, đọc trước 7345

Trên máy của tôi, truy vấn mới thực hiện chỉ trong 6891 ms thời gian CPU và 7107 ms thời gian đã trôi qua. Lưu ý rằng việc cần sử dụng đệ quy theo cách này cho thấy thiếu một cái gì đó từ mô hình dữ liệu (hoặc có thể nó chỉ không được nêu trong câu hỏi được đăng). Nếu có một bảng tương đối nhỏ chứa tất cả có thể JobNames, sẽ tốt hơn nhiều nếu sử dụng bảng đó thay vì đệ quy trên bảng lớn. Những gì nó rút ra là nếu bạn có một tập kết quả chứa tất cả những JobNamesgì bạn cần thì bạn có thể sử dụng chỉ mục tìm kiếm để lấy phần còn lại của các cột bị thiếu. Tuy nhiên, bạn không thể làm điều đó với một tập kết quả JobNamesmà bạn KHÔNG cần.


Tôi đề nghị NOT EXISTS. Họ đã trả lời với "Tôi đã thử cả hai, tham gia và không tồn tại, trước khi tôi đăng câu hỏi. Không có nhiều khác biệt."
Evan Carroll

1
Tôi sẽ tò mò muốn biết liệu ý tưởng đệ quy có hiệu quả hay không, điều đó thật đáng sợ.
Evan Carroll

Tôi nghĩ rằng có mệnh đề là không bắt buộc. "ElapsedSec không phải là null" trong đó mệnh đề sẽ làm gì. truy vấn). bạn phải nói gì về ý tưởng của tôi?
KumarHarsh

@Joe Obbish, tôi đã cập nhật bài viết của mình. Cảm ơn rất nhiều.
Wendy

có, CTE đệ quy thực hiện row_number () trên (phân vùng theo thứ tự tên công việc theo tên) rn trong 1 phút. Nhưng đồng thời tôi không thấy bất kỳ mức tăng thêm nào trong CTE đệ quy sử dụng dữ liệu mẫu của bạn.
KumarHarsh

0

Xem điều gì xảy ra nếu bạn viết lại điều kiện,

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

Đến

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

Cũng xem xét việc viết lại tham gia SQL89 của bạn vì phong cách đó thật kinh khủng.

Thay vì

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

Thử

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

Tôi cũng nghi ngờ rằng tình trạng này có thể được viết tốt hơn nhưng chúng ta phải biết nhiều hơn về những gì đang xảy ra

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

Bạn có thực sự phải biết trung bình không phải là không, hoặc chỉ một yếu tố của nhóm không phải là không?


@EvanCarroll. Tôi đã thử cả hai, tham gia và không tồn tại, trước khi tôi đăng câu hỏi. Không có nhiều khác biệt.
Wendy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.