Referensi tingkat fungsi skalar referensi diri terlampaui saat menambahkan pilihan

24

Tujuan

Ketika mencoba membuat contoh uji dari fungsi referensi diri, satu versi gagal sementara yang lain berhasil.

Satu-satunya perbedaan yang ditambahkan SELECTke badan fungsi menghasilkan rencana eksekusi yang berbeda untuk keduanya.


Fungsi yang berfungsi

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;

Memanggil fungsi

SELECT dbo.test5(3);

Kembali

(No column name)
3

Fungsi yang tidak berfungsi

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;

Memanggil fungsi

SELECT dbo.test6(3);

atau

SELECT dbo.test6(2);

Menghasilkan kesalahan

Prosedur, fungsi, pemicu, atau tampilan level penyimpanan terlampaui maksimum (batas 32).

Menebak penyebabnya

Ada skalar komputasi tambahan pada rencana perkiraan fungsi gagal, panggilan

<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">

Dan expr1000 sedang

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

Yang bisa menjelaskan referensi rekursif melebihi 32.

Pertanyaan sebenarnya

Yang ditambahkan SELECTmembuat fungsi memanggil dirinya sendiri berulang kali, menghasilkan loop tanpa akhir, tetapi mengapa menambahkan SELECTmemberi hasil ini?


informasi tambahan

Perkiraan rencana eksekusi

DB <> Fiddle

Build version:
14.0.3045.24

Diuji pada kompatibilitas_level 100 dan 140

Randi Vertongen
sumber

Jawaban:

26

Ini adalah bug dalam normalisasi proyek , diekspos dengan menggunakan subquery di dalam ekspresi kasus dengan fungsi non-deterministik.

Untuk menjelaskannya, kita perlu mencatat dua hal di muka:

  1. SQL Server tidak dapat mengeksekusi subquery secara langsung, sehingga mereka selalu terbuka atau dikonversi menjadi berlaku .
  2. Semantiknya CASEsedemikian sehingga THENekspresi hanya harus dievaluasi jika WHENklausa mengembalikan true.

Subquery (sepele) yang diperkenalkan dalam kasus bermasalah karenanya menghasilkan operator yang berlaku (nested loop bergabung). Untuk memenuhi persyaratan kedua, SQL Server pada awalnya menempatkan ekspresi dbo.test6(1) + dbo.test6(2)di sisi dalam dari penerapan:

skalar komputasi yang disorot

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

... dengan CASEsemantik yang dihormati oleh predikat lulus pada bergabung:

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

Sisi dalam loop hanya dievaluasi jika kondisi pass-through bernilai false (artinya @i = 3). Sejauh ini semuanya benar. The Hitung skalar mengikuti loop bersarang bergabung juga menghormati CASEsemantik benar:

[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)

Masalahnya adalah bahwa tahap normalisasi proyek kompilasi permintaan melihat yang Expr1000tidak berkorelasi dan menentukan bahwa itu akan aman ( narator: tidak ) untuk memindahkannya ke luar loop:

proyek dipindahkan

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

Ini memecah * semantik yang diimplementasikan oleh predikat pass-through , sehingga fungsi dievaluasi ketika seharusnya tidak, dan hasil loop tak terbatas.

Anda harus melaporkan bug ini. Solusinya adalah untuk mencegah ekspresi dipindahkan di luar penerapan dengan membuatnya berkorelasi (yaitu termasuk @idalam ekspresi) tetapi ini adalah retasan tentu saja. Ada cara untuk menonaktifkan normalisasi proyek, tetapi saya telah diminta sebelumnya untuk tidak membagikannya kepada publik, jadi saya tidak akan melakukannya.

Masalah ini tidak muncul di SQL Server 2019 ketika fungsi skalar diuraikan, karena logika inlining beroperasi langsung pada pohon yang diurai (jauh sebelum normalisasi proyek). Logika sederhana dalam pertanyaan dapat disederhanakan dengan logika inlining ke non-rekursif:

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

... yang mengembalikan 3.

Cara lain untuk menggambarkan masalah inti adalah:

-- 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;

Diproduksi ulang pada versi terbaru dari semua versi dari 2008 R2 hingga 2019 CTP 3.0.

Contoh lebih lanjut (tanpa fungsi skalar) disediakan oleh Martin Smith :

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

Ini memiliki semua elemen kunci yang dibutuhkan:

  • CASE(diimplementasikan secara internal sebagai ScaOp_IIF)
  • Fungsi non-deterministik ( CRYPT_GEN_RANDOM)
  • Subquery pada cabang yang seharusnya tidak dieksekusi ( (SELECT ...))

* Secara ketat, transformasi di atas masih bisa benar jika evaluasi Expr1000ditangguhkan dengan benar, karena hanya dirujuk oleh konstruksi yang aman:

[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)

... tapi ini memerlukan flag ForceOrder internal (bukan petunjuk permintaan), yang tidak disetel juga. Dalam kasus apa pun, implementasi logika yang diterapkan oleh normalisasi proyek tidak benar atau tidak lengkap.

Laporan bug di situs Umpan Balik Azure untuk SQL Server.

Paul White mengatakan GoFundMonica
sumber