SQL Server อ่านฟังก์ชัน COALESCE ทั้งหมดแม้ว่าอาร์กิวเมนต์แรกไม่ใช่ NULL หรือไม่


98

ฉันใช้COALESCEฟังก์ชันT-SQL โดยที่อาร์กิวเมนต์แรกจะไม่เป็นโมฆะในเวลาประมาณ 95% ของเวลาที่รัน หากอาร์กิวเมนต์แรกคืออาร์กิวเมนต์NULLที่สองนั้นเป็นกระบวนการที่ค่อนข้างยาว:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )

ตัวอย่างเช่นc.FirstName = 'John'ถ้า SQL Server จะยังเรียกใช้แบบสอบถามย่อยหรือไม่

ฉันรู้ว่าด้วยIIF()ฟังก์ชั่นVB.NET ถ้าอาร์กิวเมนต์ที่สองคือ True รหัสยังคงอ่านอาร์กิวเมนต์ที่สาม (แม้ว่ามันจะไม่ถูกใช้)

คำตอบ:


95

Nope นี่คือการทดสอบอย่างง่าย:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error

หากมีการประเมินเงื่อนไขที่สองข้อยกเว้นจะถูกส่งออกเพื่อหารด้วยศูนย์

ตามเอกสาร MSDNสิ่งนี้เกี่ยวข้องกับวิธีCOALESCEการที่ล่ามดูซึ่งเป็นวิธีที่ง่ายในการเขียนCASEคำสั่ง

CASE เป็นที่รู้จักกันดีว่าเป็นหนึ่งในฟังก์ชั่นเดียวใน SQL Server ที่ (ส่วนใหญ่) วงจรสั้นเชื่อถือได้

มีข้อยกเว้นบางประการเมื่อเปรียบเทียบกับตัวแปรสเกลาร์และการรวมที่แสดงโดย Aaron Bertrand ในคำตอบอื่นที่นี่ (และสิ่งนี้จะใช้ทั้งกับCASEและCOALESCE):

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

จะสร้างการหารด้วยศูนย์ข้อผิดพลาด

สิ่งนี้ควรถูกพิจารณาว่าเป็นบั๊กและตามกฎCOALESCEจะแยกวิเคราะห์จากซ้ายไปขวา


6
@JNK โปรดดูคำตอบของฉันเพื่อดูกรณีที่ง่ายมากซึ่งไม่เป็นความจริง (ข้อกังวลของฉันคือมีสถานการณ์ที่ยังไม่ถูกค้นพบมากยิ่งขึ้น - ทำให้ยากที่จะยอมรับว่าCASEจะประเมินวงจรซ้าย - ขวา - ขวาเสมอ )
Aaron Bertrand

4
พฤติกรรมที่น่าสนใจอื่น ๆ @SQLKiwi ชี้ให้ฉัน: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- ทำซ้ำหลายครั้ง คุณจะได้รับNULLบางครั้ง ลองอีกครั้งด้วยISNULL- คุณจะไม่ได้รับNULL...
Aaron Bertrand


@ มาร์ตินใช่ฉันเชื่ออย่างนั้น แต่ไม่ใช่พฤติกรรมที่ผู้ใช้ส่วนใหญ่พบว่าใช้งานง่ายเว้นแต่พวกเขาเคยได้ยิน (หรือถูกกัดโดย) ปัญหาดังกล่าว
Aaron Bertrand

73

แล้วเรื่องนี้ - ตามที่ฉันได้รับจาก Itzik Ben-Gan ใครเล่าเกี่ยวกับเรื่องนี้โดย Jaime Lafargue ?

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

ผลลัพธ์:

Msg 8134, Level 16, State 1, Line 2
Divide by zero error encountered.

