Tối ưu hóa truy vấn con với chức năng Windowing


8

Vì các kỹ năng điều chỉnh hiệu suất của tôi dường như không bao giờ cảm thấy đủ, tôi luôn tự hỏi liệu có tối ưu hóa hơn tôi có thể thực hiện đối với một số truy vấn không. Tình huống mà câu hỏi này liên quan đến là một hàm Windowed MAX được lồng trong một truy vấn con.

Dữ liệu mà tôi đang đào bới là một loạt các giao dịch trên các nhóm khác nhau lớn hơn. Tôi đã có 4 lĩnh vực quan trọng, ID duy nhất của giao dịch, ID nhóm của một loạt giao dịch và ngày được liên kết với giao dịch hoặc nhóm giao dịch duy nhất tương ứng. Hầu hết các ngày của Ngày khớp với Ngày giao dịch duy nhất tối đa cho một lô, nhưng có những lúc điều chỉnh thủ công đi qua hệ thống của chúng tôi và một hoạt động ngày duy nhất xảy ra sau khi bắt đầu ngày giao dịch nhóm. Chỉnh sửa thủ công này không điều chỉnh ngày nhóm theo thiết kế.

Những gì tôi xác định trong truy vấn này là những bản ghi trong đó Ngày duy nhất rơi sau Ngày nhóm. Truy vấn mẫu sau đây xây dựng tương đương sơ bộ kịch bản của tôi và câu lệnh CHỌN trả về các bản ghi tôi đang tìm, tuy nhiên, tôi có đang tiếp cận giải pháp này theo cách hiệu quả nhất không? Việc này sẽ mất một lúc để chạy trong bảng thực tế của tôi khi bản ghi của tôi đếm số trong 9 chữ số trên, nhưng chủ yếu là sự khinh thường của tôi đối với các truy vấn con khiến tôi tự hỏi liệu có cách tiếp cận nào tốt hơn ở đây không. Tôi không quan tâm đến bất kỳ chỉ số nào vì tôi tin rằng những chỉ số đó đã được áp dụng; những gì tôi đang tìm kiếm là một cách tiếp cận truy vấn thay thế sẽ đạt được điều tương tự, nhưng thậm chí hiệu quả hơn. Bất kỳ thông tin phản hồi đều được chào đón.

CREATE TABLE #Example
(
    UniqueID INT IDENTITY(1,1)
  , GroupID INT
  , GroupDate DATETIME
  , UniqueDate DATETIME
)

