Trong SQL Server, tôi có nên buộc LOOP THAM GIA trong trường hợp sau không?


15

Thông thường, tôi khuyên bạn không nên sử dụng gợi ý tham gia vì tất cả các lý do tiêu chuẩn. Tuy nhiên, gần đây, tôi đã tìm thấy một mô hình mà hầu như tôi luôn tìm thấy một vòng lặp bắt buộc để thực hiện tốt hơn. Trên thực tế, tôi đang bắt đầu sử dụng và giới thiệu nó rất nhiều đến mức tôi muốn có ý kiến ​​thứ hai để đảm bảo rằng tôi không thiếu thứ gì. Đây là một kịch bản đại diện (mã rất cụ thể để tạo một ví dụ ở cuối):

--Case 1: NO HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
JOIN SampleTable AS S ON S.ID = D.ID

--Case 2: LOOP JOIN HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID

SampleTable có 1 triệu hàng và PK của nó là ID.
Bảng tạm thời #Driver chỉ có một cột, ID, không có chỉ mục và 50K hàng.

Những gì tôi luôn tìm thấy là như sau:

Trường hợp 1: KHÔNG CÓ GỢI Ý
Chỉ mục Quét trên mẫu
Hash Hash Tham gia
thời lượng cao hơn (avg 333ms)
CPU cao hơn (avg 331ms)
Đọc logic thấp hơn (4714)

Trường hợp 2: LOOP THAM GIA GỢI Ý
Index Tìm kiếm trên SampleTable
Vòng Tham
thấp hơn Thời gian (204ms trung bình, 39% ít hơn)
thấp hơn CPU (trung bình 206, 38% ít hơn)
Phần lớn cao hơn logic Reads (160.015, 34X hơn)

Lúc đầu, số lần đọc cao hơn của trường hợp thứ hai làm tôi sợ một chút vì việc đọc thấp hơn thường được coi là một thước đo hiệu quả. Nhưng tôi càng nghĩ về những gì đang thực sự xảy ra, nó không liên quan đến tôi. Đây là suy nghĩ của tôi:

SampleTable được chứa trên 4714 trang, chiếm khoảng 36MB. Trường hợp 1 quét tất cả chúng là lý do tại sao chúng tôi nhận được 4714 lượt đọc. Hơn nữa, nó phải thực hiện 1 triệu băm, rất tốn CPU và cuối cùng sẽ tăng thời gian theo tỷ lệ. Đó là tất cả các băm này dường như làm tăng thời gian trong trường hợp 1.

Bây giờ hãy xem xét trường hợp 2. Nó không thực hiện bất kỳ băm nào, mà thay vào đó, nó đang thực hiện 50000 tìm kiếm riêng biệt, đó là những gì đang thúc đẩy việc đọc. Nhưng làm thế nào đắt là đọc tương đối? Người ta có thể nói rằng nếu đó là những lần đọc vật lý, nó có thể khá tốn kém. Nhưng hãy nhớ rằng 1) chỉ đọc lần đầu tiên của một trang nhất định có thể là vật lý và 2) ngay cả như vậy, trường hợp 1 sẽ có vấn đề tương tự hoặc tồi tệ hơn vì nó được đảm bảo đánh vào mọi trang.

Vì vậy, tính toán cho thực tế là cả hai trường hợp phải truy cập mỗi trang ít nhất một lần, nó dường như là một câu hỏi trong đó nhanh hơn, 1 triệu băm hoặc khoảng 155000 lần đọc so với bộ nhớ? Các thử nghiệm của tôi dường như nói cái sau, nhưng SQL Server luôn chọn cái trước.

Câu hỏi

Vì vậy, trở lại câu hỏi của tôi: Tôi có nên tiếp tục ép buộc gợi ý LOOP THAM GIA này khi thử nghiệm cho thấy các loại kết quả này, hoặc tôi có thiếu điều gì trong phân tích của mình không? Tôi ngần ngại chống lại trình tối ưu hóa của SQL Server, nhưng có vẻ như nó chuyển sang sử dụng hàm băm sớm hơn nhiều so với trường hợp như thế này.

Cập nhật 2014-04-28

