Pada akhirnya, SQL Server tidak mungkin untuk mengevaluasi skalar UDF hanya sekali dalam kueri. Namun, ada beberapa langkah yang bisa diambil untuk mendorongnya. Dengan pengujian saya percaya bahwa Anda bisa mendapatkan sesuatu yang berfungsi dengan versi SQL Server saat ini, tetapi ada kemungkinan bahwa perubahan di masa depan akan mengharuskan Anda untuk meninjau kembali kode Anda.
Jika mungkin untuk mengedit kode, hal pertama yang baik untuk dicoba adalah membuat fungsi deterministik jika memungkinkan. Paul White menunjukkan di sini bahwa fungsi harus dibuat dengan SCHEMABINDING
opsi dan kode fungsi itu sendiri harus deterministik.
Setelah melakukan perubahan berikut:
CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
DECLARE @tbl TABLE (VAL VARCHAR(5));
-- make the function expensive to call
INSERT INTO @tbl
SELECT [VALUE]
FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');
RETURN 1;
END;
Permintaan dari pertanyaan dijalankan dalam 64 ms:
SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();
Paket kueri tidak lagi memiliki operator filter:
Untuk memastikan bahwa itu dijalankan hanya sekali kita dapat menggunakan DMV sys.dm_exec_function_stats baru yang dirilis di SQL Server 2016:
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');
Mengeluarkan ALTER
fungsi yang berlawanan akan mengatur ulang execution_count
objek tersebut. Kueri di atas mengembalikan 1 yang berarti fungsi hanya dijalankan sekali.
Perhatikan bahwa hanya karena fungsi bersifat deterministik, bukan berarti hanya akan dievaluasi sekali untuk setiap kueri. Bahkan, untuk beberapa permintaan menambahkan SCHEMABINDING
dapat menurunkan kinerja. Pertimbangkan pertanyaan berikut:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
Superfluous DISTINCT
ditambahkan untuk menyingkirkan operator Filter. Rencananya terlihat menjanjikan:
Berdasarkan itu, orang akan mengharapkan UDF untuk dievaluasi sekali dan akan digunakan sebagai tabel luar dalam loop bersarang bergabung. Namun, kueri membutuhkan 6446 ms untuk dijalankan di mesin saya. Menurut sys.dm_exec_function_stats
fungsinya dijalankan 100 kali. Bagaimana itu mungkin? Dalam " Compute Scalars, Expressions, dan Performance Execution Plan ", Paul White menunjukkan bahwa operator Compal Scalar dapat ditunda:
Lebih sering daripada tidak, sebuah Compute Scalar hanya mendefinisikan ekspresi; perhitungan yang sebenarnya ditangguhkan sampai sesuatu nanti dalam rencana pelaksanaan membutuhkan hasil.
Untuk kueri ini, sepertinya panggilan UDF ditunda sampai diperlukan, pada saat itu dievaluasi 100 kali.
Menariknya, contoh CTE dieksekusi dalam 71 ms pada mesin saya ketika UDF tidak didefinisikan dengan SCHEMABINDING
, seperti pada pertanyaan asli. Fungsi ini hanya dijalankan sekali ketika kueri dijalankan. Berikut adalah rencana permintaan untuk itu:
Tidak jelas mengapa Compute Scalar tidak ditangguhkan. Ini bisa jadi karena ketidaktentuan fungsi membatasi pengaturan ulang operator yang dapat dilakukan oleh optimizer kueri.
Pendekatan alternatif adalah menambahkan tabel kecil ke CTE dan untuk menanyakan satu-satunya baris dalam tabel itu. Meja kecil mana pun bisa digunakan, tetapi mari kita gunakan yang berikut ini:
CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);
INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);
Permintaan kemudian menjadi:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
Penambahan dbo.X_ONE_ROW_TABLE
menambahkan ketidakpastian untuk pengoptimal. Jika tabel memiliki nol baris maka CTE akan mengembalikan 0 baris. Dalam hal apa pun, pengoptimal tidak dapat menjamin bahwa CTE akan mengembalikan satu baris jika UDF tidak deterministik, sehingga sepertinya UDF akan dievaluasi sebelum bergabung. Saya akan mengharapkan pengoptimal untuk memindai dbo.X_ONE_ROW_TABLE
, menggunakan agregat aliran untuk mendapatkan nilai maksimum dari satu baris yang dikembalikan (yang membutuhkan fungsi untuk dievaluasi), dan menggunakannya sebagai tabel luar untuk loop bersarang bergabung ke dbo.X_100_INTEGERS
dalam permintaan utama . Ini tampaknya yang terjadi :
Permintaan dijalankan dalam sekitar 110 ms pada mesin saya dan UDF dievaluasi hanya sekali menurut sys.dm_exec_function_stats
. Tidak benar untuk mengatakan bahwa pengoptimal permintaan dipaksa untuk mengevaluasi UDF hanya sekali. Namun, sulit untuk membayangkan penulisan ulang pengoptimal yang akan mengarah pada permintaan biaya yang lebih rendah, bahkan dengan keterbatasan sekitar UDF dan menghitung biaya skalar.
Singkatnya, untuk fungsi deterministik (yang harus menyertakan SCHEMABINDING
opsi) cobalah menulis kueri dengan cara sesederhana mungkin. Jika pada SQL Server 2016 atau versi yang lebih baru, konfirmasikan bahwa fungsi hanya dijalankan sekali menggunakan sys.dm_exec_function_stats
. Rencana pelaksanaan dapat menyesatkan dalam hal itu.
Untuk fungsi yang tidak dipertimbangkan oleh SQL Server menjadi deterministik, termasuk apa pun yang tidak memiliki SCHEMABINDING
opsi, salah satu pendekatan adalah menempatkan UDF dalam CTE yang dibuat dengan hati-hati atau tabel turunan. Ini membutuhkan sedikit perawatan tetapi CTE yang sama dapat berfungsi untuk fungsi deterministik dan nondeterministik.