การสแกนแม้ว่าฉันคาดหวังการค้นหา


9

ฉันต้องการเพิ่มประสิทธิภาพของSELECTคำสั่ง แต่ SQL Server จะทำการสแกนดัชนีแทนการค้นหา นี่คือแบบสอบถามที่แน่นอนอยู่ในขั้นตอนการจัดเก็บ:

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

และนี่คือดัชนี:

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

แผนการ:

วางแผนรูปภาพ

เหตุใด SQL Server จึงเลือกการสแกน ฉันจะแก้ไขได้อย่างไร

คำจำกัดความของคอลัมน์:

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

พารามิเตอร์สถานะสามารถเป็น:

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

IsUserGotAnActiveDirectoryUser สามารถ:

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

คุณสามารถโพสต์แผนปฏิบัติการจริงที่อื่น (ไม่ใช่รูปภาพของมัน แต่เป็นไฟล์. sqlplan ในรูปแบบ XML) ฉันเดาว่าคุณเปลี่ยนแปลงขั้นตอน แต่ไม่ได้รับการคอมไพล์ใหม่ที่ระดับคำสั่ง คุณสามารถเปลี่ยนข้อความของแบบสอบถาม (เช่นเพิ่มคำนำหน้าสคีมาเป็นชื่อตาราง ) แล้วส่งผ่านค่าที่ถูกต้องได้@Statusหรือไม่
Aaron Bertrand

1
นอกจากนี้ยังมีความหมายดัชนี begs คำถาม - ทำไมเป็นกุญแจสำคัญในStatus DESC? มีกี่ค่าสำหรับStatusสิ่งที่พวกเขา (ถ้าจำนวนมีขนาดเล็ก) และแต่ละค่าแสดงอย่างเท่าเทียมกัน? แสดงผลลัพธ์ของSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

คำตอบ:


11

ฉันไม่คิดว่าการสแกนเกิดจากการค้นหาสตริงว่างเปล่า (และในขณะที่คุณสามารถเพิ่มดัชนีที่กรองแล้วสำหรับกรณีนั้นจะช่วยเฉพาะรูปแบบเฉพาะของการสืบค้น) คุณมีแนวโน้มที่จะตกเป็นเหยื่อของการดมกลิ่นพารามิเตอร์และแผนเดียวไม่เหมาะสำหรับการรวมกันของพารามิเตอร์ต่างๆ (และค่าพารามิเตอร์) ทั้งหมดที่คุณจะให้กับแบบสอบถามนี้

ฉันเรียกสิ่งนี้ว่า "อ่างล้างมือในครัว" ขั้นตอนเพราะคุณคาดหวังหนึ่งแบบสอบถามเพื่อให้ทุกสิ่งรวมถึงอ่างครัว

ฉันมีวิดีโอเกี่ยวกับวิธีแก้ปัญหาของฉันที่นี่และที่นี่รวมถึงโพสต์บล็อกเกี่ยวกับเรื่องนี้แต่โดยหลักแล้วประสบการณ์ที่ดีที่สุดที่ฉันมีสำหรับข้อความค้นหาดังกล่าวคือ:

  • สร้างคำสั่งแบบไดนามิก - นี้จะช่วยให้คุณสามารถออกคำสั่งการกล่าวขวัญคอลัมน์ที่ไม่มีพารามิเตอร์ถูกจัดและทำให้มั่นใจว่าคุณจะมีแผนการที่จะเพิ่มประสิทธิภาพได้อย่างแม่นยำสำหรับพารามิเตอร์ที่เกิดขึ้นจริงที่ถูกส่งผ่านไปกับค่านิยม
  • ใช้OPTION (RECOMPILE) - สิ่งนี้จะป้องกันค่าพารามิเตอร์ที่เฉพาะเจาะจงจากการบังคับแผนผิดประเภทโดยเฉพาะอย่างยิ่งมีประโยชน์เมื่อคุณมีข้อมูลเอียงสถิติที่ไม่ดีหรือเมื่อการดำเนินการครั้งแรกของคำสั่งใช้ค่าผิดปรกติที่จะนำไปสู่แผนแตกต่างกัน การประหารชีวิต
  • ใช้ตัวเลือกเซิร์ฟเวอร์optimize for ad hoc workloads - นี่เป็นการป้องกันการเปลี่ยนแปลงของคิวรีที่ใช้เพียงครั้งเดียวจากการสร้างมลภาวะแคชของแผน

