ตรวจสอบการมีอยู่ของ EXISTS ดีกว่า COUNT! …ไม่


35

ฉันได้อ่านบ่อยเมื่อมีการตรวจสอบการดำรงอยู่ของแถวควรเสมอทำได้ด้วย EXISTS แทนกับการนับ

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

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

ฉันไม่คุ้นเคยกับวิธีการที่จะบอกว่าเกิดอะไรขึ้น "ภายใน" SQL Server ดังนั้นฉันจึงสงสัยว่ามีข้อผิดพลาดที่ไม่มีผู้แปลที่มี EXISTS ที่ให้ความรู้สึกที่สมบูรณ์แบบกับการวัดที่ฉันทำ

คุณมีคำอธิบายเกี่ยวกับปรากฏการณ์นั้นบ้างไหม?

แก้ไข:

นี่คือสคริปต์เต็มรูปแบบที่คุณสามารถเรียกใช้:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

ใน SQL Server 2008R2 (เซเว่น 64 บิต) ฉันได้รับผลนี้

COUNT เวอร์ชัน:

ตาราง '# 455F344D' จำนวนการสแกน 1, การอ่านเชิงตรรกะ 8, การอ่านทางกายภาพ 0, การอ่านล่วงหน้าอ่าน 0, การอ่านตรรกะล่วงหน้า lob 0, lob ทางกายภาพอ่าน 0, lob การอ่านล่วงหน้าอ่าน 0
ตาราง '# 492FC531' จำนวนการสแกน 1, อ่านโลจิคัล 30, อ่านฟิสิคัล 0, อ่านล่วงหน้าอ่าน 0, โลจิคัลล็อกอ่าน 0, lob อ่านฟิสิคัล 0, อ่านล็อบล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server:
เวลา CPU = 0 ms, เวลาที่ผ่านไป = 81 ms

EXISTS เวอร์ชัน:

ตาราง '# 492FC531' จำนวนการสแกน 1, การอ่านแบบลอจิคัล 96, การอ่านแบบฟิสิคัล 0, การอ่านล่วงหน้าอ่าน 0, การอ่านแบบลอจิคัล lob 0, lob ทางกายภาพอ่าน 0, lob การอ่านล่วงหน้าอ่าน 0
ตาราง '# 455F344D' จำนวนการสแกน 1, การอ่านเชิงตรรกะ 8, การอ่านทางกายภาพ 0, การอ่านล่วงหน้าอ่าน 0, lob ตรรกะอ่าน 0, lob การอ่านทางกายภาพ 0, lob การอ่านล่วงหน้าอ่าน 0

เวลาดำเนินการของ SQL Server:
เวลา CPU = 0 ms, เวลาที่ผ่านไป = 76 ms

คำตอบ:


43

ฉันได้อ่านบ่อยเมื่อมีการตรวจสอบการดำรงอยู่ของแถวควรเสมอทำได้ด้วย EXISTS แทนกับการนับ

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

EXISTSสำหรับสิ่งที่มันคุ้มค่าที่จะใช้ของตัวเองเกี่ยวกับเรื่องนี้ก็คือว่าคำสั่งการดำรงอยู่จะแสดงเป็นธรรมชาติมากที่สุดโดยใช้ มันก็เป็นประสบการณ์ของฉันที่EXISTS มีแนวโน้มที่จะเพิ่มประสิทธิภาพที่ดีกว่าทางเลือกการOUTER JOINปฏิเสธ NULLการใช้COUNT(*)และการกรอง=0เป็นอีกทางเลือกหนึ่งที่เกิดขึ้นกับการสนับสนุนบางอย่างในเครื่องมือเพิ่มประสิทธิภาพการสืบค้น SQL Server แต่ฉันพบว่าตัวเองไม่น่าเชื่อถือในแบบสอบถามที่ซับซ้อนมากขึ้น ไม่ว่าในกรณีใด ๆEXISTSดูเหมือนว่าจะเป็นธรรมชาติมากกว่าสำหรับทางเลือกเหล่านั้น

ฉันสงสัยว่ามีข้อบกพร่องที่ไม่มีใครเทียบได้กับ EXISTS ที่ให้ความรู้สึกสมบูรณ์แบบกับการวัดที่ฉันทำ

ตัวอย่างเฉพาะของคุณน่าสนใจเพราะมันเน้นวิธีที่เครื่องมือเพิ่มประสิทธิภาพเกี่ยวข้องกับแบบสอบถามย่อยในCASEนิพจน์ (และEXISTSโดยเฉพาะการทดสอบ)

เคียวรีย่อยในนิพจน์ CASE

พิจารณาคำถามค้นหา (ถูกกฎหมายอย่างสมบูรณ์) ต่อไปนี้:

DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);

