ค้นหา 100x ช้าลงใน SQL Server 2014 แถว Row Spool ประมาณผู้ร้ายหรือไม่


13

ฉันมีแบบสอบถามที่วิ่งใน800 มิลลิวินาทีใน SQL Server 2012และใช้เวลาประมาณ170 วินาทีใน SQL Server 2014 ฉันคิดว่าฉันได้ จำกัด เรื่องนี้ให้แคบลงเพื่อประเมินความน่าจะเป็นของRow Count Spoolผู้ให้บริการ ฉันได้อ่านเกี่ยวกับตัวดำเนินการสปูลแล้ว (เช่นที่นี่และที่นี่ ) แต่ฉันยังคงมีปัญหาในการทำความเข้าใจบางสิ่ง:

  • เหตุใดแบบสอบถามนี้จึงต้องการRow Count Spoolผู้ดำเนินการ ฉันไม่คิดว่ามันจำเป็นสำหรับความถูกต้องดังนั้นสิ่งที่พยายามเพิ่มประสิทธิภาพโดยเฉพาะคืออะไร?
  • เหตุใด SQL Server จึงประมาณว่าการเข้าร่วมกับRow Count Spoolผู้ดำเนินการลบแถวทั้งหมดออก
  • นี่เป็นข้อบกพร่องใน SQL Server 2014 หรือไม่ ถ้าเป็นเช่นนั้นฉันจะยื่นในการเชื่อมต่อ แต่ฉันต้องการความเข้าใจที่ลึกซึ้งยิ่งขึ้นก่อน

หมายเหตุ: ฉันสามารถเขียนแบบสอบถามอีกครั้งเป็นLEFT JOINหรือเพิ่มดัชนีลงในตารางเพื่อให้ได้ประสิทธิภาพที่ยอมรับได้ทั้งใน SQL Server 2012 และ SQL Server 2014 ดังนั้นคำถามนี้เกี่ยวกับการทำความเข้าใจแบบสอบถามเฉพาะและแผนในเชิงลึกมากขึ้น วิธีวลีที่ค้นหาแตกต่างกัน


แบบสอบถามช้า

ดูPastebin นี้สำหรับสคริปต์ทดสอบฉบับเต็ม นี่คือคำถามทดสอบเฉพาะที่ฉันกำลังดู:

-- Prune any existing customers from the set of potential new customers
-- This query is much slower than expected in SQL Server 2014 
SELECT *
FROM #potentialNewCustomers -- 10K rows
WHERE cust_nbr NOT IN (
    SELECT cust_nbr
    FROM #existingCustomers -- 1MM rows
)


SQL Server 2014: แผนแบบสอบถามโดยประมาณ

SQL Server เชื่อว่าการLeft Anti Semi JoinถึงRow Count Spoolจะกรอง 10,000 แถวลงไปที่ 1 แถว ด้วยเหตุนี้มันเลือกสำหรับต่อมาเข้าร่วมLOOP JOIN#existingCustomers

ป้อนคำอธิบายรูปภาพที่นี่


SQL Server 2014: แผนแบบสอบถามจริง

ตามที่คาดไว้ (โดยทุกคนยกเว้น SQL Server!), Row Count Spoolไม่ได้ลบแถวใด ๆ ดังนั้นเราจึงวนซ้ำ 10,000 ครั้งเมื่อ SQL Server คาดว่าจะวนซ้ำเพียงครั้งเดียว

ป้อนคำอธิบายรูปภาพที่นี่


SQL Server 2012: แผนแบบสอบถามโดยประมาณ

เมื่อใช้ SQL Server 2012 (หรือOPTION (QUERYTRACEON 9481)ใน SQL Server 2014) Row Count Spoolจะไม่ลดจำนวนแถวโดยประมาณและเลือกการเข้าร่วมแฮชซึ่งส่งผลให้มีการวางแผนที่ดีขึ้น

ป้อนคำอธิบายรูปภาพที่นี่

