เหตุใดจึงมีแผนปฏิบัติการแตกต่างกันระหว่าง OFFSET … FETCH และแบบ ROW_NUMBER แบบเก่า


15

OFFSET ... FETCHรุ่นใหม่แนะนำด้วย SQL Server 2012 เสนอการเพจที่ง่ายและเร็วขึ้น ทำไมจึงมีความแตกต่างใด ๆ เมื่อพิจารณาว่าทั้งสองรูปแบบมีความหมายเหมือนกันและเป็นเรื่องธรรมดามาก?

ใครจะสันนิษฐานว่าเครื่องมือเพิ่มประสิทธิภาพรับรู้ทั้งสองและเพิ่มประสิทธิภาพพวกเขา (เล็กน้อย) อย่างเต็มที่

นี่เป็นกรณีที่ง่ายมากซึ่งOFFSET ... FETCHเร็วกว่าประมาณ 2 เท่าตามประมาณการต้นทุน

SELECT * INTO #objects FROM sys.objects

SELECT *
FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
) x
WHERE r >= 30 AND r < (30 + 10)
    ORDER BY object_id

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

ชดเชย fetch.png

หนึ่งสามารถแตกต่างกันกรณีทดสอบนี้โดยการสร้าง CI object_idหรือเพิ่มตัวกรอง แต่มันเป็นไปไม่ได้ที่จะลบความแตกต่างแผนทั้งหมด OFFSET ... FETCHเร็วกว่าเสมอเพราะทำงานได้น้อยลงในเวลาดำเนินการ


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

คำตอบ:


13

ตัวอย่างในคำถามนั้นไม่ได้ให้ผลลัพธ์ที่เหมือนกันเลย ( OFFSETตัวอย่างมีข้อผิดพลาดแบบแยกจากกัน) แบบฟอร์มที่อัปเดตด้านล่างแก้ไขปัญหานั้นลบการเรียงลำดับพิเศษสำหรับROW_NUMBERเคสและใช้ตัวแปรเพื่อทำให้การแก้ปัญหาเป็นเรื่องทั่วไปมากขึ้น:

DECLARE 
    @PageSize bigint = 10,
    @PageNumber integer = 3;

WITH Numbered AS
(
    SELECT TOP ((@PageNumber + 1) * @PageSize) 
        o.*,
        rn = ROW_NUMBER() OVER (
            ORDER BY o.[object_id])
    FROM #objects AS o
    ORDER BY 
        o.[object_id]
)
SELECT
    x.name,
    x.[object_id],
    x.principal_id,
    x.[schema_id],
    x.parent_object_id,
    x.[type],
    x.type_desc,
    x.create_date,
    x.modify_date,
    x.is_ms_shipped,
    x.is_published,
    x.is_schema_published
FROM Numbered AS x
WHERE
    x.rn >= @PageNumber * @PageSize
    AND x.rn < ((@PageNumber + 1) * @PageSize)
ORDER BY
    x.[object_id];

SELECT
    o.name,
    o.[object_id],
    o.principal_id,
    o.[schema_id],
    o.parent_object_id,
    o.[type],
    o.type_desc,
    o.create_date,
    o.modify_date,
    o.is_ms_shipped,
    o.is_published,
    o.is_schema_published
FROM #objects AS o
ORDER BY 
    o.[object_id]
    OFFSET @PageNumber * @PageSize - 1 ROWS 
    FETCH NEXT @PageSize ROWS ONLY;

ROW_NUMBERแผนมีค่าใช้จ่ายโดยประมาณของ0.0197935 :

แผนจำนวนแถว

OFFSETแผนมีค่าใช้จ่ายโดยประมาณของ0.0196955 :

แผนชดเชย

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

ในกรณีที่ใช้ค่าตัวอักษรคงที่ (เช่นOFFSET 30ในตัวอย่างดั้งเดิม) เครื่องมือเพิ่มประสิทธิภาพสามารถใช้การเรียง TopN แทนการเรียงแบบเต็มตามด้วยอันดับสูงสุด เมื่อแถวที่ต้องการจากการเรียง TopN เป็นตัวอักษรคงที่และ <= 100 (ผลรวมของOFFSETและFETCH) เอ็นจิ้นการดำเนินการสามารถใช้อัลกอริทึมการเรียงลำดับที่แตกต่างกันซึ่งสามารถทำงานได้เร็วกว่าการเรียง TopN ทั่วไป ทั้งสามกรณีมีลักษณะประสิทธิภาพโดยรวมแตกต่างกัน

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

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