Tôi đã thực hiện thêm một số thử nghiệm và phát hiện ra rằng kết quả tôi đạt được ở trên (trên CPU VM w / 2) Tôi không thể sao chép trong các môi trường khác (tôi đã thử trên 2 máy vật lý khác nhau với CPU 8 & 12). Trình tối ưu hóa đã làm tốt hơn nhiều trong các trường hợp sau đến mức không có vấn đề rõ rệt như vậy. Tôi đoán bài học kinh nghiệm, có vẻ hiển nhiên khi nhìn lại, là môi trường có thể ảnh hưởng đáng kể đến việc trình tối ưu hóa hoạt động tốt như thế nào.

Kế hoạch thực hiện

Kế hoạch thực hiện Trường hợp 1 Kế hoạch 1 Kế hoạch thực hiện Trường hợp 2 nhập mô tả hình ảnh ở đây

Mã để tạo trường hợp mẫu

------------------------------------------------------------
-- 1. Create SampleTable with 1,000,000 rows
------------------------------------------------------------    

CREATE TABLE SampleTable
    (  
       ID         INT NOT NULL PRIMARY KEY CLUSTERED
     , Number1    INT NOT NULL
     , Number2    INT NOT NULL
     , Number3    INT NOT NULL
     , Number4    INT NOT NULL
     , Number5    INT NOT NULL
    )

--Add 1 million rows
;WITH  
    Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows  
    Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows  
    Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows 
    Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows 
    Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows 
    Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows 
    FinalCte AS (SELECT  ROW_NUMBER() OVER (ORDER BY C) AS Number FROM   Cte5)
INSERT INTO SampleTable
SELECT Number, Number, Number, Number, Number, Number
FROM  FinalCte
WHERE Number <= 1000000

------------------------------------------------------------
-- Create 2 SPs that join from #Driver to SampleTable.
------------------------------------------------------------    
GO
IF OBJECT_ID('JoinTest_NoHint') IS NOT NULL DROP PROCEDURE JoinTest_NoHint
GO
CREATE PROC JoinTest_NoHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    JOIN SampleTable AS S ON S.ID = D.ID
GO
IF OBJECT_ID('JoinTest_LoopHint') IS NOT NULL DROP PROCEDURE JoinTest_LoopHint
GO
CREATE PROC JoinTest_LoopHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID
GO

------------------------------------------------------------
-- Create driver table with 50K rows
------------------------------------------------------------    
GO
IF OBJECT_ID('tempdb..#Driver') IS NOT NULL DROP TABLE #Driver
SELECT ID
INTO #Driver
FROM SampleTable
WHERE ID % 20 = 0

------------------------------------------------------------
-- Run each test and run Profiler
------------------------------------------------------------    

GO
/*Reg*/  EXEC JoinTest_NoHint
GO
/*Loop*/ EXEC JoinTest_LoopHint


------------------------------------------------------------
-- Results
------------------------------------------------------------    

/*

Duration CPU   Reads    TextData
315      313   4714     /*Reg*/  EXEC JoinTest_NoHint
309      296   4713     /*Reg*/  EXEC JoinTest_NoHint
327      329   4713     /*Reg*/  EXEC JoinTest_NoHint
398      406   4715     /*Reg*/  EXEC JoinTest_NoHint
316      312   4714     /*Reg*/  EXEC JoinTest_NoHint
217      219   160017   /*Loop*/ EXEC JoinTest_LoopHint
211      219   160014   /*Loop*/ EXEC JoinTest_LoopHint
217      219   160013   /*Loop*/ EXEC JoinTest_LoopHint
190      188   160013   /*Loop*/ EXEC JoinTest_LoopHint
187      187   160015   /*Loop*/ EXEC JoinTest_LoopHint

*/

Câu trả lời:


13

SampleTable được chứa trên 4714 trang, chiếm khoảng 36MB. Trường hợp 1 quét tất cả chúng là lý do tại sao chúng tôi nhận được 4714 lượt đọc. Hơn nữa, nó phải thực hiện 1 triệu băm, rất tốn CPU và cuối cùng sẽ tăng thời gian theo tỷ lệ. Đó là tất cả các băm này dường như làm tăng thời gian trong trường hợp 1.