เปิดใช้งานการเพิ่มประสิทธิภาพสำหรับปริมาณงานเฉพาะกิจ:

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;

เปลี่ยนขั้นตอนของคุณ:

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

เมื่อคุณมีเวิร์กโหลดตามชุดของคิวรีที่คุณสามารถตรวจสอบได้คุณสามารถวิเคราะห์การประมวลผลและดูว่าอันไหนที่จะได้รับประโยชน์มากที่สุดจากดัชนีเพิ่มเติมหรือแตกต่างกัน - คุณสามารถทำสิ่งนี้ได้จากหลายมุมมอง มีการระบุพารามิเตอร์บ่อยที่สุดหรือไม่ " เป็น "ข้อความค้นหาใดที่มีระยะเวลานานที่สุด" เราไม่สามารถตอบคำถามเหล่านั้นตามรหัสของคุณเท่านั้นเราสามารถแนะนำได้ว่าดัชนีใด ๆจะมีประโยชน์สำหรับชุดย่อยของชุดพารามิเตอร์ที่เป็นไปได้ทั้งหมดที่คุณพยายามให้การสนับสนุน ตัวอย่างเช่นถ้า@Statusเป็น NULL ดังนั้นจึงไม่สามารถค้นหาดัชนีที่ไม่ใช่คลัสเตอร์ได้ ดังนั้นสำหรับกรณีที่ผู้ใช้ไม่สนใจสถานะคุณจะได้รับการสแกนเว้นแต่คุณจะมีดัชนีที่ให้บริการกับส่วนคำสั่งอื่น ๆ (แต่ดัชนีดังกล่าวจะไม่มีประโยชน์เช่นกันเนื่องจากตรรกะการสืบค้นปัจจุบันของคุณ - สตริงว่างหรือสตริงว่างเปล่าไม่ได้เลือกอย่างแน่นอน)

ในกรณีนี้ขึ้นอยู่กับชุดของStatusค่าที่เป็นไปได้และวิธีการกระจายค่าเหล่านั้นOPTION (RECOMPILE)อาจไม่จำเป็น แต่ถ้าคุณมีค่าบางอย่างที่จะให้ผล 100 แถวและบางค่าที่จะให้ผลแสนคุณอาจต้องการที่นั่น (แม้จะมีค่าใช้จ่ายของ CPU ซึ่งควรจะได้รับความซับซ้อนของแบบสอบถามนี้) เพื่อให้คุณสามารถ รับการค้นหาในหลาย ๆ กรณีให้มากที่สุด หากช่วงของค่ามี จำกัด เพียงพอคุณสามารถทำอะไรที่ยุ่งยากกับ dynamic SQL ซึ่งคุณบอกว่า "ฉันมีค่าที่เลือกได้มากสำหรับ@Statusนี้ดังนั้นเมื่อค่าเฉพาะนั้นถูกส่งผ่านให้ทำการเปลี่ยนแปลงเล็กน้อยกับข้อความแบบสอบถามเพื่อให้ นี่ถือว่าเป็นคิวรีอื่นและปรับให้เหมาะสมสำหรับค่าพารามิเตอร์นั้น "


3
ฉันใช้วิธีนี้หลายครั้งและเป็นวิธีที่ยอดเยี่ยมในการเพิ่มประสิทธิภาพให้ทำสิ่งที่คุณคิดว่าควรทำ Kim Tripp พูดถึงวิธีแก้ปัญหาที่คล้ายกันที่นี่: sqlskills.com/blogs/kimberly/high-performance-procedures และมีวิดีโอของเซสชันที่เธอทำที่ PASS สองสามปีที่ผ่านมา ที่กล่าวว่ามันไม่ได้เพิ่มตันกับสิ่งที่นายเบอร์ทรานด์กล่าวที่นี่ นี่เป็นหนึ่งในเครื่องมือที่ทุกคนควรเก็บไว้ในแถบเครื่องมือ มันสามารถบันทึกความเจ็บปวดขนาดใหญ่บางอย่างสำหรับการค้นหาที่ดักจับทั้งหมด
mskinner

3