มีวิธีการแก้ปัญหาที่น่ารำคาญของหลักสูตรมี แต่ประเด็นก็คือยังคงที่CASEไม่เสมอรับประกันจากซ้ายไปขวาประเมินผล / ลัดวงจร ฉันรายงานข้อผิดพลาดที่นี่และมันถูกปิดเป็น "โดยการออกแบบ" ต่อมา Paul White ได้ยื่นรายการเชื่อมต่อนี้และมันถูกปิดเป็นคงที่ ไม่ใช่เพราะได้รับการแก้ไขแล้ว แต่เนื่องจากพวกเขาอัปเดต Books Online พร้อมคำอธิบายที่ถูกต้องมากขึ้นของสถานการณ์ที่มวลรวมสามารถเปลี่ยนลำดับการประเมินผลของCASEนิพจน์ได้ ฉันเพิ่งblogged เพิ่มเติมเกี่ยวกับเรื่องนี้ที่นี่

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

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

แก้ไข ที่นี่เป็นอีกกรณีหนึ่ง (ฉันต้องหยุดทำเช่นนั้น) โดยที่การCASEแสดงออกไม่ได้ประเมินตามลำดับที่คุณคาดหวังแม้ว่าจะไม่มีการรวมมวลก็ตาม


2
และดูเหมือนกับว่ามีปัญหาอีกเรื่องหนึ่งCASE ที่ได้รับการแก้ไขอย่างเงียบ ๆ
Martin Smith

IMO สิ่งนี้ไม่ได้พิสูจน์ว่าการประเมินผลการแสดงออกของ CASE นั้นไม่รับประกันว่าจะมีการคำนวณค่ารวมก่อนการเลือก (เพื่อให้สามารถใช้ภายในได้)
Salman

1
@SalmanA ฉันไม่แน่ใจว่าสิ่งนี้จะเกิดอะไรขึ้นนอกจากจะพิสูจน์ได้ว่าลำดับการประเมินในนิพจน์ CASE นั้นไม่รับประกัน เราได้รับการยกเว้นเนื่องจากมีการคำนวณผลรวมก่อนแม้ว่าจะอยู่ในข้ออื่นของ ELSE ว่า - หากคุณไปตามเอกสาร - ไม่ควรเข้าถึง
แอรอนเบอร์ทรานด์

@AaronBertrand มวลรวมจะคำนวณก่อนคำสั่ง CASE (และควรมี IMO) เอกสารที่แก้ไขชี้ให้เห็นอย่างชัดเจนว่าข้อผิดพลาดเกิดขึ้นก่อนที่จะทำการประเมิน CASE
Salman A

@SalmanA มันยังแสดงให้เห็นถึงนักพัฒนาที่ไม่เป็นทางการว่าการแสดงออกของ CASE นั้นไม่ได้ประเมินตามลำดับที่เขียนขึ้น - กลไกพื้นฐานไม่เกี่ยวข้องถ้าสิ่งที่คุณพยายามทำคือเข้าใจว่าทำไมข้อผิดพลาดมาจากสาขา CASE ที่ไม่ควรทำ ' ยังไม่ถึง คุณมีข้อโต้แย้งเปรียบเทียบกับตัวอย่างอื่น ๆ ทั้งหมดในหน้านี้ด้วยหรือไม่?
Aaron Bertrand

37

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

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

เกี่ยวข้องกับคำถามเดิมมีปัญหาอื่น ๆ เกี่ยวกับ CASE (และดังนั้น COALESCE) ที่มีการใช้งานฟังก์ชั่นผลกระทบด้านข้างหรือแบบสอบถามย่อย พิจารณา:

SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
SELECT ISNULL((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);

แบบฟอร์ม COALESCE มักส่งคืนค่า NULL โดยมีรายละเอียดเพิ่มเติมที่https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

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

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

ฉันไม่คิดว่าจะเป็นเรื่องที่น่าพอใจอย่างมาก


18

ฉันเจอกรณีอื่นที่CASE/ COALESCEไม่ลัดวงจร TVF ต่อไปนี้จะเพิ่มการละเมิด PK หากถูกส่ง1เป็นพารามิเตอร์

CREATE FUNCTION F (@P INT)
RETURNS @T TABLE (
  C INT PRIMARY KEY)
AS
  BEGIN
      INSERT INTO @T
      VALUES      (1),
                  (@P)

      RETURN
  END

หากเรียกดังต่อไปนี้

DECLARE @Number INT = 1

SELECT COALESCE(@Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = @Number), 
                         (SELECT TOP (1)  C
                          FROM   F(@Number))) 

