การใช้ข้อยกเว้นในนิพจน์ตารางแบบเรียกซ้ำ


33

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

with cte as (
    select *
    from (
        values(1),(2),(3),(4),(5)
    ) v (a)
)
,r as (
    select a
    from cte
    where a in (1,2,3)
    union all
    select a
    from (
        select a
        from cte
        except
        select a
        from r
    ) x
)
select a
from r

ฉันเจอสิ่งนี้ในขณะที่พยายามตอบคำถามเกี่ยวกับ Stack Overflow

คำตอบ:


26

ดูคำตอบของ Martin Smithสำหรับข้อมูลเกี่ยวกับสถานะปัจจุบันของEXCEPTใน CTE แบบเรียกซ้ำ

เพื่ออธิบายสิ่งที่คุณเห็นและทำไม:

ฉันกำลังใช้ตัวแปรตารางที่นี่เพื่อสร้างความแตกต่างระหว่างค่าจุดยึดและค่ารายการที่เรียกซ้ำ (ไม่เปลี่ยนความหมาย)

DECLARE @V TABLE (a INTEGER NOT NULL)
INSERT  @V (a) VALUES (1),(2)
;
WITH rCTE AS 
(
    -- Anchor
    SELECT
        v.a
    FROM @V AS v

    UNION ALL

    -- Recursive
    SELECT
        x.a
    FROM
    (
        SELECT
            v2.a
        FROM @V AS v2

        EXCEPT

        SELECT
            r.a
        FROM rCTE AS r
    ) AS x
)
SELECT
    r2.a
FROM rCTE AS r2
OPTION (MAXRECURSION 0)

แผนแบบสอบถามคือ:

แผน CTE แบบเรียกซ้ำ

การดำเนินการเริ่มต้นที่รูทของแผน (ตัวเลือก) และการควบคุมส่งผ่านต้นไม้ไปยังดัชนีสปูลการต่อข้อมูลจากนั้นไปที่การสแกนตารางระดับบนสุด

แถวแรกจากการสแกนผ่านต้นไม้และเป็น (a) เก็บไว้ในสปูลสปูลและ (b) กลับไปที่ลูกค้า ไม่ได้กำหนดแถวใดก่อน แต่ให้เราสมมติว่าเป็นแถวที่มีค่า {1} เพื่อการโต้แย้ง แถวแรกที่ปรากฏคือ {1}

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

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

Inner join เรียก Table Spool บนอินพุตด้านนอกซึ่งอ่านแถวบนสุดจากสแต็ก {2} และลบออกจาก worktable สแต็กตอนนี้มี {1}

เมื่อได้รับแถวหนึ่งจากอินพุตด้านนอก Inner Inner จะผ่านการควบคุมอินพุตด้านในไปยัง Left Anti-Semi Join (LASJ) สิ่งนี้ร้องขอแถวจากอินพุตด้านนอกผ่านการควบคุมไปยังการเรียงลำดับ การเรียงลำดับเป็นตัววนซ้ำการบล็อกดังนั้นจึงอ่านแถวทั้งหมดจากตัวแปรตารางและเรียงลำดับจากน้อยไปหามาก

แถวแรกที่ปล่อยโดย Sort เรียงดังนั้นค่า {1} ด้านในของ LASJ ส่งคืนค่าปัจจุบันของสมาชิกแบบเรียกซ้ำ (ค่าเพิ่งแตกออกจากสแต็ก) ซึ่งคือ {2} ค่าที่ LASJ คือ {1} และ {2} ดังนั้น {1} จะถูกปล่อยออกมาเนื่องจากค่าไม่ตรงกัน

แถวนี้ {1} ไหลแผนผังแผนผังคิวรีไปยังสปูลดัชนี (สแต็ก) ซึ่งถูกเพิ่มไปยังสแต็กซึ่งตอนนี้มี {1, 1} และปล่อยออกไปยังไคลเอ็นต์ ไคลเอ็นต์ได้รับลำดับ {1}, {2}, {1}