คำเตือน : บางสิ่งในคำตอบนี้อาจทำให้ DBA สะดุ้ง ฉันกำลังเข้าใกล้จากจุดยืนของประสิทธิภาพที่แท้จริง - วิธีรับดัชนีค้นหาเมื่อคุณรับดัชนีสแกน

เมื่อพ้นทางแล้วล่ะก็

ข้อความค้นหาของคุณคือสิ่งที่รู้จักกันในชื่อ "แบบสอบถามอ่างล้างจานในครัว" - ข้อความค้นหาเดียวหมายถึงการตอบสนองเงื่อนไขการค้นหาที่หลากหลาย หากผู้ใช้ตั้ง@statusค่าคุณต้องการกรองสถานะนั้น หาก@statusเป็นNULLเช่นนั้นให้ส่งคืนสถานะทั้งหมดและอื่น ๆ

สิ่งนี้จะนำเสนอปัญหาเกี่ยวกับการจัดทำดัชนี แต่ไม่เกี่ยวข้องกับการระบุเป้าหมายเนื่องจากเงื่อนไขการค้นหาทั้งหมดของคุณเป็นเกณฑ์ "เท่ากับ"

นี่คือ sargable:

WHERE [status]=@status

สิ่งนี้ไม่สามารถทำได้เนื่องจาก SQL Server ต้องการประเมินISNULL([status], 0)สำหรับทุกแถวแทนที่จะค้นหาค่าเดียวในดัชนี:

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

ฉันได้สร้างอ่างครัวที่มีปัญหาใหม่ในรูปแบบที่เรียบง่าย:

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

หากคุณลองทำสิ่งต่อไปนี้คุณจะได้รับการสแกนดัชนีแม้ว่า A เป็นคอลัมน์แรกของดัชนี:

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

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

อย่างไรก็ตามสิ่งนี้สร้างดัชนีการค้นหา:

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

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

ตราบใดที่คุณใช้พารามิเตอร์จำนวนที่จัดการได้ (สองตัวในกรณีของคุณ) คุณอาจจะแค่UNIONค้นหาข้อความจำนวนมากโดยทั่วไปแล้วการเรียงสับเปลี่ยนทั้งหมดของเกณฑ์การค้นหา หากคุณมีเกณฑ์สามข้อนี้จะดูยุ่งเหยิงโดยมีสี่ข้อที่ไม่สามารถจัดการได้อย่างสมบูรณ์ คุณได้รับการเตือน

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;

สำหรับหนึ่งในสามของผู้ที่สี่ที่จะใช้ดัชนีแสวงหาคุณจะต้องมีดัชนีที่สองใน(B, A)แต่ ต่อไปนี้เป็นวิธีที่คิวรีของคุณอาจดูด้วยการเปลี่ยนแปลงเหล่านี้ (รวมถึงการสร้างการสืบค้นใหม่เพื่อให้สามารถอ่านได้มากขึ้น)

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

... รวมทั้งคุณจะต้องมีดัชนีเพิ่มเติมEmployeeโดยที่ดัชนีคอลัมน์ทั้งสองกลับด้าน

เพื่อความสมบูรณ์ควรจะกล่าวถึงที่x=@xหมายโดยปริยายว่าxไม่สามารถเป็นNULLเพราะจะไม่เท่ากับNULL NULLทำให้แบบสอบถามง่ายขึ้นเล็กน้อย