CREATE CLUSTERED INDEX [CX_1] ON [#Example]
(
    [UniqueID] ASC
)


SET NOCOUNT ON

--Populate some test data
DECLARE @i INT = 0, @j INT = 5, @UniqueDate DATETIME, @GroupDate DATETIME

WHILE @i < 10000
BEGIN

    IF((@i + @j)%173 = 0)
    BEGIN
        SET @UniqueDate = GETDATE()+@i+5
    END
    ELSE
    BEGIN
        SET @UniqueDate = GETDATE()+@i
    END

    SET @GroupDate = GETDATE()+(@j-1)

    INSERT INTO #Example (GroupID, GroupDate, UniqueDate)
    VALUES (@j, @GroupDate, @UniqueDate)

    SET @i = @i + 1

    IF (@i % 5 = 0)
    BEGIN
        SET @j = @j+5
    END
END
SET NOCOUNT OFF

CREATE NONCLUSTERED INDEX [IX_2_4_3] ON [#Example]
(
    [GroupID] ASC,
    [UniqueDate] ASC,
    [GroupDate] ASC
)
INCLUDE ([UniqueID])

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT UniqueID
     , GroupID
     , GroupDate
     , UniqueDate
FROM (
    SELECT UniqueID
         , GroupID
         , GroupDate
         , UniqueDate
         , MAX(UniqueDate) OVER (PARTITION BY GroupID) AS maxUniqueDate
    FROM #Example
    ) calc_maxUD
WHERE maxUniqueDate > GroupDate
    AND maxUniqueDate = UniqueDate

DROP TABLE #Example

dbfiddle ở đây


2
Nếu bạn muốn thực hiện điều chỉnh một truy vấn, các chỉ mục trên bảng của bạn là một phần quan trọng của câu hỏi.
Daniel Hutmacher

@DanielHutmacher Tôi hoàn toàn đồng ý, mặc dù tôi sẽ không kết xuất một lược đồ cho khu vực DWH và Staging của mình, vì vậy đây là điều tốt nhất tôi có thể làm trong lý do.
John Eisbrener

Câu trả lời:


9

Tôi cho rằng không có chỉ số nào, vì bạn chưa cung cấp bất kỳ chỉ số nào.

Ngay lập tức, chỉ mục sau sẽ loại bỏ toán tử Sắp xếp trong kế hoạch của bạn, điều này có khả năng sẽ tiêu tốn rất nhiều bộ nhớ:

CREATE INDEX IX ON #Example (GroupID, UniqueDate) INCLUDE (UniqueID, GroupDate);

Truy vấn con không phải là một vấn đề hiệu suất trong trường hợp này. Nếu có bất cứ điều gì, tôi sẽ xem xét các cách để loại bỏ chức năng cửa sổ (MAX ... QUÁ) để tránh cấu trúc Spool Nested Loop và Table Spool.

Với cùng một chỉ mục, thoạt nhìn có thể thoạt nhìn trông kém hiệu quả hơn và nó thực hiện từ hai đến ba lần quét trên bảng cơ sở, nhưng nó loại bỏ một số lượng lớn các lần đọc bên trong vì nó thiếu các toán tử Spool. Tôi đoán rằng nó vẫn sẽ hoạt động tốt hơn, đặc biệt nếu bạn có đủ lõi CPU và hiệu suất IO trên máy chủ của mình:

SELECT e.UniqueID
     , e.GroupID
     , e.GroupDate
     , e.UniqueDate
FROM (
    SELECT GroupID, MAX(UniqueDate) AS maxUniqueDate
    FROM #Example
    GROUP BY GroupID) AS agg
INNER JOIN #Example AS e ON agg.GroupID=e.GroupID
WHERE agg.maxUniqueDate > e.GroupDate
    AND agg.maxUniqueDate = e.UniqueDate
OPTION (MERGE JOIN);

(Lưu ý: Tôi đã thêm một MERGE JOINgợi ý truy vấn, nhưng điều này có thể sẽ tự động xảy ra nếu số liệu thống kê của bạn theo thứ tự. Cách tốt nhất là để lại gợi ý như thế này nếu bạn có thể.)


6
xấu xí, nhưng kế hoạch thực hiện là đẹp hơn. Đó là sự kỳ diệu của các ngôn ngữ khai báo như T-SQL.
Daniel Hutmacher

11

Khi nào và nếu bạn có thể nâng cấp từ SQL Server 2012 lên SQL Server 2016, bạn có thể tận dụng hiệu suất được cải thiện nhiều (đặc biệt đối với các tập hợp cửa sổ không khung) được cung cấp bởi toán tử Window Aggregate chế độ hàng loạt mới.

Hầu như tất cả các kịch bản xử lý dữ liệu lớn hoạt động tốt hơn với lưu trữ cột so với lưu trữ hàng. Ngay cả khi không thay đổi thành cột lưu trữ cho các bảng cơ sở của bạn, bạn vẫn có thể đạt được các lợi ích của toán tử mới và thực thi chế độ hàng loạt 2016 bằng cách tạo một chỉ mục được lưu trữ cột không được lọc trống trên một trong các bảng cơ sở hoặc bằng cách tham gia ngoài dự phòng vào một tổ chức cột bàn.

Sử dụng tùy chọn thứ hai, truy vấn trở thành:

-- Just to get batch mode processing and the window aggregate operator
CREATE TABLE #Dummy (a integer NOT NULL, INDEX DummyCC CLUSTERED COLUMNSTORE);

-- Identify any UniqueDates that are greater than the GroupDate within their GroupID
SELECT
    calc_maxUD.UniqueID,
    calc_maxUD.GroupID,
    calc_maxUD.GroupDate,
    calc_maxUD.UniqueDate
FROM 
(
    SELECT
        E.UniqueID,
        E.GroupID,
        E.GroupDate,
        E.UniqueDate,
        maxUniqueDate = MAX(UniqueDate) OVER (
            PARTITION BY GroupID)
    FROM #Example AS E
    LEFT JOIN #Dummy AS D -- The only change to the original query
        ON 1 = 0
) AS calc_maxUD
WHERE 
    calc_maxUD.maxUniqueDate > calc_maxUD.GroupDate
    AND calc_maxUD.maxUniqueDate = calc_maxUD.UniqueDate;

db <>

Lưu ý thay đổi duy nhất đối với truy vấn ban đầu là tạo một bảng tạm thời trống và thêm liên kết bên trái. Kế hoạch thực hiện là:

kế hoạch tổng hợp cửa sổ chế độ hàng loạt

(58 row(s) affected)
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0
Table '#Example'. Scan count 1, logical reads 40, physical reads 0, read-ahead reads 0

Để biết thêm thông tin và các tùy chọn, hãy xem loạt bài xuất sắc của Itzik Ben-Gan, Những điều bạn cần biết về Toán tử tổng hợp cửa sổ chế độ hàng loạt trong SQL Server 2016 (gồm ba phần).


7

Tôi sẽ ném ol 'Cross Áp dụng ra đó:

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT TOP 1 e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ORDER BY e2.UniqueDate DESC
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

Với một số loại chỉ số, nó làm khá tốt.

CREATE CLUSTERED INDEX cx_whatever ON #Example (GroupID)

CREATE UNIQUE NONCLUSTERED INDEX ix_whatever ON #Example (GroupID, UniqueDate DESC, GroupDate)

Thời gian thống kê và io trông như thế này (truy vấn của bạn là kết quả đầu tiên)

Table 'Worktable'. Scan count 3, logical reads 28004, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example'. Scan count 1, logical reads 51, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 15 ms,  elapsed time = 20 ms.

Table '#Example'. Scan count 10001, logical reads 21336, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 11 ms.

Các kế hoạch truy vấn ở đây (một lần nữa, kế hoạch của bạn là đầu tiên):

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

Tại sao tôi thích phiên bản này? Tôi tránh các cuộn chỉ. Nếu những thứ đó bắt đầu tràn ra đĩa, nó sẽ trở nên xấu xí.

Nhưng bạn cũng có thể muốn thử điều này.

SELECT e.*
    FROM #Example AS e
    CROSS APPLY ( SELECT e2.UniqueDate AS maxUniqueDate
                    FROM #Example AS e2
                    WHERE e2.GroupID = e.GroupID 
                    ) AS ca
    WHERE ca.maxUniqueDate > e.GroupDate
        AND ca.maxUniqueDate = e.UniqueDate;

Nếu đây là một DW lớn, bạn có thể thích Hash Hash và lọc hàng trong liên kết, thay vì ở cuối TOP 1truy vấn dưới dạng toán tử Bộ lọc.

Kế hoạch có tại đây: https://www.brentozar.com/pastetheplan/?id=BkUF55ATx

Số liệu thống kê thời gian và io ở đây:

Table 'Workfile'. 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 '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 '#Example'. Scan count 2, logical reads 84, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 5 ms.

Hi vọng điêu nay co ich!

Một chỉnh sửa, dựa trên ý tưởng của @ ypercube và một chỉ mục mới.

CREATE NONCLUSTERED INDEX ix_meh ON #Example (UniqueDate,GroupDate) INCLUDE (UniqueID,GroupID);

WITH t1 AS 
(
    SELECT DISTINCT
    e.GroupID ,
    MAX(UniqueDate) AS MaxUniqueDate
    FROM #Example AS e
    GROUP BY e.GroupID
)
SELECT *
FROM #Example AS e
CROSS APPLY (
SELECT *
FROM t1
    WHERE t1.MaxUniqueDate > e.GroupDate
        AND t1.MaxUniqueDate = e.UniqueDate
        AND t1.GroupID = e.GroupID
) ca

Đây là thời gian thống kê và io:

Table 'Workfile'. 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 '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 '#Example'. Scan count 2, logical reads 91, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 4 ms.

Đây là kế hoạch:

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


Có vẻ ví dụ của tôi quá sạch sẽ, vì có những tình huống tôi có thể có nhiều Ngày duy nhất lớn hơn Ngày nhóm trong môi trường thực tế của mình. Điều kiện này sẽ làm mất hiệu lực truy vấn Cross Cross thứ 2 của bạn, nhưng các cách tiếp cận khác đều hoạt động mà không có vấn đề. Cảm ơn cho một số tùy chọn hơn!
John Eisbrener

4

Tôi sẽ xem top with ties

Nếu GroupDategiống nhau GroupIdthì:

select top 1 with ties 
   UniqueID
 , GroupID
 , GroupDate
 , UniqueDate
from #Example
where UniqueDate > GroupDate
order by row_number() over (partition by GroupId order by UniqueDate desc)

Khác: sử dụng top with tiestrong một biểu thức bảng chung

with cte as (
  select top 1 with ties 
      UniqueID
    , GroupID
    , GroupDate
    , UniqueDate
  from #Example
  order by row_number() over (partition by GroupId order by UniqueDate desc)
)
select *
from cte
where UniqueDate > GroupDate

dbfiddle: http://dbfiddle.uk/?rdbms=sqlserver_2016&fiddle=c058994c2f5f3d99b212f06e1dae9fd3

Truy vấn gốc

Table 'Worktable'. Scan count 3, logical reads 28001, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 31 ms,  elapsed time = 31 ms.

vs top with tiestrong một biểu thức bảng chung

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 '#Example____________________________________________________________________________________________________________0000000000CB'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 16 ms,  elapsed time = 15 ms.

4

Vì vậy, tôi đã thực hiện một số phân tích về các cách tiếp cận khác nhau được đăng cho đến nay và trong môi trường của tôi, có vẻ như cách tiếp cận của Daniel chiến thắng một cách nhất quán về thời gian thực hiện. Đáng ngạc nhiên (với tôi) phương pháp tiếp cận CROSS ỨNG DỤNG thứ ba của sp_BlitzErik không thua xa. Đây là kết quả đầu ra nếu có ai quan tâm, nhưng cảm ơn TON cho tất cả các phương pháp thay thế. Tôi đã học được nhiều hơn từ việc đào sâu vào các câu trả lời cho câu hỏi này hơn là trong một thời gian dài!

Windowed Function - baseline metric

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. 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 'Worktable'. Scan count 89815, logical reads 42553550, physical reads 0, read-ahead reads 84586, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7819, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 87753 ms,  elapsed time = 13031 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


Basic Aggregated Subquery - Daniel Hutmacher

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 48, logical reads 82408, physical reads 9629, read-ahead reads 72779, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14565, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 40527 ms,  elapsed time = 6182 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


CROSS APPLY Operation A - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 6199331, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 3099273, logical reads 12844012, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 3109676, logical reads 9350502, physical reads 0, read-ahead reads 0, 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 'Table02Dim'. Scan count 3109676, logical reads 9482456, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. 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 '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 '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.

 SQL Server Execution Times:
   CPU time = 132632 ms,  elapsed time = 20955 ms.


CROSS APPLY Operation C - sp_BlitzErik

(10406 row(s) affected)
Table 'DateDim'. Scan count 18, logical reads 1194, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 18, logical reads 280362, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 56, logical reads 92800, physical reads 10872, read-ahead reads 81928, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 89791, logical reads 6861425, physical reads 0, read-ahead reads 14563, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 18, logical reads 15376, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 18, logical reads 15726, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 46082 ms,  elapsed time = 6804 ms.
Warning: Null value is eliminated by an aggregate or other SET operation.


TOP 1 WITH TIES - B - SqlZim

(10406 row(s) affected)
Table 'DateDim'. Scan count 9, logical reads 791, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'TableFact'. Scan count 9, logical reads 140181, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. 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 'Worktable'. Scan count 89791, logical reads 6866304, physical reads 0, read-ahead reads 93468, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table01Dim'. Scan count 9, logical reads 7688, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Table02Dim'. Scan count 9, logical reads 7835, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 79406 ms,  elapsed time = 15852 ms.

Tôi chỉ nhìn vào cách các tùy chọn được đăng sẽ xếp chồng lên nhau nếu tôi đưa ví dụ của bạn lên 100 nghìn hàng và thêm các đề xuất chỉ mục của mọi người. Có vẻ khá đại diện cho kết quả thực tế của bạn là tốt. Có vẻ như phiên bản top with tieskhóa của tôi với nhiều hàng. dbfiddle.uk/ Kẻ
SqlZim
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.