ลำดับของคำสั่งใน“ EXISTS (…) หรือ EXISTS (…)”


11

ฉันมีคลาสของคำถามที่ทดสอบการมีอยู่ของหนึ่งในสองสิ่งนี้ มันเป็นรูปแบบ

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

ข้อความจริงถูกสร้างขึ้นใน C และดำเนินการเป็นแบบสอบถามเฉพาะกิจผ่านการเชื่อมต่อ ODBC

เมื่อเร็ว ๆ นี้พบว่า SELECT ที่สองอาจเร็วกว่า SELECT แรกในกรณีส่วนใหญ่และการเปลี่ยนคำสั่งของสองประโยค EXISTS ทำให้เกิดการเร่งความเร็วอย่างมากในกรณีทดสอบที่ไม่เหมาะสมอย่างน้อยหนึ่งกรณีที่เราเพิ่งสร้างขึ้น

สิ่งที่ชัดเจนที่ต้องทำคือเดินหน้าต่อไปและสลับสองประโยค แต่ฉันต้องการดูว่ามีคนคุ้นเคยกับ SQL Server มากกว่านี้หรือไม่ รู้สึกเหมือนว่าฉันอาศัยความบังเอิญและ "รายละเอียดการนำไปปฏิบัติ"

(ดูเหมือนว่าถ้า SQL Server ฉลาดขึ้นก็จะดำเนินการทั้งสองข้อ EXISTS ในแบบคู่ขนานและปล่อยให้สิ่งใดสิ่งหนึ่งที่ทำให้เกิดการลัดวงจรเป็นครั้งแรก

มีวิธีที่ดีกว่าในการทำให้ SQL Server ปรับปรุงเวลาใช้งานของแบบสอบถามอย่างสม่ำเสมอหรือไม่?

ปรับปรุง

ขอบคุณสำหรับเวลาและความสนใจในคำถามของฉัน ฉันไม่ได้คาดหวังคำถามเกี่ยวกับแผนแบบสอบถามจริง แต่ฉันยินดีที่จะแบ่งปัน

สำหรับส่วนประกอบซอฟต์แวร์ที่รองรับ SQL Server 2008R2 ขึ้นไป รูปร่างของข้อมูลอาจแตกต่างกันมากขึ้นอยู่กับการกำหนดค่าและการใช้งาน เพื่อนร่วมงานของฉันคิดว่าจะทำการเปลี่ยนแปลงนี้กับแบบสอบถามเนื่องจากตาราง (ในตัวอย่าง) dbf_1162761$z$rv$1257927703จะมีจำนวนมากกว่าหรือเท่ากับจำนวนแถวในdbf_1162761$z$dd$1257927703ตารางนั้นมากกว่าตาราง - บางครั้งก็มีความหมายมากขึ้น (คำสั่งของขนาด)

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

สำหรับสิ่งที่คุ้มค่าบิต "เพิ่มประสิทธิภาพสำหรับ UNKNOWN" ก็ถูกเพิ่มเมื่อไม่นานมานี้ด้วยเนื่องจากการดมกลิ่นพารามิเตอร์กำลังทำลายบางกรณี

ข้อความค้นหาเดิม:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

แผนเดิม:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

ข้อความค้นหาคงที่:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

แผนคงที่:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)

1
คำถาม & คำตอบที่เกี่ยวข้อง: การต่อข้อมูลทางกายภาพ: รับประกันการดำเนินการตามคำสั่งหรือไม่?
Paul White 9

คำตอบ:


11

ตามกฎทั่วไปแล้ว SQL Server จะดำเนินการส่วนต่าง ๆ ของCASEคำสั่งตามลำดับ แต่มีอิสระในการจัดลำดับORเงื่อนไขใหม่ สำหรับคำค้นหาบางคำคุณสามารถรับประสิทธิภาพที่ดีขึ้นอย่างต่อเนื่องโดยการเปลี่ยนลำดับของWHENนิพจน์ภายในCASEคำสั่ง บางครั้งคุณสามารถได้รับประสิทธิภาพที่ดีขึ้นเมื่อเปลี่ยนลำดับของเงื่อนไขในORคำสั่ง แต่ไม่รับประกันพฤติกรรม

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

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

พิจารณาคำถามต่อไปนี้:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

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

สำหรับข้อมูลตัวอย่างนี้ฉันจะเขียนแบบสอบถามเช่นนี้

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

สิ่งนั้นบังคับให้ SQL Server เรียกใช้เคียวรีย่อยอย่างมีประสิทธิภาพกับตารางด้วยดัชนีคลัสเตอร์ก่อน นี่คือผลลัพธ์จากSET STATISTICS IO, TIME ON:

ตาราง 'X_CI' จำนวนการสแกน 0, การอ่านเชิงตรรกะ 3, การอ่านทางกายภาพ 0

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

ดูที่แผนคิวรีหากการค้นหาที่เลเบล 1 ส่งคืนข้อมูลใด ๆ ที่ไม่ใช่การสแกนที่เลเบล 2 ไม่จำเป็นและจะไม่เกิดขึ้น:

แบบสอบถามที่ดี

แบบสอบถามต่อไปนี้มีประสิทธิภาพน้อยกว่ามาก:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

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

แผนแบบสอบถามไม่ดี

ผลการดำเนินงานกลับมาที่:

ตาราง 'X_HEAP' จำนวนการสแกน 1, ตรรกะอ่าน 7247

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

กลับไปที่คิวรีดั้งเดิมสำหรับคิวรีนี้ฉันเห็นการค้นหาและการสแกนที่ประเมินตามลำดับที่ดีสำหรับประสิทธิภาพ:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

และในแบบสอบถามนี้พวกเขาจะถูกประเมินตามลำดับที่ตรงกันข้าม:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

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

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

ภาคผนวก:

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

นี่คือตัวเลือกหนึ่งที่ใช้งานได้กับตารางสาธิตอย่างง่าย:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

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


4
หรือCASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 ENDอาจเป็นทางเลือกแม้ว่าจะยังต้องอาศัยการตัดสินใจด้วยตนเองว่าแบบสอบถามใดเร็วกว่าและวางคำถามนั้นไว้ก่อน ฉันไม่แน่ใจว่ามีวิธีในการแสดงหรือไม่เพื่อที่ SQL Server จะเรียงลำดับใหม่โดยอัตโนมัติเพื่อให้ราคาถูกถูกประเมินโดยอัตโนมัติก่อน
Martin Smith
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.