และใช่คำตอบ SQL แบบไดนามิกของ Aaron Bertrand เป็นตัวเลือกที่ดีกว่าในกรณีส่วนใหญ่ (เช่นเมื่อใดก็ตามที่คุณสามารถอยู่กับ recompiles ได้


3

คำถามพื้นฐานของคุณดูเหมือนจะเป็น "ทำไม" และฉันคิดว่าคุณอาจพบคำตอบประมาณ 55 นาทีหรือมากกว่านั้นในการนำเสนอที่ยอดเยี่ยมโดย Adam Machanic ที่ TechEdเมื่อไม่กี่ปีที่ผ่านมา

ฉันพูดถึง 5 นาทีต่อนาที 55 แต่การนำเสนอทั้งหมดมีค่าเวลา หากคุณดูที่แผนแบบสอบถามสำหรับการค้นหาของคุณฉันมั่นใจว่าคุณจะพบว่ามีเพรดิเคตที่เหลือสำหรับการค้นหา โดยทั่วไป SQL ไม่สามารถ "เห็น" ทุกส่วนของดัชนีได้เนื่องจากบางส่วนถูกซ่อนอยู่โดยความไม่เท่าเทียมกันและเงื่อนไขอื่น ๆ ผลลัพธ์ที่ได้คือการสแกนดัชนีสำหรับชุดซุปเปอร์ตาม Predicate ผลลัพธ์นั้นจะถูกสพูลแล้วสแกนอีกครั้งโดยใช้เพรดิเคตที่เหลือ

ตรวจสอบคุณสมบัติของ Scan Operator (F4) และดูว่าคุณมีทั้ง "Seek Predicate" และ "Predicate" ในรายการคุณสมบัติหรือไม่

ดังที่คนอื่น ๆ ระบุไว้แบบสอบถามนั้นยากที่จะจัดทำดัชนีตามที่เป็นอยู่ ฉันได้ทำงานกับคนที่คล้ายกันหลายคนเมื่อเร็ว ๆ นี้และแต่ละคนต้องการโซลูชันที่แตกต่าง :(


0

ก่อนที่เราจะถามว่าการค้นหาดัชนีเป็นที่ต้องการมากกว่าการสแกนดัชนีหรือไม่กฎข้อหนึ่งคือการตรวจสอบว่ามีการส่งคืนแถวจำนวนเท่าใดเทียบกับจำนวนแถวทั้งหมดของตารางต้นแบบ ตัวอย่างเช่นหากคุณคาดว่าแบบสอบถามของคุณจะส่งคืน 10 แถวจาก 1 ล้านแถวดังนั้นการค้นหาดัชนีอาจเป็นที่ต้องการมากกว่าการสแกนดัชนี อย่างไรก็ตามหากต้องการส่งคืนแถวสองสามพันแถว (หรือมากกว่า) จากแบบสอบถามการค้นหาดัชนีอาจไม่ต้องการ

ข้อความค้นหาของคุณไม่ซับซ้อนดังนั้นหากคุณสามารถโพสต์แผนการดำเนินการเราอาจมีแนวคิดที่ดีกว่าที่จะช่วยเหลือคุณ


กรองสองสามพันแถวจากตาราง 1 ล้านฉันยังคงต้องการการค้นหา - มันยังคงมีการปรับปรุงประสิทธิภาพมากมายในการสแกนทั้งตาราง
Daniel Hutmacher

-6

นี่เป็นเพียงรูปแบบดั้งเดิม

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

นี่คือการแก้ไข - ไม่แน่ใจ 100% เกี่ยวกับมัน แต่ (อาจจะ) ลอง
แม้แต่หนึ่งหรืออาจจะเป็นปัญหา
นี้จะทำลาย ActiveDirectoryUser null

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

3
มันไม่ชัดเจนสำหรับฉันว่าคำตอบนี้แก้คำถามของ OP ได้อย่างไร
Erik

@ เอริคเราอยากให้โอพีลองดูหน่อยได้ไหม? สองคนหรือออกไป คุณรู้หรือไม่ว่าสิ่งนี้ไม่สามารถช่วยในการค้นหาได้
paparazzo

@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser IS NOT NULL ถูกลบ ทั้งสองไม่จำเป็นต้องลบ OR และลบ IsUserGotAnActiveDirectoryUser IS NULL คุณแน่ใจหรือไม่ว่าแบบสอบถามนี้จะไม่ทำงานอย่างรวดเร็วหลังจาก OP
paparazzo

@ ypercubeᵀᴹอาจทำสิ่งต่าง ๆ ได้มากมาย ฉันไม่ได้มองหาง่ายขึ้น สองหรือหายไป หรือโดยทั่วไปจะไม่ดีสำหรับแผนแบบสอบถาม ฉันไปถึงที่นั่นเป็นสโมสรแบบหนึ่งและฉันไม่ได้เป็นส่วนหนึ่งของสโมสร แต่ฉันทำเพื่อหาเลี้ยงชีพและโพสต์สิ่งที่ฉันรู้ว่าได้ผล คำตอบของฉันไม่ได้รับผลกระทบจากคะแนนโหวตลง
paparazzo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.