Nhận quét mặc dù tôi mong đợi một tìm kiếm


9

Tôi cần tối ưu hóa một SELECTcâu lệnh nhưng SQL Server luôn thực hiện quét chỉ mục thay vì tìm kiếm. Đây là truy vấn, tất nhiên, là trong một thủ tục được lưu trữ:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

Và đây là chỉ số:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

Kế hoạch:

Kế hoạch hình ảnh

Tại sao SQL Server chọn quét? Làm thế nào tôi có thể sửa chữa nó?

Định nghĩa cột:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

Thông số trạng thái có thể là:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser có thể là:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)

Bạn có thể đăng kế hoạch thực hiện thực tế ở đâu đó (không phải là hình ảnh của nó, nhưng tệp .sqlplan ở dạng XML) không? Tôi đoán là bạn đã thay đổi thủ tục nhưng thực tế không có được một bản biên dịch mới ở cấp độ câu lệnh. Bạn có thể thay đổi một số văn bản của truy vấn (như thêm tiền tố lược đồ vào tên bảng ), sau đó chuyển vào một giá trị hợp lệ cho @Status?
Aaron Bertrand

1
Ngoài ra định nghĩa chỉ số đặt ra câu hỏi - tại sao lại có khóa Status DESC? Có bao nhiêu giá trị Status, chúng là gì (nếu số lượng nhỏ) và mỗi giá trị được biểu diễn gần như bằng nhau? Cho chúng tôi thấy đầu ra củaSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Câu trả lời:


11

Tôi không nghĩ rằng việc quét được gây ra bởi một tìm kiếm cho một chuỗi trống (và trong khi bạn có thể thêm một chỉ mục được lọc cho trường hợp đó, nó sẽ chỉ giúp các biến thể rất cụ thể của truy vấn). Bạn có nhiều khả năng là nạn nhân của việc đánh hơi tham số và một kế hoạch duy nhất không được tối ưu hóa cho tất cả các kết hợp tham số khác nhau (và giá trị tham số) mà bạn sẽ cung cấp cho truy vấn này.

Tôi gọi đây là thủ tục "bồn rửa nhà bếp" , bởi vì bạn đang mong đợi một truy vấn sẽ cung cấp tất cả mọi thứ, bao gồm cả bồn rửa nhà bếp.

Tôi có một video về giải pháp của mình cho vấn đề này ở đây , nhưng về cơ bản, trải nghiệm tốt nhất tôi có cho các truy vấn đó là:

  • Xây dựng câu lệnh một cách linh hoạt - điều này sẽ cho phép bạn bỏ qua các mệnh đề đề cập đến các cột không có tham số nào được cung cấp và đảm bảo rằng bạn sẽ có một kế hoạch được tối ưu hóa chính xác cho các tham số thực tế được truyền với các giá trị.
  • Sử dụngOPTION (RECOMPILE) - điều này ngăn các giá trị tham số cụ thể buộc loại kế hoạch sai, đặc biệt hữu ích khi bạn có dữ liệu sai lệch, thống kê xấu hoặc khi lần thực hiện đầu tiên của câu lệnh sử dụng giá trị không điển hình sẽ dẫn đến một kế hoạch khác so với sau và thường xuyên hơn hành quyết.
  • Sử dụng tùy chọn máy chủoptimize for ad hoc workloads - điều này ngăn các biến thể truy vấn chỉ được sử dụng một lần khỏi gây ô nhiễm bộ nhớ cache của gói.

Cho phép tối ưu hóa cho khối lượng công việc ad hoc:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Thay đổi thủ tục của bạn:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Khi bạn có khối lượng công việc dựa trên tập các truy vấn mà bạn có thể theo dõi, bạn có thể phân tích các lần thực hiện và xem cái nào sẽ có lợi nhất từ ​​các chỉ mục bổ sung hoặc khác nhau - bạn có thể thực hiện việc này từ nhiều góc độ khác nhau, từ sự kết hợp đơn giản của " thông số được cung cấp thường xuyên nhất? " đến "truy vấn cá nhân nào có thời gian chạy dài nhất?" Chúng tôi không thể trả lời những câu hỏi đó chỉ dựa trên mã của bạn, chúng tôi chỉ có thể đề xuất đó mọi chỉ mục sẽ chỉ hữu ích cho một tập hợp con của tất cả các kết hợp tham số có thể bạn đang cố gắng hỗ trợ. Ví dụ, nếu@Statuslà NULL, sau đó không thể tìm kiếm đối với chỉ số không phân cụm đó. Vì vậy, đối với những trường hợp người dùng không quan tâm đến trạng thái, bạn sẽ quét, trừ khi bạn có một chỉ mục phục vụ cho các mệnh đề khác (nhưng chỉ mục đó sẽ không hữu ích, dựa trên logic truy vấn hiện tại của bạn - chuỗi rỗng hoặc chuỗi rỗng không chọn lọc chính xác).

