Teman baik saya Aaron Bertrand menginspirasi saya untuk menulis artikel ini. Dia mengingatkan saya tentang bagaimana kadang-kadang kita menerima begitu saja ketika mereka tampak jelas bagi kita dan tidak selalu repot-repot memeriksa cerita lengkap di baliknya. Relevansinya dengan T-SQL adalah bahwa terkadang kita berasumsi bahwa kita mengetahui segala sesuatu yang perlu diketahui tentang fitur T-SQL tertentu, dan tidak selalu repot-repot memeriksa dokumentasi untuk melihat apakah ada fitur lainnya. Dalam artikel ini saya membahas sejumlah fitur T-SQL yang sering diabaikan, atau yang mendukung parameter atau kemampuan yang sering diabaikan. Jika Anda memiliki contoh permata T-SQL Anda sendiri yang sering diabaikan, silakan bagikan di bagian komentar artikel ini.
Sebelum Anda mulai membaca artikel ini tanyakan pada diri Anda apa yang Anda ketahui tentang fitur T-SQL berikut:EOMONTH, TRANSLATE, TRIM, CONCAT dan CONCAT_WS, LOG, variabel kursor, dan MERGE dengan OUTPUT.
Dalam contoh saya, saya akan menggunakan database sampel yang disebut TSQLV5. Anda dapat menemukan skrip yang membuat dan mengisi database ini di sini, dan diagram ER-nya di sini.
EOMONTH memiliki parameter kedua
Fungsi EOMONTH diperkenalkan di SQL Server 2012. Banyak orang berpikir bahwa itu hanya mendukung satu parameter yang memegang tanggal input, dan itu hanya mengembalikan tanggal akhir bulan yang sesuai dengan tanggal input.
Pertimbangkan kebutuhan yang sedikit lebih canggih untuk menghitung akhir bulan sebelumnya. Misalnya, Anda perlu membuat kueri tabel Sales.Orders, dan mengembalikan pesanan yang dilakukan pada akhir bulan sebelumnya.
Salah satu cara untuk mencapai ini adalah dengan menerapkan fungsi EOMONTH ke SYSDATETIME untuk mendapatkan tanggal akhir bulan dari bulan berjalan, dan kemudian menerapkan fungsi DATEADD untuk mengurangi satu bulan dari hasilnya, seperti:
USE TSQLV5; SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));
Perhatikan bahwa jika Anda benar-benar menjalankan kueri ini di database sampel TSQLV5, Anda akan mendapatkan hasil kosong karena tanggal pemesanan terakhir yang dicatat dalam tabel adalah 6 Mei 2019. Namun, jika tabel memiliki pesanan dengan tanggal pemesanan yang jatuh pada tanggal terakhir hari bulan sebelumnya, kueri akan mengembalikannya.
Apa yang tidak disadari banyak orang adalah bahwa EOMONTH mendukung parameter kedua di mana Anda menunjukkan berapa bulan untuk menambah, atau mengurangi. Berikut sintaks fungsi [yang sepenuhnya didokumentasikan]:
EOMONTH ( start_date [, month_to_add ] )
Tugas kita dapat dicapai dengan lebih mudah dan alami hanya dengan menetapkan -1 sebagai parameter kedua untuk fungsi tersebut, seperti:
SELECT orderid, orderdate FROM Sales.Orders WHERE orderdate = EOMONTH(SYSDATETIME(), -1);
TRANSLATE terkadang lebih sederhana daripada REPLACE
Banyak orang yang akrab dengan fungsi REPLACE dan cara kerjanya. Anda menggunakannya ketika Anda ingin mengganti semua kemunculan satu substring dengan yang lain dalam string input. Namun, terkadang, saat Anda memiliki beberapa pengganti yang perlu diterapkan, menggunakan REPLACE agak rumit dan menghasilkan ekspresi yang berbelit-belit.
Sebagai contoh, misalkan Anda diberi string input @s yang berisi angka dengan format Spanyol. Di Spanyol mereka menggunakan titik sebagai pemisah untuk kelompok ribuan, dan koma sebagai pemisah desimal. Anda perlu mengonversi input ke format AS, di mana koma digunakan sebagai pemisah untuk grup ribuan, dan titik sebagai pemisah desimal.
Menggunakan satu panggilan ke fungsi REPLACE, Anda hanya dapat mengganti semua kemunculan satu karakter atau substring dengan yang lain. Untuk menerapkan dua penggantian (titik ke koma dan koma ke titik), Anda perlu membuat panggilan fungsi bersarang. Bagian yang sulit adalah jika Anda menggunakan REPLACE sekali untuk mengubah titik menjadi koma, dan kemudian untuk kedua kalinya melawan hasil untuk mengubah koma menjadi titik, Anda hanya akan mendapatkan titik. Cobalah:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');
Anda mendapatkan output berikut:
123,456,789.00
Jika Anda ingin tetap menggunakan fungsi REPLACE, Anda memerlukan tiga panggilan fungsi. Satu untuk mengganti titik dengan karakter netral yang Anda tahu tidak biasanya muncul dalam data (misalnya, ~). Lain terhadap hasil untuk mengganti semua koma dengan titik. Lain terhadap hasil untuk mengganti semua kemunculan karakter sementara (~ dalam contoh kami) dengan koma. Berikut ungkapan lengkapnya:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');
Kali ini Anda mendapatkan hasil yang tepat:
123,456,789.00
Ini agak bisa dilakukan, tetapi menghasilkan ekspresi yang panjang dan berbelit-belit. Bagaimana jika Anda memiliki lebih banyak pengganti untuk diterapkan?
Banyak orang tidak menyadari bahwa SQL Server 2017 memperkenalkan fungsi baru yang disebut TRANSLATE yang sangat menyederhanakan penggantian tersebut. Berikut sintaks fungsinya:
TRANSLATE ( inputString, characters, translations )
Input kedua (karakter) adalah string dengan daftar karakter individual yang ingin Anda ganti, dan input ketiga (terjemahan) adalah string dengan daftar karakter terkait yang ingin Anda ganti dengan karakter sumber. Ini secara alami berarti bahwa parameter kedua dan ketiga harus memiliki jumlah karakter yang sama. Yang penting dari fungsinya adalah tidak melakukan pass terpisah untuk setiap penggantian. Jika ya, itu akan berpotensi menghasilkan bug yang sama seperti pada contoh pertama yang saya tunjukkan menggunakan dua panggilan ke fungsi REPLACE. Akibatnya, menangani tugas kita menjadi mudah:
DECLARE @s AS VARCHAR(20) = '123.456.789,00'; SELECT TRANSLATE(@s, '.,', ',.');
Kode ini menghasilkan keluaran yang diinginkan:
123,456,789.00
Itu cukup rapi!
TRIM lebih dari LTRIM(RTRIM())
SQL Server 2017 memperkenalkan dukungan untuk fungsi TRIM. Banyak orang, termasuk saya sendiri, awalnya hanya berasumsi bahwa itu tidak lebih dari jalan pintas sederhana ke LTRIM(RTRIM(input)). Namun, jika Anda memeriksa dokumentasinya, Anda menyadari bahwa itu sebenarnya lebih kuat dari itu.
Sebelum saya masuk ke detailnya, pertimbangkan tugas berikut:berikan string input @s, hapus garis miring di depan dan di belakang (mundur dan maju). Sebagai contoh, misalkan @s berisi string berikut:
//\\ remove leading and trailing backward (\) and forward (/) slashes \\//
Keluaran yang diinginkan adalah:
remove leading and trailing backward (\) and forward (/) slashes
Perhatikan bahwa output harus mempertahankan spasi awal dan akhir.
Jika Anda tidak mengetahui kemampuan penuh TRIM, inilah salah satu cara yang mungkin dapat Anda lakukan untuk menyelesaikan tugas tersebut:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ') AS outputstring;
Solusinya dimulai dengan menggunakan TRANSLATE untuk mengganti semua spasi dengan karakter netral (~) dan garis miring ke depan dengan spasi, kemudian menggunakan TRIM untuk memangkas spasi awal dan akhir dari hasilnya. Langkah ini pada dasarnya memangkas garis miring di depan dan di belakang, untuk sementara menggunakan ~ alih-alih spasi asli. Inilah hasil dari langkah ini:
\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\
Langkah kedua kemudian menggunakan TRANSLATE untuk mengganti semua spasi dengan karakter netral lainnya (^) dan garis miring ke belakang dengan spasi, kemudian menggunakan TRIM untuk memangkas spasi awal dan akhir dari hasilnya. Langkah ini pada dasarnya memangkas garis miring ke belakang dan ke belakang, untuk sementara menggunakan ^ alih-alih spasi perantara. Inilah hasil dari langkah ini:
~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~
Langkah terakhir menggunakan TRANSLATE untuk mengganti spasi dengan garis miring ke belakang, ^ dengan garis miring ke depan, dan ~ dengan spasi, menghasilkan keluaran yang diinginkan:
remove leading and trailing backward (\) and forward (/) slashes
Sebagai latihan, coba selesaikan tugas ini dengan solusi kompatibel pra-SQL Server 2017 di mana Anda tidak dapat menggunakan TRIM dan TRANSLATE.
Kembali ke SQL Server 2017 dan di atasnya, jika Anda repot-repot memeriksa dokumentasi, Anda akan menemukan bahwa TRIM lebih canggih dari yang Anda pikirkan pada awalnya. Berikut Sintaks fungsinya:
TRIM ( [ characters FROM ] string )
Karakter FROM opsional bagian memungkinkan Anda untuk menentukan satu atau lebih karakter yang ingin Anda potong dari awal dan akhir string input. Dalam kasus kami, yang perlu Anda lakukan adalah menentukan '/\' sebagai bagian ini, seperti:
DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//'; SELECT TRIM( '/\' FROM @s) AS outputstring;
Itu peningkatan yang cukup signifikan dibandingkan dengan solusi sebelumnya!
CONCAT dan CONCAT_WS
Jika Anda telah bekerja dengan T-SQL untuk sementara waktu, Anda tahu betapa canggungnya berurusan dengan NULL ketika Anda perlu menggabungkan string. Sebagai contoh, perhatikan data lokasi yang direkam untuk karyawan di tabel HR.Employees:
SELECT empid, country, region, city FROM HR.Employees;
Kueri ini menghasilkan keluaran berikut:
empid country region city ----------- --------------- --------------- --------------- 1 USA WA Seattle 2 USA WA Tacoma 3 USA WA Kirkland 4 USA WA Redmond 5 UK NULL London 6 UK NULL London 7 UK NULL London 8 USA WA Seattle 9 UK NULL London
Perhatikan bahwa untuk beberapa karyawan bagian wilayah tidak relevan dan wilayah yang tidak relevan diwakili oleh NULL. Misalkan Anda perlu menggabungkan bagian lokasi (negara, wilayah, dan kota), menggunakan koma sebagai pemisah, tetapi mengabaikan wilayah NULL. Saat wilayah relevan, Anda ingin hasilnya memiliki bentuk <coutry>,<region>,<city>
dan ketika wilayah tidak relevan Anda ingin hasilnya memiliki bentuk <country>,<city>
. Biasanya, menggabungkan sesuatu dengan NULL menghasilkan hasil NULL. Anda dapat mengubah perilaku ini dengan mematikan opsi sesi CONCAT_NULL_YIELDS_NULL, tetapi saya tidak akan merekomendasikan untuk mengaktifkan perilaku tidak standar.
Jika Anda tidak mengetahui keberadaan fungsi CONCAT dan CONCAT_WS, Anda mungkin akan menggunakan ISNULL atau COALESCE untuk mengganti NULL dengan string kosong, seperti:
SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location FROM HR.Employees;
Inilah output dari kueri ini:
empid location ----------- ----------------------------------------------- 1 USA,WA,Seattle 2 USA,WA,Tacoma 3 USA,WA,Kirkland 4 USA,WA,Redmond 5 UK,London 6 UK,London 7 UK,London 8 USA,WA,Seattle 9 UK,London
SQL Server 2012 memperkenalkan fungsi CONCAT. Fungsi ini menerima daftar input string karakter dan menggabungkannya, dan saat melakukannya, ia mengabaikan NULL. Jadi menggunakan CONCAT Anda dapat menyederhanakan solusi seperti ini:
SELECT empid, CONCAT(country, ',' + region, ',', city) AS location FROM HR.Employees;
Namun, Anda harus secara eksplisit menentukan pemisah sebagai bagian dari input fungsi. Untuk membuat hidup kita lebih mudah, SQL Server 2017 memperkenalkan fungsi serupa yang disebut CONCAT_WS di mana Anda memulai dengan menunjukkan pemisah, diikuti dengan item yang ingin Anda gabungkan. Dengan fungsi ini solusinya lebih disederhanakan seperti ini:
SELECT empid, CONCAT_WS(',', country, region, city) AS location FROM HR.Employees;
Langkah selanjutnya tentu saja mindreading. Pada 1 April 2020 Microsoft berencana untuk merilis CONCAT_MR. Fungsi akan menerima input kosong, dan mencari tahu secara otomatis elemen mana yang Anda inginkan untuk digabungkan dengan membaca pikiran Anda. Kueri kemudian akan terlihat seperti ini:
SELECT empid, CONCAT_MR() AS location FROM HR.Employees;
LOG memiliki parameter kedua
Serupa dengan fungsi EOMONTH, banyak orang tidak menyadari bahwa memulai dengan SQL Server 2012, fungsi LOG mendukung parameter kedua yang memungkinkan Anda untuk menunjukkan basis logaritma. Sebelumnya, T-SQL mendukung fungsi LOG(input) yang mengembalikan logaritma natural dari input (menggunakan konstanta e sebagai basis), dan LOG10(input) yang menggunakan 10 sebagai basis.
Tidak menyadari keberadaan parameter kedua pada fungsi LOG, ketika orang ingin menghitung Logb (x), di mana b adalah basis selain e dan 10, mereka sering melakukannya jauh. Anda dapat mengandalkan persamaan berikut:
Logb (x) =Loga (x)/Loga (b)Sebagai contoh, untuk menghitung Log2 (8), Anda mengandalkan persamaan berikut:
Masuk2 (8) =Loge (8)/Masuke (2)Diterjemahkan ke T-SQL, Anda menerapkan perhitungan berikut:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x) / LOG(@b);
Setelah Anda menyadari bahwa LOG mendukung parameter kedua di mana Anda menunjukkan basis, perhitungannya menjadi:
DECLARE @x AS FLOAT = 8, @b AS INT = 2; SELECT LOG(@x, @b);
Variabel kursor
Jika Anda telah bekerja dengan T-SQL untuk sementara waktu, Anda mungkin memiliki banyak kesempatan untuk bekerja dengan kursor. Seperti yang Anda ketahui, saat bekerja dengan kursor, Anda biasanya menggunakan langkah-langkah berikut:
- Deklarasikan kursor
- Buka kursor
- Ulangi catatan kursor
- Tutup kursor
- Hapus alokasi kursor
Sebagai contoh, anggaplah Anda perlu melakukan beberapa tugas per database dalam instans Anda. Menggunakan kursor, Anda biasanya akan menggunakan kode yang mirip dengan berikut ini:
DECLARE @dbname AS sysname; DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT name FROM sys.databases; OPEN C; FETCH NEXT FROM C INTO @dbname; WHILE @@FETCH_STATUS = 0 BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... do your thing here ... */ FETCH NEXT FROM C INTO @dbname; END; CLOSE C; DEALLOCATE C;
Perintah CLOSE melepaskan kumpulan hasil saat ini dan membebaskan kunci. Perintah DEALLOCATE menghapus referensi kursor, dan ketika referensi terakhir tidak dialokasikan, membebaskan struktur data yang terdiri dari kursor. Jika Anda mencoba menjalankan kode di atas dua kali tanpa perintah CLOSE dan DEALLOCATE, Anda akan mendapatkan error berikut:
Msg 16915, Level 16, State 1, Line 4 A cursor with the name 'C' already exists. Msg 16905, Level 16, State 1, Line 6 The cursor is already open.
Pastikan Anda menjalankan perintah CLOSE dan DEALLOCATE sebelum melanjutkan.
Banyak orang tidak menyadari bahwa ketika mereka perlu bekerja dengan kursor hanya dalam satu batch, yang merupakan kasus paling umum, alih-alih menggunakan kursor biasa, Anda dapat bekerja dengan variabel kursor. Seperti variabel apa pun, ruang lingkup variabel kursor hanya kumpulan di mana ia dideklarasikan. Ini berarti bahwa segera setelah batch selesai, semua variabel kedaluwarsa. Menggunakan variabel kursor, setelah batch selesai, SQL Server menutup dan membatalkan alokasinya secara otomatis, sehingga Anda tidak perlu menjalankan perintah CLOSE dan DEALLOCATE secara eksplisit.
Berikut kode yang direvisi menggunakan variabel kursor kali ini:
DECLARE @dbname AS sysname, @C AS CURSOR; SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR SELECT name FROM sys.databases; OPEN @C; FETCH NEXT FROM @C INTO @dbname; WHILE @@FETCH_STATUS = 0 BEGIN PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...'; /* ... do your thing here ... */ FETCH NEXT FROM @C INTO @dbname; END;
Jangan ragu untuk menjalankannya beberapa kali dan perhatikan bahwa kali ini Anda tidak mendapatkan kesalahan apa pun. Ini lebih bersih, dan Anda tidak perlu khawatir menyimpan sumber daya kursor jika Anda lupa menutup dan membatalkan alokasi kursor.
GABUNG dengan OUTPUT
Sejak dimulainya klausa OUTPUT untuk pernyataan modifikasi di SQL Server 2005, ternyata menjadi alat yang sangat praktis setiap kali Anda ingin mengembalikan data dari baris yang dimodifikasi. Orang-orang menggunakan fitur ini secara teratur untuk tujuan seperti pengarsipan, audit, dan banyak kasus penggunaan lainnya. Namun, salah satu hal yang mengganggu tentang fitur ini adalah jika Anda menggunakannya dengan pernyataan INSERT, Anda hanya diperbolehkan mengembalikan data dari baris yang disisipkan, mengawali kolom output dengan disisipkan . Anda tidak memiliki akses ke kolom tabel sumber, meskipun terkadang Anda perlu mengembalikan kolom dari sumber di samping kolom dari target.
Sebagai contoh, perhatikan tabel T1 dan T2, yang Anda buat dan isi dengan menjalankan kode berikut:
DROP TABLE IF EXISTS dbo.T1, dbo.T2; GO CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL); CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL); INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');
Perhatikan bahwa properti identitas digunakan untuk menghasilkan kunci di kedua tabel.
Misalkan Anda perlu menyalin beberapa baris dari T1 ke T2; katakanlah, yang di mana keycol % 2 =1. Anda ingin menggunakan klausa OUTPUT untuk mengembalikan kunci yang baru dibuat di T2, tetapi Anda juga ingin mengembalikan di samping kunci tersebut masing-masing kunci sumber dari T1. Harapan intuitif adalah menggunakan pernyataan INSERT berikut:
INSERT INTO dbo.T2(datacol) OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;
Sayangnya, seperti yang disebutkan, klausa OUTPUT tidak mengizinkan Anda untuk merujuk ke kolom dari tabel sumber, jadi Anda mendapatkan kesalahan berikut:
Msg 4104, Level 16, State 1, Line 2Pengidentifikasi multi-bagian "T1.keycol" tidak dapat diikat.
Banyak orang tidak menyadari bahwa anehnya batasan ini tidak berlaku untuk pernyataan MERGE. Jadi meskipun agak canggung, Anda dapat mengubah pernyataan INSERT Anda menjadi pernyataan MERGE, tetapi untuk melakukannya, Anda memerlukan predikat MERGE untuk selalu salah. Ini akan mengaktifkan klausa WHEN NOT MATCHED dan menerapkan satu-satunya tindakan INSERT yang didukung di sana. Anda dapat menggunakan kondisi palsu palsu seperti 1 =2. Berikut kode lengkap yang dikonversi:
MERGE INTO dbo.T2 AS TGT USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC ON 1 = 2 WHEN NOT MATCHED THEN INSERT(datacol) VALUES(SRC.datacol) OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;
Kali ini kode berhasil dijalankan, menghasilkan output berikut:
T1_keycol T2_keycol ----------- ----------- 1 1 3 2 5 3
Mudah-mudahan, Microsoft akan meningkatkan dukungan untuk klausa OUTPUT dalam pernyataan modifikasi lainnya untuk memungkinkan pengembalian kolom dari tabel sumber juga.
Kesimpulan
Jangan berasumsi, dan RTFM! :-)