ตอนนี้การควบคุมจะส่งกลับไปยังการต่อข้อมูล, ย้อนกลับด้านใน (กลับเป็นครั้งสุดท้าย, อาจทำอีกครั้ง), ผ่านการเข้าร่วมภายในไปยัง LASJ มันอ่านอินพุตภายในอีกครั้งโดยรับค่า {2} จาก Sort

สมาชิกที่เรียกซ้ำยังคงเป็น {2} ดังนั้นเวลานี้ LASJ จะพบ {2} และ {2} ทำให้ไม่มีแถวถูกปล่อยออกมา การหาแถวที่ไม่มีในอินพุตด้านใน (ตอนนี้การเรียงลำดับอยู่นอกแถว) แล้วการควบคุมจะส่งกลับไปยังการรวมภายใน

Inner Join อ่านอินพุตด้านนอกของมันซึ่งส่งผลให้ค่า {1} ถูกแตกออกจากสแต็ก {1, 1} ทำให้สแต็กเหลือเพียง {1} กระบวนการนี้ทำซ้ำโดยค่า {2} จากการเรียกใช้การสแกนตารางและการเรียงลำดับใหม่ผ่านการทดสอบ LASJ และเพิ่มลงในสแต็กและส่งผ่านไปยังไคลเอ็นต์ซึ่งได้รับ {1}, {2}, {1}, {2} ... และรอบเราไป

คำอธิบายที่โปรดปรานของฉันเกี่ยวกับสปูลสแต็คที่ใช้ในแผน CTE แบบเรียกซ้ำคือ Craig Freedman


31

คำอธิบาย BOL ของ CTE แบบเรียกซ้ำจะอธิบายความหมายของการเรียกใช้แบบเรียกซ้ำดังนี้:

  1. แยกการแสดงออกของ CTE ออกเป็นจุดยึดและสมาชิกแบบเรียกซ้ำ
  2. เรียกใช้สมาชิกจุดยึดสร้างการเรียกครั้งแรกหรือชุดผลลัพธ์พื้นฐาน (T0)
  3. รันสมาชิกแบบเรียกซ้ำโดยใช้ Ti เป็นอินพุตและ Ti + 1 เป็นเอาต์พุต
  4. ทำซ้ำขั้นตอนที่ 3 จนกว่าจะส่งคืนชุดเปล่า
  5. ส่งคืนชุดผลลัพธ์ นี่คือยูเนี่ยนทั้งหมดของ T0 ถึง Tn

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

การนำสิ่งนี้ไปใช้กับ CTE ของคุณฉันคาดหวังว่าวงวนไม่สิ้นสุดที่มีรูปแบบต่อไปนี้

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       4 | 5 |   |   |
|         3 |       1 | 2 | 3 |   |
|         4 |       4 | 5 |   |   |
|         5 |       1 | 2 | 3 |   |
+-----------+---------+---+---+---+ 

เพราะ

select a
from cte
where a in (1,2,3)

คือการแสดงออก Anchor ผลตอบแทนนี้ชัดเจน1,2,3ว่าเป็นT0

หลังจากนั้นนิพจน์แบบเรียกใช้ซ้ำจะทำงาน

select a
from cte
except
select a
from r

ด้วยการ1,2,3ป้อนข้อมูลที่จะให้ผลลัพธ์ของการ4,5เป็นT1แล้วเสียบที่ย้อนกลับไปในรอบถัดไปของการเรียกซ้ำจะกลับมา1,2,3เรื่อย ๆ

นี่ไม่ใช่สิ่งที่เกิดขึ้นจริงอย่างไรก็ตาม นี่คือผลลัพธ์ของการเชิญ 5 ครั้งแรก

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       1 | 2 | 4 | 5 |
|         3 |       1 | 2 | 3 | 4 |
|         4 |       1 | 2 | 3 | 5 |
|         5 |       1 | 2 | 3 | 4 |
+-----------+---------+---+---+---+