Trong trường hợp này, tùy thuộc vào tập hợp các Statusgiá trị có thể và cách phân phối các giá trị đó, OPTION (RECOMPILE)có thể không cần thiết. Nhưng nếu bạn có một số giá trị sẽ mang lại 100 hàng và một số giá trị sẽ mang lại hàng trăm nghìn, bạn có thể muốn nó ở đó (ngay cả với chi phí CPU, nên có biên độ do sự phức tạp của truy vấn này), để bạn có thể tìm kiếm trong càng nhiều trường hợp càng tốt. Nếu phạm vi của các giá trị là đủ hữu hạn, bạn thậm chí có thể làm điều gì đó khó khăn với SQL động, nơi bạn nói "Tôi có giá trị rất chọn lọc này @Status, vì vậy khi giá trị cụ thể đó được thông qua, hãy thực hiện thay đổi nhỏ này cho văn bản truy vấn để đây được coi là một truy vấn khác nhau và được tối ưu hóa cho giá trị param đó. "


3
Tôi đã sử dụng phương pháp này nhiều lần và đó là một cách tuyệt vời để khiến trình tối ưu hóa thực hiện mọi thứ theo cách mà bạn nghĩ nó nên làm theo cách nào. Kim Tripp nói về một giải pháp tương tự ở đây: sqlskills.com/bloss/kimberly/high-performance-procedures Và có một video về một phiên cô đã làm tại PASS vài năm trước, thực sự đi sâu vào chi tiết về lý do tại sao nó hoạt động. Điều đó nói rằng, nó thực sự không thêm một tấn vào những gì ông Bertrand đã nói ở đây. Đây là một trong những công cụ mà mọi người nên giữ trong hộp công cụ của mình. Nó thực sự có thể tiết kiệm một số nỗi đau lớn cho những truy vấn bắt tất cả.
mskinner

3

Tuyên bố miễn trừ trách nhiệm : Một số nội dung trong câu trả lời này có thể khiến DBA nao núng. Tôi đang tiếp cận nó từ quan điểm hiệu suất thuần túy - làm thế nào để có được Tìm kiếm chỉ mục khi bạn luôn nhận được Quét chỉ mục.

Với điều đó ra khỏi đường đi, ở đây đi.

Truy vấn của bạn là những gì được gọi là "truy vấn bồn rửa nhà bếp" - một truy vấn duy nhất có nghĩa là để phục vụ cho một loạt các điều kiện tìm kiếm có thể. Nếu người dùng đặt @statusthành một giá trị, bạn muốn lọc theo trạng thái đó. Nếu @statusNULL, trả lại tất cả các trạng thái, và như vậy.

Điều này đưa ra các vấn đề với việc lập chỉ mục, nhưng chúng không liên quan đến tính khả dụng, bởi vì tất cả các điều kiện tìm kiếm của bạn đều "bằng" tiêu chí.

Đây là sargable:

WHERE [status]=@status

Điều này không thể thực hiện được vì SQL Server cần đánh giá ISNULL([status], 0)cho mỗi hàng thay vì tìm kiếm một giá trị duy nhất trong chỉ mục:

WHERE ISNULL([status], 0)=@status

Tôi đã tạo lại vấn đề bồn rửa nhà bếp ở dạng đơn giản hơn:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Nếu bạn thử các cách sau, bạn sẽ nhận được Quét chỉ mục, mặc dù A là cột đầu tiên của chỉ mục:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Tuy nhiên, điều này tạo ra Chỉ số Tìm kiếm:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

Miễn là bạn đang sử dụng một lượng tham số có thể quản lý (hai trong trường hợp của bạn), bạn có thể chỉ cần UNIONmột loạt các truy vấn tìm kiếm - về cơ bản là tất cả các hoán vị của tiêu chí tìm kiếm. Nếu bạn có ba tiêu chí, điều này sẽ trông lộn xộn, với bốn tiêu chí sẽ hoàn toàn không thể quản lý được. Mày đã được cảnh báo.

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Đối với người thứ ba trong số bốn người đó sử dụng Chỉ mục Tìm kiếm, bạn sẽ cần một chỉ mục thứ hai (B, A). Đây là cách truy vấn của bạn có thể trông như thế nào với những thay đổi này (bao gồm cả việc tái cấu trúc truy vấn của tôi để làm cho nó dễ đọc hơn).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... cộng với bạn sẽ cần một chỉ mục bổ sung Employeevới hai cột chỉ mục được đảo ngược.