หรือเป็น

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 

ทั้งคู่ให้ผลลัพธ์

การละเมิดข้อ จำกัด คีย์หลัก 'PK__F__3BD019A800551192' ไม่สามารถแทรกคีย์ซ้ำในวัตถุ 'dbo. @ T' ค่าคีย์ที่ซ้ำกันคือ (1)

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

วางแผน

การเขียนคิวรีนี้ซ้ำเพื่อหลีกเลี่ยงปัญหา

SELECT COALESCE(Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = Number), 
                         (SELECT TOP (1)  C
                          FROM   F(Number))) 
FROM (VALUES(1)) V(Number)   

ซึ่งให้แผน

plan2


8

ตัวอย่างอื่น

CREATE TABLE T1 (C INT PRIMARY KEY)

CREATE TABLE T2 (C INT PRIMARY KEY)

INSERT INTO T1 
OUTPUT inserted.* INTO T2
VALUES (1),(2),(3);

แบบสอบถาม

SET STATISTICS IO ON;

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (LOOP JOIN)

ไม่แสดงการอ่านT2ใด ๆ เลย

การค้นหาของT2อยู่ภายใต้การผ่านเพรดิเคตและตัวดำเนินการจะไม่ถูกดำเนินการ แต่

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (MERGE JOIN)

ไม่แสดงให้เห็นว่าT2มีการอ่าน แม้ว่าไม่T2จำเป็นต้องมีคุณค่าจากสิ่งใด

ของหลักสูตรนี้ไม่น่าแปลกใจจริงๆ แต่ฉันคิดว่ามูลค่าเพิ่มไปยังที่เก็บตัวอย่างเคาน์เตอร์ถ้าเพียงเพราะมันทำให้เกิดปัญหาของสิ่งที่การลัดวงจรสั้นแม้หมายถึงในภาษาประกาศตั้งอยู่


7

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

SELECT COALESCE(c.FirstName
            ,(SELECT TOP 1 b.FirstName
              FROM TableA a 
              JOIN TableB b ON .....
              WHERE C.FirstName IS NULL) -- this is the changed part
            )

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

SELECT COALESCE(c.FirstName, x.FirstName)
FROM
   TableC c
   OUTER APPLY (
      SELECT TOP 1 b.FirstName
      FROM
         TableA a 
         JOIN TableB b ON ...
      WHERE
         c.FirstName IS NULL -- the important part
   ) x

โดยทั่วไปนี่เป็นเทคนิคการเข้าร่วมตาราง "ยาก" แต่รวมถึงเงื่อนไขว่าเมื่อใดที่แถวใด ๆ ควรเข้าร่วม จากประสบการณ์ของฉันสิ่งนี้ได้ช่วยวางแผนการปฏิบัติจริง ๆ เป็นครั้งคราว


3

ไม่มันจะไม่ มันจะทำงานเมื่อเป็นc.FirstNameNULL

อย่างไรก็ตามคุณควรลองด้วยตัวเอง การทดลอง คุณบอกว่าแบบสอบถามย่อยของคุณมีความยาว เกณฑ์มาตรฐาน วาดข้อสรุปของคุณเองในเรื่องนี้

@Aaron คำตอบสำหรับแบบสอบถามย่อยที่กำลังเรียกใช้นั้นสมบูรณ์กว่า

อย่างไรก็ตามฉันยังคิดว่าคุณควรปรับปรุงการค้นหาและใช้งานLEFT JOINใหม่ เวลาส่วนใหญ่ของแบบสอบถามย่อยสามารถออก reworking แบบสอบถามของคุณเพื่อใช้LEFT JOINs

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


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

3

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

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