SELECT
    CASE
        WHEN (SELECT W.b FROM @When AS W) = 1
            THEN (SELECT T.c FROM @Then AS T)
        ELSE (SELECT E.d FROM @Else AS E)
    END
FROM @Base AS B;

ความหมายของCASEที่WHEN/ELSEคำสั่งจะโดยทั่วไปการประเมินในการสั่งซื้อเกี่ยวกับใจ ในแบบสอบถามด้านบนมันจะไม่ถูกต้องสำหรับ SQL Server เพื่อส่งคืนข้อผิดพลาดถ้าELSEแบบสอบถามย่อยส่งคืนมากกว่าหนึ่งแถวถ้าWHENอนุประโยคเป็นที่พอใจ หากต้องการเคารพความหมายเหล่านี้เครื่องมือเพิ่มประสิทธิภาพจะสร้างแผนการที่ใช้ภาคแสดงการส่งผ่าน:

ภาคแสดงการส่งผ่าน

ด้านในของการรวมลูปซ้อนกันจะถูกประเมินเฉพาะเมื่อเพรดิเคตของ pass-through ส่งกลับค่า false ผลกระทบโดยรวมคือCASEการทดสอบนิพจน์ตามลำดับและแบบสอบถามย่อยจะได้รับการประเมินถ้าไม่พอใจก่อนหน้านี้

CASE นิพจน์ที่มีเคียวรี่ย่อย EXISTS

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

รายละเอียดของการนำไปปฏิบัตินั้นจะถูกแทนที่แบบสอบถามย่อยแบบโลจิคัลโดยการรวมที่สัมพันธ์ ('นำไปใช้') ด้วยคอลัมน์โพรบ งานจะดำเนินการโดยกฎการทำให้เข้าใจง่ายในเครื่องมือเพิ่มประสิทธิภาพแบบสอบถามที่เรียกว่าRemoveSubqInPrj(ลบแบบสอบถามย่อยในการประมาณการ) เราสามารถดูรายละเอียดโดยใช้ค่าสถานะการติดตาม 8606:

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);

ส่วนหนึ่งของแผนผังการป้อนข้อมูลที่แสดงการEXISTSทดสอบแสดงอยู่ด้านล่าง:

ScaOp_Exists 
    LogOp_Project
        LogOp_Select
            LogOp_Get TBL: #T2
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier [T2].ID
                ScaOp_Identifier [T1].ID

สิ่งนี้ถูกเปลี่ยนRemoveSubqInPrjเป็นโครงสร้างที่นำโดย:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

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

หนึ่งในรูปร่างของแผนการดำเนินการที่เป็นไปได้สำหรับเคียวรีนี้คือการนำโครงสร้างลอจิคัลไปใช้โดยตรง:

NLJ Semi เข้าร่วมกับโพรบ

Compute Scalar ขั้นสุดท้ายประเมินผลลัพธ์ของCASEนิพจน์โดยใช้ค่าคอลัมน์โพรบ:

คำนวณการแสดงออกของสเกลาร์

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

รวมกับคอลัมน์โพรบ

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

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

หากเราเพิ่มภาคการจับคู่BETWEENลงT2ในแบบสอบถามทั้งหมดที่เกิดขึ้นคือการตรวจสอบนี้จะดำเนินการสำหรับแต่ละแถวเป็นส่วนที่เหลือในการรวมกึ่งผสาน (ยากที่จะสังเกตเห็นในแผนการดำเนินการ แต่มี)

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
            AND T2.ID BETWEEN 5000 AND 7000 -- New
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

เพรดิเคตที่เหลือ

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

มีวิธีการเขียนแบบสอบถามเพื่อสร้างการค้นหาในอินพุตทั้งสองไปยังการรวมกึ่งผสาน วิธีหนึ่งเกี่ยวข้องกับการเขียนแบบสอบถามในลักษณะที่ค่อนข้างแปลกประหลาด (เอาชนะเหตุผลที่ฉันชอบโดยทั่วไปEXISTS):

WITH T2 AS
(
    SELECT TOP (9223372036854775807) * 
    FROM #T2 AS T2 
    WHERE ID BETWEEN 5000 AND 7000
)
SELECT 
    T1.ID, 
    DoesExist = 
        CASE 
            WHEN EXISTS 
            (
                SELECT * FROM T2 
                WHERE T2.ID = T1.ID
            ) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

แผนหลอกลวง

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


6

"COUNT (*) VS EXISTS" อาร์กิวเมนต์คือจะทำอย่างไรกับการตรวจสอบว่ามีการบันทึกอยู่ ตัวอย่างเช่น:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

VS

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

สคริปต์ SQL ของคุณไม่ได้ใช้COUNT(*)เป็นบันทึกที่มีอยู่ตรวจสอบและดังนั้นฉันจะไม่บอกว่ามันใช้ในสถานการณ์ของคุณ


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