Có một chi phí khởi đầu cho một phép nối băm (xây dựng bảng băm, cũng là một hoạt động chặn), nhưng cuối cùng, hàm băm có chi phí trên mỗi hàng theo lý thuyết thấp nhất trong ba loại liên kết vật lý được SQL Server hỗ trợ, cả trong điều khoản của IO và CPU. Hash tham gia thực sự đi vào chính nó với đầu vào xây dựng tương đối nhỏ và đầu vào thăm dò lớn. Điều đó nói rằng, không có loại tham gia vật lý nào là 'tốt hơn' trong tất cả các kịch bản.

Bây giờ hãy xem xét trường hợp 2. Nó không thực hiện bất kỳ băm nào, mà thay vào đó, nó đang thực hiện 50000 tìm kiếm riêng biệt, đó là những gì đang thúc đẩy việc đọc. Nhưng làm thế nào đắt là đọc tương đối? Người ta có thể nói rằng nếu đó là những lần đọc vật lý, nó có thể khá tốn kém. Nhưng hãy nhớ rằng 1) chỉ đọc lần đầu tiên của một trang nhất định có thể là vật lý và 2) ngay cả như vậy, trường hợp 1 sẽ có vấn đề tương tự hoặc tồi tệ hơn vì nó được đảm bảo đánh vào mọi trang.

Mỗi tìm kiếm yêu cầu điều hướng một cây b đến gốc, chi phí tính toán cao so với một đầu dò băm duy nhất. Ngoài ra, mẫu IO chung cho phía bên trong của phép nối vòng lặp lồng nhau là ngẫu nhiên, so với mẫu truy cập tuần tự của đầu vào quét phía đầu dò với phép nối băm. Tùy thuộc vào hệ thống con IO vật lý cơ bản, các lần đọc tuần tự có thể nhanh hơn các lần đọc ngẫu nhiên. Ngoài ra, cơ chế đọc trước SQL Server hoạt động tốt hơn với IO tuần tự, tạo ra số lần đọc lớn hơn.

Vì vậy, tính toán cho thực tế là cả hai trường hợp phải truy cập mỗi trang ít nhất một lần, nó dường như là một câu hỏi trong đó nhanh hơn, 1 triệu băm hoặc khoảng 155000 lần đọc so với bộ nhớ? Các thử nghiệm của tôi dường như nói cái sau, nhưng SQL Server luôn chọn cái trước.

Trình tối ưu hóa truy vấn SQL Server đưa ra một số giả định. Một là việc truy cập đầu tiên vào một trang được thực hiện bởi một truy vấn sẽ dẫn đến IO vật lý ('giả định bộ đệm lạnh'). Cơ hội mà lần đọc sau sẽ đến từ một trang đã đọc vào bộ nhớ bởi cùng một truy vấn được mô hình hóa, nhưng điều này không hơn gì một phỏng đoán có giáo dục.

Lý do mô hình của trình tối ưu hóa hoạt động theo cách này là vì nói chung tốt hơn là tối ưu hóa cho trường hợp xấu nhất (cần IO vật lý). Nhiều thiếu sót có thể được che đậy bởi sự song song và chạy mọi thứ trong bộ nhớ. Truy vấn lập kế hoạch trình tối ưu hóa sẽ tạo ra nếu nó giả sử tất cả dữ liệu trong bộ nhớ có thể hoạt động rất kém nếu giả định đó được chứng minh là không hợp lệ.

Kế hoạch được tạo bằng cách sử dụng giả định bộ đệm lạnh có thể không thực hiện tốt cũng như nếu giả sử bộ đệm ấm, nhưng hiệu suất trong trường hợp xấu nhất của nó thường sẽ vượt trội.

Tôi có nên tiếp tục ép buộc gợi ý LOOP THAM GIA này khi thử nghiệm cho thấy các loại kết quả này, hoặc tôi có thiếu điều gì trong phân tích của mình không? Tôi ngần ngại chống lại trình tối ưu hóa của SQL Server, nhưng có vẻ như nó chuyển sang sử dụng hàm băm sớm hơn nhiều so với trường hợp như thế này.