Để cho đầy đủ, tôi nên đề cập rằng điều đó x=@xcó nghĩa là xkhông thể NULLbởi vì NULLkhông bao giờ bằng NULL. Điều đó đơn giản hóa các truy vấn một chút.

Và, vâng, câu trả lời SQL động của Aaron Bertrand là một lựa chọn tốt hơn trong hầu hết các trường hợp (tức là bất cứ khi nào bạn có thể sống với các biên dịch lại).


3

Câu hỏi cơ bản của bạn dường như là "Tại sao" và tôi nghĩ rằng bạn có thể tìm thấy câu trả lời về phút 55 hoặc hơn của bài thuyết trình tuyệt vời này của Adam Machanic tại TechEd vài năm trước.

Tôi đề cập đến 5 phút ở phút 55 nhưng toàn bộ bài thuyết trình đáng để dành thời gian. Nếu bạn nhìn vào kế hoạch truy vấn cho truy vấn của mình, tôi chắc chắn bạn sẽ thấy nó có Dự đoán dư cho tìm kiếm. Về cơ bản SQL không thể "nhìn thấy" tất cả các phần của chỉ mục vì một số trong số chúng bị ẩn bởi sự bất bình đẳng và các điều kiện khác. Kết quả là quét chỉ mục cho một siêu tập hợp dựa trên Vị ngữ. Kết quả đó được lưu lại và sau đó được quét lại bằng cách sử dụng biến vị ngữ còn lại.

Kiểm tra các thuộc tính của Toán tử quét (F4) và xem bạn có cả "Tìm kiếm vị ngữ" và "Vị ngữ" trong danh sách thuộc tính không.

Như những người khác đã chỉ ra, truy vấn rất khó để lập chỉ mục. Tôi đã làm việc trên nhiều cái tương tự gần đây và mỗi cái cần một giải pháp khác nhau. :


0

Trước khi chúng tôi đặt câu hỏi liệu tìm kiếm chỉ mục có được ưu tiên hơn quét chỉ mục hay không, một nguyên tắc chung là kiểm tra xem có bao nhiêu hàng được trả về so với tổng số hàng của bảng bên dưới. Ví dụ: nếu bạn mong muốn truy vấn của mình trả về 10 hàng trong số 1 triệu hàng, thì tìm kiếm chỉ mục có thể được ưu tiên cao hơn quét chỉ mục. Tuy nhiên, nếu một vài nghìn hàng (hoặc nhiều hơn) được trả về từ truy vấn, thì tìm kiếm chỉ mục có thể KHÔNG nhất thiết được ưu tiên.

Truy vấn của bạn không phức tạp, vì vậy nếu bạn có thể đăng kế hoạch thực hiện, chúng tôi có thể có ý tưởng tốt hơn để hỗ trợ bạn.


Lọc vài nghìn hàng từ một bảng 1 triệu, tôi vẫn muốn tìm kiếm - đó vẫn là một cải tiến hiệu suất lớn so với việc quét toàn bộ bảng.
Daniel Hutmacher

-6

đây chỉ là định dạng ban đầu

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

đây là bản sửa đổi - không chắc chắn 100% về nó nhưng (có thể) hãy dùng thử
ngay cả một HOẶC có thể sẽ là một vấn đề
sẽ xảy ra trên ActiveDirectoryUser null

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )

3
Tôi không rõ câu trả lời này giải quyết câu hỏi của OP như thế nào.
Erik

@Erik Chúng tôi có thể thích OP để thử không? Hai OR đã đi xa. Bạn có biết chắc chắn điều này không thể giúp thực hiện truy vấn?
paparazzo

@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser KHÔNG phải là NULL bị xóa. Hai cái đó không cần thiết loại bỏ OR và loại bỏ IsUserGotAnActiveDirectoryUser IS NULL. Bạn có chắc chắn truy vấn này sẽ không chạy nhanh sau đó OP?
paparazzo

@ ypercubeᵀᴹ Có thể đã làm rất nhiều thứ. Tôi không tìm kiếm đơn giản hơn. Hai Hoặc đã biến mất. Hoặc thường là xấu cho kế hoạch truy vấn. Tôi nhận được có một loại câu lạc bộ ở đây và tôi không phải là một phần của câu lạc bộ. Nhưng tôi làm điều này để kiếm sống và đăng những gì tôi biết đã làm việc. Câu trả lời của tôi không bị ảnh hưởng bởi phiếu bầu.
paparazzo
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.