จากการใช้OPTION (MAXRECURSION 1)และการปรับขึ้นในการเพิ่มขึ้นของ1มันจะเห็นได้ว่าจะเข้าสู่รอบที่แต่ละระดับอย่างต่อเนื่องต่อเนื่องจะสลับระหว่าง outputting และ1,2,3,4 1,2,3,5

ตามที่กล่าวถึงโดย@Quassnoiในบล็อกโพสต์นี้ รูปแบบของผลลัพธ์ที่สังเกตได้จะเหมือนกับว่าการร้องขอแต่ละครั้งกำลังทำ(1),(2),(3),(4),(5) EXCEPT (X)อยู่โดยที่Xแถวสุดท้ายจากการร้องขอก่อนหน้านี้

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

Anchor ปล่อย1,2,3ให้กับลูกค้าสแต็คเนื้อหา3,2,1

3 สแตกออกจากกองเนื้อหาสแต็ค 2,1

LASJ ส่งคืน1,2,4,5เนื้อหาสแต็ก5,4,2,1,2,1

5 สแตกออกจากกองเนื้อหาสแต็ค 4,2,1,2,1

LASJ ส่งคืน1,2,3,4 เนื้อหาสแต็ก4,3,2,1,5,4,2,1,2,1

สแตก 4 ครั้งออกจากสแต็กเนื้อหาสแต็ก 3,2,1,5,4,2,1,2,1

LASJ ส่งคืน1,2,3,5 เนื้อหาสแต็ก5,3,2,1,3,2,1,5,4,2,1,2,1

5 สแตกออกจากกองเนื้อหาสแต็ค 3,2,1,3,2,1,5,4,2,1,2,1

LASJ ส่งคืน1,2,3,4 เนื้อหาสแต็ก 4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

ถ้าคุณลองและแทนที่สมาชิกแบบเรียกซ้ำด้วยนิพจน์เชิงตรรกะ (โดยไม่มีนิพจน์ซ้ำ / NULL)

select a
from (
    select a
    from cte
    where a not in 
    (select a
    from r)
) x

สิ่งนี้ไม่ได้รับอนุญาตและทำให้เกิดข้อผิดพลาด "ไม่อนุญาตให้มีการอ้างอิงแบบเรียกซ้ำในแบบสอบถามย่อย" ดังนั้นอาจเป็นการกำกับดูแลที่EXCEPTได้รับอนุญาตในกรณีนี้

เพิ่มเติม: Microsoft ได้ตอบกลับการเชื่อมต่อข้อเสนอแนะของฉันเป็นด้านล่าง

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

ในการ จำกัด การสอบถามซ้ำEXCEPTเราปฏิบัติตามมาตรฐาน ANSI SQL ซึ่งรวมถึงข้อ จำกัด นี้นับตั้งแต่มีการแนะนำการสอบถามซ้ำ (ในปี 1999 ฉันเชื่อว่า) ไม่มีข้อตกลงอย่างกว้างขวางเกี่ยวกับสิ่งที่ความหมายควรใช้เพื่อเรียกซ้ำEXCEPT(เรียกอีกอย่างว่า "การปฏิเสธแบบไม่มีการกำหนด") ในภาษาที่ประกาศเช่น SQL นอกจากนี้ยังยากที่จะใช้ความหมายดังกล่าวได้อย่างมีประสิทธิภาพ (สำหรับฐานข้อมูลที่มีขนาดพอสมควร) ในระบบ RDBMS

และดูเหมือนว่าการใช้งานในท้ายที่สุดนั้นถูกสร้างขึ้นในปี 2014 สำหรับฐานข้อมูลที่มีระดับความเข้ากันได้ตั้งแต่ 120 ขึ้นไป

การอ้างอิงแบบเรียกซ้ำในส่วนข้อยกเว้นสร้างข้อผิดพลาดตามมาตรฐาน ANSI SQL

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