ซ้ายเข้าร่วมใหม่เขียน

สำหรับการอ้างอิงต่อไปนี้เป็นวิธีที่ฉันอาจเขียนแบบสอบถามอีกครั้งเพื่อให้ได้ประสิทธิภาพที่ดีใน SQL Server 2012, 2014 และ 2016 ทั้งหมดอย่างไรก็ตามฉันยังคงสนใจในพฤติกรรมเฉพาะของแบบสอบถามด้านบนและไม่ว่าจะเป็น เป็นบั๊กในเครื่องมือประมาณการ Cardinality ใหม่ของ SQL Server 2014

-- Re-writing with LEFT JOIN yields much better performance in 2012/2014/2016
SELECT n.*
FROM #potentialNewCustomers n
LEFT JOIN (SELECT 1 AS test, cust_nbr FROM #existingCustomers) c
    ON c.cust_nbr = n.cust_nbr
WHERE c.test IS NULL

ป้อนคำอธิบายรูปภาพที่นี่

คำตอบ:


10

เหตุใดแบบสอบถามนี้จึงต้องการตัวดำเนินการ Row Count Spool ... มันพยายามเพิ่มประสิทธิภาพที่เฉพาะเจาะจงอะไร?

cust_nbrคอลัมน์#existingCustomersเป็น nullable หากจริง ๆ แล้วมันมีโมฆะใด ๆ การตอบสนองที่ถูกต้องที่นี่คือการกลับแถวศูนย์ ( NOT IN (NULL,...) จะให้ผลชุดที่ว่างเปล่าเสมอ)

ดังนั้นเคียวรีสามารถคิดได้ว่าเป็น

SELECT p.*
FROM   #potentialNewCustomers p
WHERE  NOT EXISTS (SELECT *
                   FROM   #existingCustomers e1
                   WHERE  p.cust_nbr = e1.cust_nbr)
       AND NOT EXISTS (SELECT *
                       FROM   #existingCustomers e2
                       WHERE  e2.cust_nbr IS NULL) 

ด้วยสปูล rowcount เพื่อหลีกเลี่ยงการประเมิน

EXISTS (SELECT *
        FROM   #existingCustomers e2
        WHERE  e2.cust_nbr IS NULL) 

มากกว่าหนึ่งครั้ง.

นี่ดูเหมือนจะเป็นกรณีที่ความแตกต่างเล็กน้อยในข้อสมมติฐานสามารถสร้างความแตกต่างในการทำงาน

หลังจากอัปเดตแถวเดียวดังนี้ ...

UPDATE #existingCustomers
SET    cust_nbr = NULL
WHERE  cust_nbr = 1;

... แบบสอบถามเสร็จสมบูรณ์ภายในเวลาไม่ถึงหนึ่งวินาที แถวนับในเวอร์ชันตามจริงและตามแผนโดยประมาณขณะนี้เกือบเป็นจุดแล้ว

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM   #potentialNewCustomers
WHERE  cust_nbr NOT IN (SELECT cust_nbr
                        FROM   #existingCustomers 
                       ) 

ป้อนคำอธิบายรูปภาพที่นี่

ศูนย์แถวเอาท์พุทตามที่อธิบายไว้ข้างต้น

สถิติฮิสโตแกรมและขีด จำกัด การอัปเดตอัตโนมัติใน SQL Server ไม่ละเอียดพอที่จะตรวจจับการเปลี่ยนแปลงแถวเดี่ยวชนิดนี้ได้ อาจเป็นไปได้ว่าคอลัมน์นั้นมีค่าเป็นโมฆะอาจมีเหตุผลที่จะทำงานบนพื้นฐานที่มีอย่างน้อยหนึ่งรายการNULLแม้ว่าฮิสโทแกรมสถิติจะไม่แสดงว่ามีอยู่ในขณะนี้


9

เหตุใดแบบสอบถามนี้จึงต้องการตัวดำเนินการ Row Count Spool ฉันไม่คิดว่ามันจำเป็นสำหรับความถูกต้องดังนั้นสิ่งที่พยายามเพิ่มประสิทธิภาพโดยเฉพาะคืออะไร?

ดูคำตอบอย่างละเอียดของ Martinสำหรับคำถามนี้ จุดสำคัญคือถ้าหากหนึ่งแถวภายในNOT INis NULL, ตรรกะบูลีนจะทำงานเช่นนั้น "การตอบสนองที่ถูกต้องคือการคืนค่าศูนย์แถว" Row Count Spoolผู้ประกอบการคือการเพิ่มประสิทธิภาพนี้ (จำเป็น) ตรรกะ

เหตุใด SQL Server จึงประมาณว่าการเข้าร่วมกับผู้ดำเนินการนับแถวเก็บพักเอาแถวทั้งหมดออกหรือไม่

ไมโครซอฟท์ยังมีกระดาษสีขาวที่ดีเยี่ยมใน SQL 2014 Cardinality ประมาณการ ในเอกสารนี้ฉันพบข้อมูลต่อไปนี้:

CE ใหม่สันนิษฐานว่าค่าการสอบถามมีอยู่ในชุดข้อมูลแม้ว่าค่าจะอยู่นอกช่วงของฮิสโตแกรม CE ใหม่ในตัวอย่างนี้ใช้ความถี่เฉลี่ยที่คำนวณโดยการคูณ cardinality ของตารางด้วยความหนาแน่น

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

แต่ในกรณีนี้โดยเฉพาะสมมติว่าNULLมูลค่าจะพบนำไปสู่ข้อสันนิษฐานว่าการเข้าร่วมกับจะกรองแถวทั้งหมดจากRow Count Spool #potentialNewCustomersในกรณีที่มีแถวจริงNULLนี่เป็นการประมาณที่ถูกต้อง (ดังที่เห็นในคำตอบของมาร์ติน) อย่างไรก็ตามในกรณีที่เกิดขึ้นไม่ได้เป็นNULLแถวผลกระทบที่สามารถทำลายล้างได้เนื่องจาก SQL Server สร้างการประเมินหลังการเข้าร่วม 1 แถวโดยไม่คำนึงถึงจำนวนแถวเข้าที่ปรากฏ สิ่งนี้สามารถนำไปสู่ตัวเลือกการเข้าร่วมที่แย่มากในส่วนที่เหลือของแผนแบบสอบถาม

นี่เป็นข้อบกพร่องใน SQL 2014 หรือไม่ ถ้าเป็นเช่นนั้นฉันจะยื่นในการเชื่อมต่อ แต่ฉันต้องการความเข้าใจที่ลึกซึ้งยิ่งขึ้นก่อน

ฉันคิดว่ามันอยู่ในพื้นที่สีเทาระหว่างข้อบกพร่องและข้อสมมติฐานที่ส่งผลกระทบต่อประสิทธิภาพหรือข้อ จำกัด ของ Cardinality Estimator ใหม่ของ SQL Server อย่างไรก็ตามการเล่นโวหารนี้สามารถทำให้เกิดการถดถอยอย่างมากในประสิทธิภาพเมื่อเทียบกับ SQL 2012 ในกรณีที่เฉพาะเจาะจงของNOT INประโยคที่ไม่มีค่าที่ไม่ได้เกิดขึ้นที่จะมีNULLค่าใด ๆ

ดังนั้นฉันได้ยื่นเรื่องการเชื่อมต่อเพื่อให้ทีม SQL ทราบถึงความเป็นไปได้ที่จะเกิดการเปลี่ยนแปลงนี้กับเครื่องมือประมาณการ Cardinality

อัปเดต:เราใช้ CTP3 ในขณะนี้สำหรับ SQL16 และฉันยืนยันว่าปัญหาไม่ได้เกิดขึ้นที่นั่น


5

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

ดังนั้นคำถามนี้มีมากขึ้นเกี่ยวกับการทำความเข้าใจแบบสอบถามเฉพาะนี้และวางแผนในเชิงลึกและน้อยกว่าเกี่ยวกับวิธีวลีที่แตกต่างกัน

วัตถุประสงค์ที่ระบุไว้ของแบบสอบถามคือ:

-- Prune any existing customers from the set of potential new customers

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

แสดงความต้องการเชิงตรรกะอย่างเต็มที่:

  • ส่งคืนผู้มีโอกาสเป็นลูกค้าที่ไม่ใช่ลูกค้าอยู่แล้ว
  • รายชื่อลูกค้าที่มีศักยภาพมากที่สุดครั้งเดียว
  • ยกเว้นลูกค้าที่มีศักยภาพและลูกค้าปัจจุบัน (ไม่ว่าลูกค้าจะเป็นโมฆะ)

จากนั้นเราสามารถเขียนแบบสอบถามที่ตรงกับความต้องการเหล่านั้นโดยใช้ไวยากรณ์ที่เราต้องการ ตัวอย่างเช่น:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr NOT IN
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

สิ่งนี้สร้างแผนการดำเนินการที่มีประสิทธิภาพซึ่งให้ผลลัพธ์ที่ถูกต้อง:

แผนการดำเนินการ

เราสามารถแสดงNOT INเป็น<> ALLหรือNOT = ANYไม่มีผลต่อแผนหรือผลลัพธ์:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    DPNNC.cust_nbr <> ALL
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );
WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE
    NOT DPNNC.cust_nbr = ANY
    (
        SELECT 
            EC.cust_nbr 
        FROM #existingCustomers AS EC 
        WHERE 
            EC.cust_nbr IS NOT NULL
    );

