Fungsi yang ditentukan pengguna di SQL Server (UDF) adalah objek utama yang harus diperhatikan oleh setiap pengembang. Meskipun mereka sangat berguna dalam banyak skenario (klausa WHERE, kolom yang dihitung, dan batasan pemeriksaan), mereka masih memiliki beberapa batasan dan praktik buruk yang dapat menyebabkan masalah kinerja. UDF multi-pernyataan mungkin menimbulkan dampak kinerja yang signifikan, dan artikel ini akan membahas skenario ini secara khusus.
Fungsi tidak diimplementasikan dengan cara yang sama seperti dalam bahasa berorientasi objek, meskipun fungsi bernilai tabel sebaris dapat digunakan dalam skenario saat Anda memerlukan tampilan berparameter, ini tidak berlaku untuk fungsi yang mengembalikan skalar atau tabel. Fungsi-fungsi ini perlu digunakan dengan hati-hati karena dapat menyebabkan banyak masalah kinerja. Namun, mereka sangat penting dalam banyak kasus, jadi kita perlu lebih memperhatikan implementasinya. Fungsi digunakan dalam pernyataan SQL di dalam kumpulan, prosedur, pemicu atau tampilan, di dalam kueri SQL ad-hoc, atau sebagai bagian dari kueri pelaporan yang dihasilkan oleh alat seperti PowerBI atau Tableau, dalam bidang terhitung, dan periksa batasan. Meskipun fungsi skalar dapat rekursif hingga 32 level, fungsi tabel tidak mendukung rekursi.
Jenis Fungsi di SQL Server
Di SQL Server, kami memiliki tiga jenis fungsi:fungsi skalar yang ditentukan pengguna (SF) yang mengembalikan nilai skalar tunggal, fungsi bernilai tabel yang ditentukan pengguna (TVF) yang mengembalikan tabel, dan fungsi bernilai tabel sebaris (ITVF) yang tidak memiliki fungsi tubuh. Fungsi Tabel dapat berupa Inline atau Multi-statement. Fungsi sebaris tidak memiliki variabel pengembalian, mereka hanya mengembalikan fungsi nilai. Fungsi multi-pernyataan terkandung dalam blok kode BEGIN-END dan dapat memiliki beberapa pernyataan T-SQL yang tidak menimbulkan efek samping (seperti memodifikasi konten dalam tabel).
Kami akan menunjukkan setiap jenis fungsi dalam contoh sederhana:
/**
inline table function
**/
CREATE FUNCTION dbo.fnInline( @P1 INT, @P2 VARCHAR(50) )
RETURNS TABLE
AS
RETURN ( SELECT @P1 AS P_OUT_1, @P2 AS P_OUT_2 )
/**
multi-statement table function
**/
CREATE FUNCTION dbo.fnMultiTable( @P1 INT, @P2 VARCHAR(50) )
RETURNS @r_table TABLE ( OUT_1 INT, OUT_2 VARCHAR(50) )
AS
BEGIN
INSERT @r_table SELECT @P1, @P2;
RETURN;
END;
/**
scalar function
**/
CREATE FUNCTION dbo.fnScalar( @P1 INT, @P2 INT )
RETURNS INT
AS
BEGIN
RETURN @P1 + @P2
END
Keterbatasan Fungsi SQL Server
Seperti disebutkan dalam pendahuluan, ada beberapa batasan dalam penggunaan fungsi, dan saya akan membahas beberapa di bawah ini. Daftar lengkapnya dapat ditemukan di Microsoft Docs :
- Tidak ada konsep fungsi sementara
- Anda tidak dapat membuat fungsi di database lain, tetapi, tergantung pada hak istimewa Anda, Anda dapat mengaksesnya
- Dengan UDF, Anda tidak diizinkan melakukan tindakan apa pun yang mengubah status basis data,
- Di dalam UDF, Anda tidak dapat memanggil prosedur, kecuali prosedur tersimpan yang diperluas
- UDF tidak dapat mengembalikan kumpulan hasil, tetapi hanya tipe data tabel
- Anda tidak dapat menggunakan SQL dinamis atau tabel sementara di UDF
- UDF terbatas dalam kemampuan penanganan kesalahan – mereka tidak mendukung RAISERROR atau TRY…CATCH dan Anda tidak bisa mendapatkan data dari variabel sistem @ERROR
Apa yang Diizinkan dalam Fungsi Multi-Pernyataan?
Hanya hal-hal berikut yang diperbolehkan:
- Pernyataan tugas
- Semua pernyataan kontrol aliran, kecuali blok TRY…CATCH
- DECLARE panggilan, digunakan untuk membuat variabel lokal dan kursor
- Anda dapat menggunakan kueri SELECT yang memiliki daftar dengan ekspresi dan menetapkan nilai ini ke variabel yang dideklarasikan secara lokal
- Cursor hanya dapat mereferensikan tabel lokal dan harus dibuka dan ditutup di dalam badan fungsi. FETCH hanya dapat menetapkan atau mengubah nilai variabel lokal, tidak mengambil atau mengubah data basis data
Apa yang Harus Dihindari dalam Fungsi Multi-Pernyataan, Meskipun Diizinkan?
- Anda harus menghindari skenario di mana Anda menggunakan kolom yang dihitung dengan fungsi skalar – ini akan menyebabkan pembuatan ulang indeks dan pembaruan lambat yang memerlukan penghitungan ulang
- Pertimbangkan bahwa setiap fungsi multi-pernyataan memiliki rencana eksekusi dan dampak kinerjanya
- UDF bernilai tabel multi-pernyataan, jika digunakan dalam ekspresi SQL atau pernyataan gabungan akan menjadi lambat karena rencana eksekusi yang tidak optimal
- Jangan gunakan fungsi skalar dalam pernyataan WHERE dan klausa ON kecuali Anda yakin itu akan mengkueri kumpulan data kecil, dan kumpulan data itu akan tetap kecil di masa mendatang
Nama Fungsi dan Parameter
Seperti nama objek lainnya, nama fungsi harus mematuhi aturan untuk pengidentifikasi dan harus unik dalam skemanya. Jika Anda membuat fungsi skalar, Anda dapat menjalankannya dengan menggunakan pernyataan EXECUTE. Dalam hal ini, Anda tidak perlu memasukkan nama skema ke dalam nama fungsi. Lihat contoh pemanggilan fungsi EXECUTE di bawah ini (kami membuat fungsi yang mengembalikan kemunculan hari ke-N dalam sebulan dan kemudian mengambil data ini):
CREATE FUNCTION dbo.fnGetDayofWeekInMonth
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-
(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
-- In SQL Server 2012 and later versions, you can use the EXECUTE command or the SELECT command to run a UDF, or use a standard approach
DECLARE @ret DateTime
EXEC @ret = fnGetDayofWeekInMonth '2020', 'Jan', 'Mon',2
SELECT @ret AS Third_Monday_In_January_2020
SELECT dbo.fnGetDayofWeekInMonth('2020', 'Jan', DEFAULT, DEFAULT)
AS 'Using default',
dbo.fnGetDayofWeekInMonth('2020', 'Jan', 'Mon', 2) AS 'explicit'
Kami dapat menentukan default untuk parameter fungsi, mereka harus diawali dengan "@" dan sesuai dengan aturan penamaan pengenal. Parameter hanya dapat berupa nilai konstan, tidak dapat digunakan dalam kueri SQL sebagai ganti tabel, tampilan, kolom, atau objek database lainnya, dan nilai tidak dapat berupa ekspresi, bahkan yang deterministik. Semua tipe data diperbolehkan, kecuali tipe data TIMESTAMP, dan tidak ada tipe data non-skalar yang dapat digunakan, kecuali untuk parameter bernilai tabel. Dalam panggilan fungsi "standar", Anda harus menentukan atribut DEFAULT jika Anda ingin memberikan kemampuan kepada pengguna akhir untuk membuat parameter opsional. Di versi baru, menggunakan sintaks EXECUTE, ini tidak lagi diperlukan, Anda hanya tidak memasukkan parameter ini dalam panggilan fungsi. Jika kita menggunakan tipe tabel kustom, tabel tersebut harus ditandai sebagai READONLY, artinya kita tidak dapat mengubah nilai awal di dalam fungsi, tetapi dapat digunakan dalam penghitungan dan definisi parameter lain.
Kinerja Fungsi SQL Server
Topik terakhir yang akan kita bahas dalam artikel ini, menggunakan fungsi dari bab sebelumnya, adalah kinerja fungsi. Kami akan memperluas fungsi ini dan memantau waktu eksekusi dan kualitas rencana eksekusi. Kami mulai dengan membuat versi fungsi lain, dan melanjutkan dengan perbandingannya:
CREATE FUNCTION dbo.fnGetDayofWeekInMonthBound
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
WITH SCHEMABINDING
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthInline (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate)
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthTVF (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS @When TABLE (TheDate DATETIME)
WITH schemabinding
AS
Begin
INSERT INTO @When(TheDate)
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
RETURN
end
GO
Buat beberapa panggilan uji dan uji kasus
Kita mulai dengan versi tabel:
SELECT * FROM dbo.fnNthDayOfWeekOfMonthTVF('2020','Feb','Tue',2)
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)),113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
OUTER apply dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)
Membuat data pengujian:
IF EXISTS(SELECT * FROM tempdb.sys.tables WHERE name LIKE '#DataForTest%')
DROP TABLE #DataForTest
GO
SELECT *
INTO #DataForTest
FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
CROSS join (VALUES ('jan'),('feb'),('mar'),('apr'),('may'),('jun'),('jul'),('aug'),('sep'),('oct'),('nov'),('dec'))months(Themonth)
CROSS join (VALUES ('Mon'),('Tue'),('Wed'),('Thu'),('Fri'),('Sat'),('Sun'))day(TheDay)
CROSS join (VALUES (1),(2),(3),(4))nth(nth)
Uji kinerja:
DECLARE @TableLog TABLE (OrderVal INT IDENTITY(1,1), Reason VARCHAR(500), TimeOfEvent DATETIME2 DEFAULT GETDATE())
Waktu mulai:
INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start
Pertama, kami tidak menggunakan jenis fungsi untuk mendapatkan garis dasar:
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0)+ (7*Nth)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0))
[email protected]@DateFirst+(CHARINDEX(TheDay,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate
INTO #Test0
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Using the code entirely unwrapped';
Kami sekarang menggunakan fungsi bernilai Tabel inline yang diterapkan secara silang:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test1
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'Inline function cross apply'
Kami menggunakan fungsi bernilai Tabel inline yang diterapkan secara silang:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsDate
INTO #Test2
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Inline function Derived table'
Untuk membandingkan untrusted, kami menggunakan fungsi skalar dengan schemabinding:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonthBound(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test3
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Trusted (Schemabound) scalar function'
Selanjutnya, kami menggunakan fungsi skalar tanpa pengikatan skema:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test6
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Untrusted scalar function'
Kemudian, fungsi tabel multi-pernyataan diturunkan:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsdate
INTO #Test4
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'multi-statement table function derived'
Akhirnya, tabel multi-pernyataan diterapkan silang:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test5
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'multi-statement cross APPLY'--where the routine you want to time ends
Cantumkan semua pengaturan waktu:
SELECT ending.Reason AS Test, DateDiff(ms, starting.TimeOfEvent,ending.TimeOfEvent) [AS Time (ms)] FROM @TableLog starting
INNER JOIN @TableLog ending ON ending.OrderVal=starting.OrderVal+1
DROP table #Test0
DROP table #Test1
DROP table #Test2
DROP table #Test3
DROP table #Test4
DROP table #Test5
DROP table #Test6
DROP TABLE #DataForTest
Tabel di atas dengan jelas menunjukkan bahwa Anda harus mempertimbangkan kinerja vs fungsionalitas saat Anda menggunakan fungsi yang ditentukan pengguna.
Kesimpulan
Fungsi disukai oleh banyak pengembang, sebagian besar karena merupakan "konstruksi logis". Anda dapat dengan mudah membuat kasus uji, mereka deterministik dan enkapsulasi, mereka terintegrasi dengan baik dengan aliran kode SQL dan memungkinkan fleksibilitas dalam parameterisasi. Mereka adalah pilihan yang baik ketika Anda perlu menerapkan logika kompleks yang perlu dilakukan pada kumpulan data yang lebih kecil atau sudah difilter yang perlu Anda gunakan kembali dalam beberapa skenario. Tampilan tabel sebaris dapat digunakan dalam tampilan yang memerlukan parameter, terutama dari lapisan atas (aplikasi yang menghadap klien). Di sisi lain, fungsi skalar sangat bagus untuk bekerja dengan XML atau format hierarkis lainnya, karena dapat dipanggil secara rekursif.
Fungsi multi-pernyataan yang ditentukan pengguna adalah tambahan yang bagus untuk tumpukan alat pengembangan Anda, tetapi Anda harus memahami cara kerjanya dan apa batasan serta tantangan kinerjanya. Penggunaan yang salah dapat merusak kinerja database apa pun, tetapi jika Anda tahu cara menggunakan fungsi ini, fungsi ini dapat membawa banyak manfaat untuk penggunaan kembali dan enkapsulasi kode.