ตัวอย่างหนึ่งสำหรับจุดที่สามด้านบนเกิดขึ้นที่ชุดการเพจค่อนข้างกว้าง มันอาจจะมีประสิทธิภาพมากขึ้นในการแสวงหากุญแจที่จำเป็นในการใช้ดัชนี nonclustered ด้วยตนเองและค้นหากับดัชนีคลัสเตอร์เมื่อเทียบกับการสแกนดัชนีด้วยหรือOFFSET ROW_NUMBERมีปัญหาเพิ่มเติมที่ต้องพิจารณาหากแอปพลิเคชันการเพจต้องการทราบจำนวนแถวหรือหน้าทั้งหมด มีอีกสนทนาที่ดีของญาติของ 'คีย์แสวงหา' และวิธีการ 'ชดเชย' ที่นี่

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


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

5

ด้วยข้อความค้นหาเล็กน้อยของคุณฉันได้รับการประมาณการต้นทุนเท่ากัน(50/50) และสถิติ IO ที่เท่ากัน:

; WITH cte AS
(
    SELECT *, ROW_NUMBER() OVER (ORDER BY object_id) r
    FROM #objects
)
SELECT *
FROM cte
WHERE r >= 30 AND r < 40
ORDER BY r

SELECT *
FROM #objects
ORDER BY object_id
OFFSET 30 ROWS FETCH NEXT 10 ROWS ONLY

นี้หลีกเลี่ยงการจัดเรียงเพิ่มเติมที่ปรากฏในรุ่นของคุณโดยการเรียงลำดับบนแทนrobject_id


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

2
@ ใช้ ORDER BY ROW_NUMBER () ใช้กำหนดวิธีการกำหนดหมายเลข ไม่ทำสัญญาใด ๆ กับใบสั่งส่งออก - ซึ่งแยกต่างหาก มันเกิดขึ้นบ่อยครั้งที่มันจะเกิดขึ้นพร้อมกัน แต่ก็ไม่รับประกัน
Aaron Bertrand

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

1
@usr คุณได้พบกับกรณีการใช้งานทั่วไปที่เครื่องมือเพิ่มประสิทธิภาพไม่ได้พิจารณา แต่เป็นกรณีที่ไม่ได้ใช้เพียงอย่างเดียว พิจารณากรณีที่คำสั่งซื้อโดยภายใน ROW_NUMBER () คือคอลัมน์นั้นและอย่างอื่น หรือเมื่อลำดับภายนอกโดยเรียงลำดับที่สองในคอลัมน์อื่น หรือเมื่อคุณต้องการสั่งซื้อจากมากไปน้อย หรืออย่างอื่นโดยสิ้นเชิง ฉันชอบสั่งซื้อโดยการแสดงออกrแทนคอลัมน์ฐานถ้าเพียงเพราะมันตรงกับสิ่งที่ฉันจะทำในแบบสอบถามที่ไม่ซ้อนกันและสั่งซื้อโดยการแสดงออก - ฉันจะใช้นามแฝงที่กำหนดให้กับการแสดงออกแทนการแสดงออกซ้ำ
Aaron Bertrand

4
@usr และสำหรับประเด็นของ Paul จะมีหลายกรณีที่คุณสามารถหาช่องว่างในการทำงานในเครื่องมือเพิ่มประสิทธิภาพ หากพวกเขาจะไม่ได้รับการแก้ไขและคุณรู้วิธีที่ดีกว่าในการเขียนแบบสอบถามใช้วิธีที่ดีกว่า ผู้ป่วย: "หมอมันเจ็บเมื่อฉันทำ x" หมอ: "อย่าทำ x" :-)
Aaron Bertrand

-3

พวกเขาปรับเปลี่ยนเคียวรีเครื่องมือเพิ่มประสิทธิภาพเพื่อเพิ่มคุณลักษณะนี้หมายความว่าพวกเขาใช้กลไกโดยเฉพาะเพื่อสนับสนุนคำสั่ง offset ... fetch ในคำอื่น ๆ สำหรับแบบสอบถาม SQL Server ต้องทำงานมากขึ้น ดังนั้นความแตกต่างในแผนแบบสอบถาม

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