ฟังก์ชันการซ้อนฟังก์ชันสเกลาร์อ้างอิงตนเองด้วยตนเองเกินเมื่อเพิ่มการเลือก


24

วัตถุประสงค์

เมื่อพยายามสร้างตัวอย่างทดสอบของฟังก์ชันการอ้างอิงตนเองรุ่นหนึ่งล้มเหลวในขณะที่อีกรุ่นหนึ่งประสบความสำเร็จ

ความแตกต่างเพียงอย่างเดียวคือการเพิ่มSELECTให้กับส่วนของฟังก์ชันทำให้แผนการดำเนินการแตกต่างกันสำหรับทั้งสอง


ฟังก์ชั่นที่ใช้งานได้

CREATE FUNCTION dbo.test5(@i int)
RETURNS INT
AS 
BEGIN
RETURN(
SELECT TOP 1
CASE 
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN  dbo.test5(1) + dbo.test5(2)
END
)
END;

กำลังเรียกใช้ฟังก์ชัน

SELECT dbo.test5(3);

ผลตอบแทน

(No column name)
3

ฟังก์ชั่นที่ไม่ทำงาน

CREATE FUNCTION dbo.test6(@i int)
RETURNS INT
AS 
BEGIN
RETURN(
SELECT TOP 1
CASE 
WHEN @i = 1 THEN 1
WHEN @i = 2 THEN 2
WHEN @i = 3 THEN (SELECT dbo.test6(1) + dbo.test6(2))
END
)END;

กำลังเรียกใช้ฟังก์ชัน

SELECT dbo.test6(3);

หรือ

SELECT dbo.test6(2);

ผลลัพธ์ในข้อผิดพลาด

ขั้นตอนการจัดเก็บฟังก์ชั่นทริกเกอร์หรือดูรังเกินกว่าที่กำหนดไว้ (จำกัด 32)

คาดเดาสาเหตุ

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

<ColumnReference Column="Expr1002" />
<ScalarOperator ScalarString="CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END">

และ expr1000 กำลัง

<ColumnReference Column="Expr1000" />
<ScalarOperator ScalarString="[dbo].[test6]((1))+[dbo].[test6]((2))">

ซึ่งสามารถอธิบายการอ้างอิงแบบเรียกซ้ำเกิน 32

คำถามจริง

การเพิ่มSELECTทำให้ฟังก์ชั่นเรียกตัวเองซ้ำแล้วซ้ำอีกทำให้เกิดการวนซ้ำไม่รู้จบ แต่ทำไมเพิ่มการSELECTให้ผลลัพธ์นี้


ข้อมูลเพิ่มเติม

แผนการดำเนินการโดยประมาณ

DB <> ซอ

Build version:
14.0.3045.24

ทดสอบกับ Compatible_levels 100 และ 140

คำตอบ:


26

นี่เป็นข้อผิดพลาดในการทำให้เป็นมาตรฐานของโครงการโดยใช้เคียวรีย่อยภายในนิพจน์เคสด้วยฟังก์ชันที่ไม่ได้กำหนดค่าไว้

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

  1. SQL Server ไม่สามารถรัน subqueries โดยตรงดังนั้นพวกเขาจะคลี่เสมอหรือแปลงเป็นใช้
  2. ซีแมนทิกส์CASEนั้นเป็นTHENนิพจน์ที่ควรได้รับการประเมินถ้าWHENประโยคส่งคืนจริง

เคียวรีย่อย (trivial) ที่แนะนำในกรณีที่เป็นปัญหาจึงส่งผลให้ตัวดำเนินการใช้ (การรวมลูปซ้อนกัน) เพื่อให้เป็นไปตามข้อกำหนดที่สอง SQL Server จะวางนิพจน์dbo.test6(1) + dbo.test6(2)ทางด้านในของการนำไปใช้:

เน้นการคำนวณสเกลาร์

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

... ด้วยCASEความหมายที่ได้รับเกียรติจากภาคการส่งผ่านในการเข้าร่วม:

[@i]=(1) OR [@i]=(2) OR IsFalseOrNull [@i]=(3)