หรือใช้NOT EXISTS:

WITH DistinctPotentialNonNullCustomers AS
(
    SELECT DISTINCT 
        PNC.cust_nbr 
    FROM #potentialNewCustomers AS PNC
    WHERE 
        PNC.cust_nbr IS NOT NULL
)
SELECT
    DPNNC.cust_nbr
FROM DistinctPotentialNonNullCustomers AS DPNNC
WHERE 
    NOT EXISTS
    (
        SELECT * 
        FROM #existingCustomers AS EC
        WHERE
            EC.cust_nbr = DPNNC.cust_nbr
            AND EC.cust_nbr IS NOT NULL
    );

มีเวทมนตร์ไม่มีอะไรเกี่ยวกับเรื่องนี้โดยเฉพาะอย่างยิ่งหรืออะไรที่ไม่เหมาะสมเกี่ยวกับการใช้IN, ANYหรือALL- เราก็ต้องเขียนแบบสอบถามอย่างถูกต้องดังนั้นจึงมักจะก่อให้เกิดผลที่เหมาะสม

รูปแบบกะทัดรัดที่สุดใช้EXCEPT:

SELECT 
    PNC.cust_nbr 
FROM #potentialNewCustomers AS PNC
WHERE 
    PNC.cust_nbr IS NOT NULL
EXCEPT
SELECT
    EC.cust_nbr 
FROM #existingCustomers AS EC
WHERE 
    EC.cust_nbr IS NOT NULL;

สิ่งนี้ให้ผลลัพธ์ที่ถูกต้องเช่นกันแม้ว่าแผนการดำเนินการอาจมีประสิทธิภาพน้อยลงเนื่องจากไม่มีการกรองบิตแมป:

แผนการดำเนินการที่ไม่ใช่บิตแมป

คำถามดั้งเดิมนั้นน่าสนใจเพราะจะทำให้เกิดปัญหาเกี่ยวกับประสิทธิภาพกับการใช้งานการตรวจสอบโมฆะที่จำเป็น จุดของคำตอบนี้คือการเขียนแบบสอบถามอย่างถูกต้องเพื่อหลีกเลี่ยงปัญหาเช่นกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.