Apakah SQL Server cache menghitung nilai dalam kueri?

10

Setiap kali saya mengalami jenis pertanyaan ini, saya selalu bertanya-tanya bagaimana SQL Server akan menyelesaikannya. Jika saya menjalankan semua jenis kueri yang memerlukan perhitungan dan kemudian menggunakan nilai itu di banyak tempat, misalnya dalam selectdan order by, akan SQL Server menghitungnya dua kali untuk setiap baris atau akan di-cache? Selanjutnya, bagaimana cara kerjanya dengan Fungsi yang Ditentukan Pengguna?

Contoh:

SELECT CompanyId, Count(*)
FROM Sales
ORDER BY Count(*) desc

SELECT Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STX, Geom.BufferWithTolerance(@radius, 0.01, 0).STEnvelope().STPointN(1).STY
FROM Table

SELECT Id, udf.MyFunction(Id)
FROM Table
ORDER BY udf.MyFunction(Id)

Apakah ada cara untuk membuatnya lebih efisien atau SQL Server cukup pintar untuk menanganinya untuk saya?

Jonas Stawski
sumber
"Itu tergantung" di sini satu pameran rextester.com/DXOB90032
Martin Smith
Yang dapat Anda bandingkan dengan rextester.com/ARSO25902
Martin Smith
@ MartinSmith bukankah Anda menggunakan fungsi non deterministik? Jika demikian maka saya akan mengharapkan SQL untuk menjalankannya dua kali.
Jonas Stawski
selalu ada pengecualian! Anda dapat mencoba SELECT RAND() FROM Sales order by RAND()- ini hanya dievaluasi sekali karena keduanya non deterministik dan run time constant.
Martin Smith

Jawaban:

11

Pengoptimal kueri SQL Server dapat menggabungkan nilai terhitung berulang menjadi satu operator Compute Scalar. Apakah itu akan melakukannya atau tidak tergantung pada biaya rencana kueri dan properti dari nilai yang dihitung. Seperti yang diharapkan, itu tidak akan melakukan ini untuk nilai yang dihitung yang tidak deterministik, yang beberapa pengecualian seperti RAND(). Ini juga tidak akan melakukan ini untuk fungsi yang ditentukan pengguna.

Saya akan mulai dengan contoh fungsi yang ditentukan pengguna. Berikut adalah contoh yang sangat baik dari fungsi yang ditentukan pengguna:

CREATE OR ALTER FUNCTION dbo.NULL_FUNCTION (@N BIGINT) RETURNS BIGINT
WITH SCHEMABINDING
AS
BEGIN
RETURN NULL;
END;

Saya juga ingin membuat tabel dan menempatkan 100 baris ke dalamnya:

CREATE TABLE X_100 (N BIGINT NOT NULL);