ด้านในของลูปจะได้รับการประเมินก็ต่อเมื่อเงื่อนไขการส่งผ่านประเมินว่าเป็นเท็จ (หมายถึง@i = 3) ทั้งหมดนี้ถูกต้องแล้ว Compute เกลาต่อไปลูปซ้อนกันเข้าร่วมยังได้รับเกียรตินิยมCASEความหมายอย่างถูกต้อง:

[Expr1001] = Scalar Operator(CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END)

ปัญหาคือว่าขั้นตอนการทำให้เป็นมาตรฐานโครงการของการรวบรวมแบบสอบถามเห็นว่าExpr1000ไม่เกี่ยวข้องและกำหนดว่ามันจะปลอดภัย ( ผู้บรรยาย: มันไม่ใช่ ) เพื่อย้ายออกนอกวง:

ย้ายโครงการแล้ว

[Expr1000] = Scalar Operator([dbo].[test6]((1))+[dbo].[test6]((2)))

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

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

ปัญหานี้ไม่ได้เกิดขึ้นใน SQL Server 2019 เมื่อฟังก์ชันสเกลาร์ถูก inlineเนื่องจากลอจิกอินไลน์ทำงานโดยตรงบนต้นไม้ที่แยกวิเคราะห์ (ก่อนที่จะทำการปรับสภาพโครงการ) ตรรกะง่าย ๆ ในคำถามสามารถทำให้ง่ายขึ้นได้โดยตรรกะอินไลน์ที่ไม่ใช่แบบเรียกซ้ำ:

[Expr1019] = (Scalar Operator((1)))
[Expr1045] = Scalar Operator(CONVERT_IMPLICIT(int,CONVERT_IMPLICIT(int,[Expr1019],0)+(2),0))

... ซึ่งคืนค่า 3

อีกวิธีในการอธิบายปัญหาหลักคือ:

-- Not schema bound to make it non-det
CREATE OR ALTER FUNCTION dbo.Error() 
RETURNS integer 
-- WITH INLINE = OFF -- SQL Server 2019 only
AS
BEGIN
    RETURN 1/0;
END;
GO
DECLARE @i integer = 1;

SELECT
    CASE 
        WHEN @i = 1 THEN 1
        WHEN @i = 2 THEN 2
        WHEN @i = 3 THEN (SELECT dbo.Error()) -- 'subquery'
        ELSE NULL
    END;

สร้างซ้ำบนบิลด์ล่าสุดของทุกรุ่นตั้งแต่ 2008 R2 ถึง 2019 CTP 3.0

ตัวอย่างเพิ่มเติม (ไม่มีฟังก์ชันสเกลาร์) จัดทำโดยMartin Smith :

SELECT IIF(@@TRANCOUNT >= 0, 1, (SELECT CRYPT_GEN_RANDOM(4)/ 0))

สิ่งนี้มีองค์ประกอบสำคัญทั้งหมดที่จำเป็น:

  • CASE(ดำเนินการภายในเป็นScaOp_IIF)
  • ฟังก์ชั่นที่ไม่ได้กำหนดไว้ ( CRYPT_GEN_RANDOM)
  • แบบสอบถามย่อยในสาขาที่ไม่ควรดำเนินการ ( (SELECT ...))

* อย่างเคร่งครัดการเปลี่ยนแปลงข้างต้นอาจยังคงถูกต้องหากการประเมินผลExpr1000ถูกเลื่อนออกไปอย่างถูกต้องเนื่องจากมีการอ้างอิงโดยการก่อสร้างที่ปลอดภัยเท่านั้น:

[Expr1002] = Scalar Operator(CASE WHEN [@i]=(1) THEN (1) ELSE CASE WHEN [@i]=(2) THEN (2) ELSE CASE WHEN [@i]=(3) THEN [Expr1000] ELSE NULL END END END)

... แต่สิ่งนี้ต้องใช้การตั้งค่าสถานะForceOrderภายใน(ไม่ใช่คำใบ้แบบสอบถาม) ซึ่งไม่ได้ตั้งค่าไว้ ไม่ว่าในกรณีใดการใช้ตรรกะที่ใช้โดยการทำให้เป็นมาตรฐานของโครงการไม่ถูกต้องหรือไม่สมบูรณ์

รายงานข้อผิดพลาดในไซต์ Azure Feedback สำหรับ SQL Server

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