Bạn nên rất cẩn thận về việc này vì hai lý do. Thứ nhất, tham gia gợi ý cũng âm thầm buộc vật lý tham gia để phù hợp với trật tự bằng văn bản của truy vấn (giống như khi bạn cũng đã quy định OPTION (FORCE ORDER). Điều này hạn chế nghiêm trọng các phương án sẵn sàng cho tôi ưu hoa, và có thể không phải lúc nào những gì bạn muốn. OPTION (LOOP JOIN)Lực lượng vòng lồng nhau tham gia truy vấn, nhưng không thực thi lệnh tham gia bằng văn bản.

Thứ hai, bạn đang đưa ra giả định rằng kích thước tập dữ liệu sẽ vẫn nhỏ và hầu hết các lần đọc logic sẽ đến từ bộ đệm. Nếu các giả định này trở nên không hợp lệ (có thể theo thời gian), hiệu suất sẽ giảm. Trình tối ưu hóa truy vấn tích hợp khá tốt trong việc phản ứng với các tình huống thay đổi; loại bỏ sự tự do đó là điều bạn nên suy nghĩ kỹ.

Nhìn chung, trừ khi có một lý do thuyết phục để buộc các vòng lặp tham gia, tôi sẽ tránh nó. Các kế hoạch mặc định thường khá gần với tối ưu và có xu hướng linh hoạt hơn khi đối mặt với hoàn cảnh thay đổi.


Cảm ơn Paul. Phân tích chi tiết tuyệt vời. Dựa trên một số thử nghiệm tiếp theo tôi đã làm, tôi nghĩ điều đang xảy ra là những phỏng đoán có giáo dục của trình tối ưu hóa luôn bị tắt đối với ví dụ cụ thể này khi kích thước của bảng tạm thời nằm trong khoảng từ 5K đến 100K. Với thực tế là các yêu cầu của chúng tôi đảm bảo bảng tạm thời sẽ <50K, nó có vẻ an toàn với tôi. Tôi tò mò, bạn vẫn sẽ tránh bất kỳ loại gợi ý tham gia nào biết điều này?
JohnnyM

1
@JohnnyM Gợi ý tồn tại vì một lý do. Nó là tốt để sử dụng chúng khi bạn có lý do âm thanh để làm như vậy. Điều đó nói rằng, tôi hiếm khi sử dụng gợi ý tham gia vì ngụ ý FORCE ORDER. Trong trường hợp kỳ lạ tôi sử dụng một gợi ý tham gia, tôi thường thêm OPTION (FORCE ORDER)với một nhận xét để giải thích tại sao.
Paul White phục hồi Monica

0

50.000 hàng được nối với bảng triệu hàng dường như là rất nhiều cho bất kỳ bảng nào không có chỉ mục.

Thật khó để nói cho bạn biết chính xác phải làm gì trong trường hợp này, vì nó quá tách biệt với vấn đề mà bạn đang thực sự cố gắng giải quyết. Tôi chắc chắn hy vọng rằng đó không phải là một mẫu chung trong mã của bạn khi bạn tham gia vào nhiều bảng tạm thời không được lập trình với số lượng hàng đáng kể.

Lấy ví dụ chỉ cho những gì nó nói, tại sao không đặt một chỉ mục trên #Driver? D.ID có thực sự độc đáo? Nếu vậy, điều đó tương đương về mặt ngữ nghĩa với câu lệnh EXISTS, ít nhất sẽ cho SQL Server biết rằng bạn không muốn tiếp tục tìm kiếm S cho các giá trị trùng lặp của D:

SELECT S.*
INTO #Results
FROM SampleTable S
WHERE EXISTS (SELECT * #Driver D WHERE S.ID = D.ID);

Nói tóm lại, đối với mẫu này, tôi sẽ không sử dụng gợi ý LOOP. Tôi chỉ đơn giản là không sử dụng mô hình này. Tôi sẽ làm một trong những điều sau đây, theo thứ tự ưu tiên nếu khả thi:

  • Sử dụng CTE thay vì bảng tạm thời cho #Driver nếu có thể
  • Sử dụng một chỉ mục không bao gồm duy nhất trên #Driver trên ID nếu nó là duy nhất (giả sử đây là lần duy nhất bạn sử dụng #Driver và bạn không muốn có bất kỳ dữ liệu nào từ chính bảng đó - nếu bạn thực sự cần dữ liệu từ bảng đó, bạn cũng có thể làm cho nó trở thành một chỉ mục cụm)
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.