WITH
L0   AS(SELECT 1 AS c UNION ALL SELECT 1),
L1   AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
L2   AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
L3   AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
L4   AS(SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
L5   AS(SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS n FROM L5)
INSERT INTO X_100 WITH (TABLOCK)
SELECT n
FROM Nums WHERE n <= 100;

The dbo.NULL_FUNCTIONfungsi determistic. Berapa kali akan dieksekusi untuk permintaan berikut?

SELECT n, dbo.NULL_FUNCTION(n)
FROM X_100;

Berdasarkan rencana permintaan, ini akan dieksekusi satu kali untuk setiap baris, atau 100 kali:

rencana permintaan 1

SQL Server 2016 memperkenalkan sys.dm_exec_function_stats DMV. Kami dapat mengambil snapshot dari DMV itu untuk melihat berapa kali UDF dieksekusi oleh sebuah query.

SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('NULL_FUNCTION');

Hasilnya adalah 100, jadi fungsinya dijalankan 100 kali.

Mari kita coba pertanyaan sederhana lainnya:

SELECT n, dbo.NULL_FUNCTION(n), dbo.NULL_FUNCTION(n) 
FROM X_100;

Rencana kueri menyarankan bahwa fungsi tersebut akan dieksekusi 200 kali:

rencana permintaan 2

Hasil sys.dm_exec_function_statsmenyarankan bahwa fungsi dijalankan 200 kali.

Perhatikan bahwa Anda tidak selalu dapat menggunakan paket kueri untuk mengetahui berapa kali skalar komputasi dijalankan. Kutipan berikut berasal dari " Hitung Kerangka, Ekspresi, dan Kinerja Rencana Eksekusi ":

Hal ini membuat orang berpikir bahwa Compute Scalar berperilaku seperti mayoritas operator lain: saat baris melewatinya, hasil perhitungan apa pun yang dikandung Compute Scalar ditambahkan ke stream. Ini umumnya tidak benar. Terlepas dari namanya, Compute Scalar tidak selalu menghitung apa pun, dan tidak selalu mengandung nilai skalar tunggal (dapat berupa vektor, alias, atau bahkan predikat Boolean, misalnya). Lebih sering daripada tidak, sebuah Compute Scalar hanya mendefinisikan ekspresi; perhitungan yang sebenarnya ditangguhkan sampai sesuatu nanti dalam rencana pelaksanaan membutuhkan hasil.

Mari kita coba contoh lain. Untuk pertanyaan berikut, saya berharap UDF dihitung satu kali:

WITH NULL_FUNCTION_CTE (NULL_VALUE) AS
(
SELECT DISTINCT dbo.NULL_FUNCTION(0)
)
SELECT n , cte.NULL_VALUE
FROM X_100
CROSS JOIN NULL_FUNCTION_CTE cte;

Rencana kueri menyarankan bahwa itu akan dihitung satu kali:

rencana permintaan

Namun, DMV mengungkapkan kebenaran. Komputasi skalar ditangguhkan sampai dibutuhkan, yang ada di operator bergabung. Itu dievaluasi 100 kali.

Anda juga bertanya apa yang dapat Anda lakukan untuk mendorong pengoptimal agar tidak menghitung ulang ekspresi yang sama beberapa kali. Hal terbaik yang dapat Anda lakukan adalah menghindari menggunakan skalar UDF dalam kode Anda. Mereka memiliki sejumlah masalah kinerja di luar pertanyaan ini, termasuk menggembungkan hibah memori, memaksa seluruh permintaan untuk dijalankan MAXDOP 1, perkiraan kardinalitas buruk, dan mengarah pada pemanfaatan CPU tambahan. Jika Anda perlu menggunakan UDF dan nilai UDF itu adalah konstan, Anda dapat menghitungnya di luar kueri dan memasukkannya ke dalam variabel lokal.

Untuk kueri tanpa UDF, Anda dapat mencoba menghindari menulis ekspresi yang mengembalikan hasil yang sama tetapi tidak diketik dengan cara yang persis sama. Untuk contoh berikut ini, saya menggunakan basis data AdventureworksDW2016CTP3 yang tersedia untuk umum, tetapi sebenarnya semua basis data akan melakukannya. Berapa kali akan COUNT(*)dihitung untuk permintaan ini?

SELECT OrderDateKey, COUNT(*) 
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

Untuk kueri ini, kita bisa mencari tahu ini dengan melihat operator Hash Match (agregat).

pertandingan hash

The COUNT(*)dihitung sekali untuk setiap nilai unik dari OrderDateKey. Termasuk ORDER BYklausa tidak menyebabkannya dihitung dua kali. Anda dapat melihat rencana eksekusi di sini .

Sekarang, pertimbangkan permintaan yang akan mengembalikan hasil yang sama persis tetapi ditulis dengan cara yang berbeda:

SELECT OrderDateKey, SUM(1)
FROM dbo.FactResellerSales
GROUP BY OrderDateKey
ORDER BY COUNT(*) DESC;

Pengoptimal kueri tidak cukup pintar untuk menggabungkannya, jadi pekerjaan tambahan akan dilakukan:

pertandingan hash 2

Joe Obbish
sumber