Sqlserver
 sql >> Teknologi Basis Data >  >> RDS >> Sqlserver

10 SP_EXECUTESQL Gotchas yang Harus Dihindari untuk SQL Dinamis yang Lebih Baik

Tahukah Anda seberapa kuat alat seperti SQL dinamis? Gunakan dengan cara yang salah, dan Anda dapat mengizinkan seseorang mengambil alih database Anda. Plus, mungkin ada terlalu banyak kerumitan. Artikel ini bertujuan untuk memperkenalkan jebakan saat menggunakan SP_EXECUTESQL dan menawarkan 10 gotcha paling umum yang harus dihindari.

SP_EXECUTESQL adalah salah satu cara Anda dapat menjalankan perintah SQL yang disematkan dalam sebuah string. Anda membangun string ini secara dinamis melalui kode. Itu sebabnya kami menyebutnya SQL dinamis. Selain serangkaian pernyataan, Anda juga dapat memasukkan daftar parameter dan nilai ke dalamnya. Faktanya, parameter dan nilai ini berbeda dari perintah EXEC. EXEC tidak menerima parameter untuk SQL dinamis. Namun, Anda menjalankan SP_EXECUTESQL menggunakan EXEC!

Untuk pemula dalam SQL dinamis, inilah cara Anda memanggil ini.

EXEC sp_executesql <command string>[, <input or output parameters list>, <parameter value1>, <parameter value n>]

Anda membentuk string perintah yang menyertakan pernyataan SQL yang valid. Secara opsional, Anda dapat meneruskan daftar parameter input atau output dan tipe datanya. Dan akhirnya, Anda memberikan daftar nilai yang dipisahkan koma. Jika Anda melewati parameter, Anda harus memberikan nilai. Nanti, Anda akan melihat contoh yang benar dan salah saat membaca.

Menggunakan SP_EXECUTESQL Saat Anda Tidak Membutuhkannya

Betul sekali. Jika Anda tidak membutuhkannya, jangan menggunakannya. Jika ini menjadi 10 perintah SP_EXECUTESQL, ini adalah yang pertama. Karena prosedur sistem ini dapat dengan mudah disalahgunakan. Tapi bagaimana Anda tahu?

Jawab ini:Apakah ada masalah jika perintah di SQL dinamis Anda menjadi statis? Jika Anda tidak memiliki apa-apa untuk dikatakan tentang hal ini, maka Anda tidak membutuhkannya. Lihat contohnya.

DECLARE @sql NVARCHAR(100) = N'SELECT ProductID, Name FROM Production.Product ' +
			      'WHERE ProductID = @ProductID';
DECLARE @paramsList NVARCHAR(100) = N'@ProductID INT';
DECLARE @param1Value INT = 1;

EXEC sp_executesql @sql, @paramsList, @param1Value
GO

Seperti yang Anda lihat, penggunaan SP_EXECUTESQL lengkap dengan string perintah, parameter, dan nilai. Tapi apakah harus seperti ini? Pasti tidak. Tidak apa-apa memiliki ini:

DECLARE @productID INT = 1;

SELECT ProductID, Name
FROM Production.Product
WHERE ProductID = @productID;

Tapi saya mungkin malu ketika Anda melihat contoh lebih lanjut di artikel. Karena saya akan bertentangan dengan apa yang saya klaim di poin pertama ini. Anda akan melihat pernyataan SQL dinamis pendek yang lebih baik daripada statis. Jadi, bersabarlah karena contoh hanya akan membuktikan poin yang diuraikan di sini. Untuk contoh lainnya, berpura-puralah sejenak bahwa Anda sedang melihat kode yang dimaksudkan untuk SQL dinamis.

Objek dan Variabel di Luar Cakupan

Menjalankan serangkaian perintah SQL dalam string menggunakan SP_EXECUTESQL seperti membuat prosedur tersimpan tanpa nama dan menjalankannya. Mengetahui hal ini, objek seperti tabel dan variabel sementara di luar string perintah akan berada di luar cakupan. Karena itu, kesalahan runtime akan terjadi.

Saat Menggunakan Variabel SQL

Lihat ini.

DECLARE @extraText VARCHAR(10) = 'Name is '; -- note this variable
DECLARE @sql NVARCHAR(100) = N'SELECT @extraText + FirstName + SPACE(1) + LastName
                               FROM Person.Person WHERE BusinessEntityID = @BusinessEntityID';
DECLARE @paramList NVARCHAR(100) = N'@BusinessEntityID INT';
DECLARE @param1Value INT = 1;

EXEC sp_executesql @sql, @paramList, @BusinessEntityId = @param1Value;
GO

Variabel @teks ekstra tidak terlihat oleh perintah yang dijalankan. Alih-alih ini, string literal atau variabel yang dideklarasikan di dalam string SQL dinamis jauh lebih baik. Bagaimanapun, hasilnya adalah:

Apakah Anda melihat kesalahan itu pada Gambar 1? Jika Anda perlu memberikan nilai di dalam string SQL dinamis, tambahkan parameter lain.

Saat Menggunakan Tabel Sementara

Tabel sementara di SQL Server juga ada dalam lingkup modul. Jadi, apa pendapat Anda tentang kode ini?

DECLARE @sql NVARCHAR(200) = N'SELECT BusinessEntityID, LastName, FirstName, MiddleName
                               INTO #TempNames
                               FROM Person.Person
                               WHERE BusinessEntityID BETWEEN 1 and 100';

EXEC sp_executesql @sql;
EXEC sp_executesql N'SELECT * FROM #TempNames'
GO

Kode di atas menjalankan 2 prosedur tersimpan secara berurutan. Tabel sementara yang dibuat dari SQL dinamis pertama tidak akan dapat diakses oleh yang kedua. Akibatnya, Anda akan mendapatkan Nama Objek Tidak Valid #TempNames kesalahan.

Kesalahan SQL Server QUOTENAME

Berikut contoh lain yang akan terlihat bagus pada pandangan pertama tetapi akan menyebabkan kesalahan Nama Objek Tidak Valid. Lihat kode di bawah ini.

DECLARE @sql NVARCHAR(100) = N'SELECT * FROM @Table';
DECLARE @tableName NVARCHAR(20) = 'Person.Person';

SET @sql = REPLACE(@sql,'@Table',QUOTENAME(@tableName));

PRINT @sql;
EXEC sp_executesql @sql;
GO

Agar valid untuk SELECT, lampirkan skema dan tabel dengan tanda kurung siku seperti ini:[Schema].[Table] . Atau jangan sertakan sama sekali (Kecuali nama tabel menyertakan satu spasi atau lebih). Dalam contoh di atas, tidak ada tanda kurung siku yang digunakan untuk tabel dan skema. Alih-alih menggunakan @Table sebagai parameter, itu menjadi pengganti untuk REPLACE. QUOTENAME telah digunakan.

QUOTENAME mengapit string dengan pembatas. Ini juga bagus untuk menangani tanda kutip tunggal dalam string SQL dinamis. Tanda kurung siku adalah pembatas default. Jadi, dalam contoh di atas, menurut Anda apa yang dilakukan QUOTENAME? Periksa Gambar 2 di bawah ini.

Pernyataan SQL PRINT membantu kami men-debug masalah dengan mencetak string SQL dinamis. Sekarang kita tahu masalahnya. Apa cara terbaik untuk memperbaikinya? Salah satu solusinya adalah kode di bawah ini:

DECLARE @sql NVARCHAR(100) = N'SELECT COUNT(*) FROM @Table';
DECLARE @tableName NVARCHAR(20) = 'Person.Person';

SET @sql = REPLACE(@sql,'@Table',QUOTENAME(PARSENAME(@tableName,2)) + '.'
                               + QUOTENAME(PARSENAME(@tableName,1)));
PRINT @sql;
EXEC sp_executesql @sql;
GO

Mari kita jelaskan ini.

Pertama, @Tabel digunakan sebagai pengganti untuk REPLACE. Ini akan mencari kemunculan @Table dan menggantinya dengan nilai yang benar. Mengapa? Jika ini digunakan sebagai parameter, kesalahan akan terjadi. Itu juga alasan untuk menggunakan REPLACE.

Kemudian, kami menggunakan PARSENAME. String yang kami berikan ke fungsi ini akan memisahkannya ke skema dan tabel. PARSENAME(@tableName,2) akan mendapatkan skema. Sementara PARSENAME(@tableName,1) akan mendapatkan meja.

Terakhir, QUOTENAME akan menyertakan nama skema dan tabel secara terpisah setelah PARSENAME selesai. Hasilnya adalah [Orang].[Orang] . Sekarang, ini valid.

Namun, cara yang lebih baik untuk membersihkan string SQL dinamis dengan nama objek akan ditampilkan nanti.

Mengeksekusi SP_EXECUTESQL dengan Pernyataan NULL

Ada hari-hari ketika Anda kesal atau putus asa. Kesalahan dapat dibuat di sepanjang jalan. Kemudian tambahkan string SQL dinamis panjang ke dalam campuran dan NULL. Dan hasilnya?

Tidak ada.

SP_EXECUTESQL akan memberi Anda hasil kosong. Bagaimana? Dengan menggabungkan NULL secara tidak sengaja. Perhatikan contoh di bawah ini:

DECLARE @crlf NCHAR(2);
DECLARE @sql NVARCHAR(200) = N'SELECT' + @crlf +
	' p.Name AS Product' + @crlf +
	',v.Name AS Vendor' + @crlf +
	',v.AccountNumber' + @crlf +
	',p.ListPrice' + @crlf +
	'FROM Purchasing.ProductVendor pv' + @crlf +
	'INNER JOIN Production.Product p ON pv.ProductID = p.ProductID' + @crlf +
	'INNER JOIN Purchasing.Vendor v ON pv.BusinessEntityID = v.BusinessEntityID' + @crlf +
	'WHERE pv.BusinessEntityID = @BusinessEntityID';
DECLARE @paramsList NVARCHAR(100) = N'@BusinessEntityID INT';
DECLARE @BusinessEntityID INT = 1500;

PRINT @sql;
EXEC sp_executesql @sql, @paramsList, @BusinessEntityID
GO

Pada awalnya, kodenya terlihat bagus. Namun mata elang di antara kita akan melihat @crlf variabel. Nilainya? Variabel tidak diinisialisasi. Jadi, ini NULL.

Tapi apa gunanya variabel itu? Di bagian selanjutnya, Anda akan mengetahui betapa pentingnya memformat dan men-debug. Untuk saat ini, mari fokus pada poin yang ada.

Pertama, menggabungkan variabel NULL ke string SQL dinamis akan menghasilkan NULL. Kemudian, PRINT akan mencetak kosong. Akhirnya, SP_EXECUTESQL akan berjalan dengan baik dengan string SQL dinamis NULL. Tapi itu tidak menghasilkan apa-apa.

NULL dapat membuat kita terpesona pada hari yang sudah buruk. Istirahat sejenak. Santai. Kemudian kembalilah dengan pikiran yang lebih jernih.

Nilai Parameter Sebaris

Berantakan.

Seperti itulah tampilan nilai inlining ke string SQL dinamis. Akan ada banyak tanda kutip tunggal untuk string dan tanggal. Jika Anda tidak hati-hati, O'Briens dan O'Neils juga akan menyebabkan kesalahan. Dan karena SQL dinamis adalah string, Anda harus CONVERT atau CAST nilai ke string. Ini contohnya.

DECLARE @shipDate DATETIME = '06/11/2011';
 DECLARE @productID INT = 750;
 DECLARE @sql NVARCHAR(1000);
 SET @sql = N'SELECT
  soh.ShipDate
 ,sod.ProductID
 ,SUM(sod.OrderQty) AS TotalQty
 ,SUM(sod.LineTotal) AS LineTotal
 FROM Sales.SalesOrderHeader soh
 INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
 WHERE soh.ShipDate BETWEEN ' + '''' + CONVERT(VARCHAR(10), @shipDate, 101) + '''' + ' AND DATEADD(MONTH,1,' + '''' + CONVERT(VARCHAR(10), @shipDate, 101) + ''') ' +
 'AND sod.ProductID = ' + CAST(@productID AS VARCHAR(8)) +
 ' GROUP BY soh.ShipDate, sod.ProductID' +
 ' ORDER BY sod.ProductID';
 
 PRINT @sql;
 EXEC sp_executesql @sql;

Saya melihat string dinamis yang lebih berantakan dari ini. Perhatikan tanda kutip tunggal, CONVERT, dan CAST. Jika parameter digunakan, ini bisa terlihat lebih baik. Anda dapat melihatnya di bawah

DECLARE @shipDate DATETIME = '06/11/2011';
 DECLARE @productID INT = 750;
 DECLARE @sql NVARCHAR(1000);
 DECLARE @paramList NVARCHAR(500) = N'@shipDate DATETIME, @productID INT';
 SET @sql = N'SELECT
  soh.ShipDate
 ,sod.ProductID
 ,SUM(sod.OrderQty) AS TotalQty
 ,SUM(sod.LineTotal) AS LineTotal
 FROM Sales.SalesOrderHeader soh
 INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
 WHERE soh.ShipDate BETWEEN @shipDate AND DATEADD(MONTH,1,@shipDate)
 AND sod.ProductID = @productID
  GROUP BY soh.ShipDate, sod.ProductID
  ORDER BY sod.ProductID';

PRINT @sql;
EXEC sp_executesql @sql, @paramList, @shipDate, @productID
GO

Lihat? Lebih sedikit tanda kutip tunggal, tidak ada CONVERT dan CAST, dan juga lebih bersih.

Tapi ada efek samping yang lebih berbahaya dari nilai sebaris.

Injeksi SQL

Jika kita hidup di dunia di mana semua orang baik, injeksi SQL tidak akan pernah terpikirkan. Tapi itu tidak terjadi. Seseorang mungkin menyuntikkan kode SQL berbahaya ke Anda. Bagaimana ini bisa terjadi?

Inilah skenario yang akan kita gunakan dalam contoh kita:

  • Nilai digabungkan ke string SQL dinamis seperti contoh kita sebelumnya. Tidak ada parameter.
  • String SQL dinamis hingga 2GB menggunakan NVARCHAR(MAX). Banyak ruang untuk memasukkan kode berbahaya.
  • Yang terburuk, string SQL dinamis dijalankan dengan izin yang lebih tinggi.
  • Contoh SQL Server menerima otentikasi SQL.

Apakah ini terlalu banyak? Untuk sistem yang dikelola oleh satu orang, ini bisa terjadi. Lagipula tidak ada yang memeriksanya. Untuk perusahaan besar, terkadang ada departemen TI yang lemah dalam hal keamanan.

Apakah ini masih menjadi ancaman hari ini? Hal ini, menurut penyedia layanan cloud Akamai dalam laporan State of the Internet/Security mereka. Antara November 2017 hingga Maret 2019, injeksi SQL mewakili hampir dua pertiga dari semua serangan aplikasi web. Itu yang tertinggi dari semua ancaman yang diperiksa. Sangat buruk.

Apakah Anda ingin melihatnya sendiri?

Praktek Injeksi SQL: Contoh Buruk

Mari kita lakukan beberapa injeksi SQL dalam contoh ini. Anda dapat mencoba ini di AdventureWorks Anda sendiri basis data. Namun pastikan bahwa autentikasi SQL diizinkan, dan Anda menjalankannya dengan izin yang lebih tinggi.

DECLARE @lastName NVARCHAR(MAX) = 'Mu';
DECLARE @firstName NVARCHAR(MAX) = 'Zheng''; CREATE LOGIN sà WITH PASSWORD=''12345''; ALTER SERVER ROLE sysadmin ADD MEMBER sà; --';
DECLARE @crlf NCHAR(2) = nchar(13) + nchar(10);

DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @crlf +
' p.LastName ' + @crlf +
',p.FirstName ' + @crlf +
',a.AddressLine1 ' + @crlf +
',a.AddressLine2 ' + @crlf +
',a.City ' + @crlf +
'FROM Person.Person p ' + @crlf +
'INNER JOIN Person.BusinessEntityAddress bea ON p.BusinessEntityID = bea.BusinessEntityID ' + @crlf +
'INNER JOIN Person.Address a ON bea.AddressID = a.AddressID ' + @crlf +
'WHERE p.LastName = ' + NCHAR(39) + @lastName + NCHAR(39) + ' ' + @crlf +
'AND p.FirstName = '  + NCHAR(39) + @firstName + NCHAR(39);

-- SELECT @sql;	-- uncomment if you want to see what's in @sql					
EXEC sp_executesql @sql;
GO

Kode di atas tidak mewakili kode sebenarnya dari perusahaan yang sudah ada. Itu bahkan tidak dapat dipanggil oleh aplikasi. Tapi ini menggambarkan perbuatan jahat. Jadi, apa yang kita miliki di sini?

Pertama, kode yang disuntikkan akan membuat akun SQL yang terlihat seperti sa , tapi tidak. Dan seperti sa , ini memiliki sysadmin izin. Akhirnya, ini akan digunakan untuk mengakses database kapan saja dengan hak penuh. Apa pun mungkin terjadi setelah ini dilakukan:mencuri, menghapus, merusak data perusahaan, sebut saja.

Apakah kode ini akan berjalan? Pastinya! Dan setelah itu, akun super akan dibuat secara diam-diam. Dan, tentu saja, alamat Zheng Mu akan muncul di set hasil. Segala sesuatu yang lain normal. Teduh, bukan begitu?

Setelah Anda menjalankan kode di atas, coba jalankan ini juga:

SELECT IS_SRVROLEMEMBER('sysadmin','sà')

Jika mengembalikan 1, dia masuk, siapa pun ini adalah. Atau, Anda dapat memeriksanya di Login Keamanan SQL Server Anda di SQL Server Management Studio.

Jadi, apa yang Anda dapatkan?

Menakutkan, bukan? (Jika ini nyata.)

Praktik Injeksi SQL:Contoh yang Baik

Sekarang, mari kita ubah kodenya sedikit dengan menggunakan parameter. Kondisi lainnya masih sama.

DECLARE @lastName NVARCHAR(MAX) = 'Mu';
DECLARE @firstName NVARCHAR(MAX) = 'Zheng''; CREATE LOGIN sà WITH PASSWORD=''12345''; ALTER SERVER ROLE sysadmin ADD MEMBER sà; --';
DECLARE @crlf NCHAR(2) = nchar(13) + nchar(10);

DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @crlf +
' p.LastName ' + @crlf +
',p.FirstName ' + @crlf +
',a.AddressLine1 ' + @crlf +
',a.AddressLine2 ' + @crlf +
',a.City ' + @crlf +
'FROM Person.Person p ' + @crlf +
'INNER JOIN Person.BusinessEntityAddress bea ON p.BusinessEntityID = bea.BusinessEntityID ' + @crlf +
'INNER JOIN Person.Address a ON bea.AddressID = a.AddressID ' + @crlf +
'WHERE p.LastName = @lastName' + @crlf +
'AND p.FirstName = @firstName';

DECLARE @paramList NVARCHAR(300) = N'@lastName NVARCHAR(50), @firstName NVARCHAR(50)';

-- SELECT @sql;	-- uncomment if you want to see what's in @sql
EXEC sp_executesql @sql, @paramList, @lastName, @firstName;
GO

Ada 2 perbedaan hasil dibandingkan dengan contoh pertama.

  • Pertama, alamat Zheng Mu tidak akan muncul. Kumpulan hasil kosong.
  • Lalu, akun pemberontak tidak dibuat. Menggunakan IS_SRVROLEMEMBER akan menghasilkan NULL.

Apa yang terjadi?

Karena parameter digunakan, nilai @namapertama adalah 'Zheng"; BUAT LOGIN sà DENGAN PASSWORD="12345"; ALT' . Ini diambil sebagai nilai literal dan dipotong menjadi 50 karakter saja. Periksa parameter nama depan dalam kode di atas. Ini NVARCHAR (50). Itu sebabnya set hasil kosong. Tidak ada orang dengan nama depan seperti itu di database.

Ini hanyalah salah satu contoh injeksi SQL dan salah satu cara untuk menghindarinya. Ada lebih banyak yang terlibat dalam melakukan hal yang sebenarnya. Tapi saya harap saya menjelaskan mengapa nilai inline dalam SQL dinamis buruk.

Pengendus Parameter

Pernahkah Anda mengalami prosedur tersimpan yang berjalan lambat dari suatu aplikasi, tetapi ketika Anda mencoba menjalankannya di SSMS, itu menjadi cepat? Ini membingungkan karena Anda menggunakan nilai parameter yang sama persis dengan yang digunakan di aplikasi.

Itu parameter mengendus dalam tindakan. SQL Server membuat rencana eksekusi saat pertama kali prosedur tersimpan dijalankan atau dikompilasi ulang. Kemudian, gunakan kembali rencana untuk putaran berikutnya. Kedengarannya bagus karena SQL Server tidak perlu membuat ulang paket setiap saat. Namun ada kalanya nilai parameter yang berbeda memerlukan rencana yang berbeda agar dapat berjalan dengan cepat.

Berikut demonstrasi menggunakan SP_EXECUTESQL dan SQL statis biasa.

DECLARE @sql NVARCHAR(150) = N'SELECT Name FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID';
DECLARE @paramList NVARCHAR(100) = N'@ProductSubcategoryID INT';
DECLARE @ProductSubcategoryID INT = 23;

EXEC sp_executesql @sql, @paramList, @ProductSubcategoryID

Yang satu ini sangat sederhana. Periksa rencana eksekusi pada Gambar 3.

Sekarang mari kita coba kueri yang sama menggunakan SQL statis.

DECLARE @ProductSubcategoryID INT = 23;
SELECT Name FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID

Lihat Gambar 4, lalu bandingkan dengan Gambar 3.

Pada Gambar 3, Pencarian Indeks dan Loop Bersarang digunakan. Namun pada Gambar 4, ini adalah Pemindaian Indeks Berkelompok . Meskipun tidak ada penalti kinerja yang terlihat pada saat ini, ini menunjukkan sniffing parameter bukan hanya imajinasi.

Ini bisa sangat membuat frustrasi setelah kueri menjadi lambat. Anda mungkin akhirnya menggunakan teknik seperti mengkompilasi ulang atau menggunakan petunjuk kueri untuk menghindarinya. Semua ini memiliki kekurangan.

String SQL Dinamis Tidak Diformat di SP_EXECUTESQL

Apa yang bisa salah dengan kode ini?

DECLARE @sql NVARCHAR(100) = N'SELECT COUNT(*) AS ProductCount' +
                              'FROM Production.Product';
PRINT @sql;
EXEC sp_executesql @sql;

Ini singkat dan sederhana. Tapi periksa Gambar 5 di bawah ini.

Kesalahan terjadi jika Anda tidak keberatan dengan satu spasi antara kata kunci dan objek saat membentuk string SQL dinamis. Seperti pada Gambar 5, di mana ProductCount alias kolom dan kata kunci FROM tidak memiliki spasi di antaranya. Ini menjadi membingungkan setelah bagian dari string mengalir ke baris kode berikutnya. Itu membuat Anda berpikir bahwa sintaksnya benar.

Perhatikan juga bahwa string menggunakan 2 baris di jendela kode, tetapi output PRINT menunjukkan 1 baris. Bayangkan jika ini adalah string perintah yang sangat, sangat panjang. Sulit untuk menemukan masalahnya sampai Anda memformat string dengan benar dari tab Pesan.

Untuk mengatasi masalah ini, tambahkan carriage return dan line feed. Anda mungkin melihat variabel @crlf dari contoh-contoh sebelumnya. Memformat string SQL dinamis Anda dengan spasi dan baris baru akan membuat string SQL dinamis lebih mudah dibaca. Ini bagus untuk debugging juga.

Pertimbangkan pernyataan SELECT dengan JOIN. Perlu beberapa baris kode seperti contoh di bawah ini.

DECLARE @sql NVARCHAR(400)
DECLARE @shipDate DATETIME = '06/11/2011';
DECLARE @paramList NVARCHAR(100) = N'@shipDate DATETIME';
DECLARE @crlf NCHAR(2) = NCHAR(13) + NCHAR(10);

set @sql = N'SELECT ' + @crlf +
 'soh.ShipDate ' + @crlf +
 ',sod.ProductID ' + @crlf +
 ',SUM(sod.OrderQty) AS TotalQty ' + @crlf +
 ',SUM(sod.LineTotal) AS LineTotal ' + @crlf +
 'FROM Sales.SalesOrderHeader soh ' + @crlf +
 'INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID ' + @crlf +
 'WHERE soh.ShipDate = @shipDate' + @crlf +
 'GROUP BY soh.ShipDate, sod.ProductID ' + @crlf +
 'ORDER BY sod.ProductID';

 PRINT @sql;
 EXEC sp_executesql @sql,@paramList,@shipDate
 GO

Untuk memformat string, @crlf variabel diatur ke NCHAR(13), carriage return, dan NCHAR(10), feed baris. Itu digabungkan ke setiap baris untuk memutus string panjang pernyataan SELECT. Untuk melihat hasilnya di tab Pesan, kami menggunakan PRINT. Periksa output pada Gambar 6 di bawah ini.

Bagaimana Anda membentuk string SQL dinamis terserah Anda. Apa pun yang cocok untuk Anda agar jelas, mudah dibaca, dan mudah di-debug ketika saatnya tiba.

Nama Objek Tidak Dibersihkan

Apakah Anda perlu mengatur nama tabel, tampilan, atau database secara dinamis untuk alasan apa pun? Kemudian Anda perlu "membersihkan" atau memvalidasi nama objek ini untuk menghindari kesalahan.

Dalam contoh kami, kami akan menggunakan nama tabel, meskipun prinsip validasi mungkin berlaku untuk tampilan juga. Cara Anda menghadapinya selanjutnya akan berbeda.

Sebelumnya, kami menggunakan PARSENAME untuk memisahkan nama skema dari nama tabel. Itu juga dapat digunakan jika string memiliki nama server dan database. Namun dalam contoh ini, kita hanya akan menggunakan nama skema dan tabel. Saya serahkan sisanya pada pikiran brilian Anda. Ini akan berfungsi terlepas dari apakah Anda memberi nama tabel Anda dengan atau tanpa spasi. Spasi pada tabel atau nama tampilan adalah valid. Jadi, ini berfungsi untuk dbo.MyFoodCravings atau [dbo].[Mengidam Makanan Saya] .

CONTOH

Mari kita buat tabel.

CREATE TABLE [dbo].[My Favorite Bikes]
(
	id INT NOT NULL,
	BikeName VARCHAR(50)
)
GO

Kemudian kita buat script yang akan menggunakan SP_EXECUTESQL. Ini akan menjalankan pernyataan DELETE generik untuk tabel apa pun yang diberi kunci 1 kolom. Hal pertama yang harus dilakukan adalah mengurai nama objek lengkap.

DECLARE @object NVARCHAR(128) = '[dbo].[My Favorite Bikes]';
DECLARE @schemaName NVARCHAR(128) = PARSENAME(@object,2);
DECLARE @tableName NVARCHAR(128) = PARSENAME(@object,1);

Dengan cara ini kami memisahkan skema dari tabel. Untuk memvalidasi lebih lanjut, kami menggunakan OBJECT_ID. Jika bukan NULL, maka valid.

IF NOT OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName)) IS NULL
BEGIN
	PRINT @object + ' is valid!'
	-- do the rest of your stuff here
END
ELSE
BEGIN
        PRINT 'Invalid object name ' + @object
	-- if you need to do anything else, insert it here
END

Perhatikan juga bahwa kami menggunakan QUOTENAME. Ini akan memastikan bahwa nama tabel dengan spasi tidak akan memicu kesalahan dengan mengapitnya dengan tanda kurung siku.

Tetapi bagaimana dengan memvalidasi kolom kunci? Anda dapat memeriksa keberadaan kolom tabel target di sys.columns .

IF (SELECT COUNT(*) FROM sys.columns
	    WHERE [object_id] = OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName))
		  AND [name] = @idKey) > 0
BEGIN
     -- add miscellaneous code here, if needed
     EXEC sp_executesql @sql, @paramsList, @id
END
ELSE
BEGIN
     PRINT 'Invalid column name ' + @idKey + ' for object ' + @object
     -- if you need to do anything else, insert it here
END

Sekarang, inilah skrip lengkap dari apa yang ingin kami capai.

DECLARE @object NVARCHAR(128) = '[dbo].[My Favorite Bikes]';
DECLARE @schemaName NVARCHAR(128) = PARSENAME(@object,2);
DECLARE @tableName NVARCHAR(128) = PARSENAME(@object,1);
DECLARE @isDebug BIT = 1;
DECLARE @idKey NVARCHAR(128) = N'id';

DECLARE @sql NVARCHAR(200) = N'DELETE FROM @object WHERE @idKey = @id';
DECLARE @id INT = 0;
DECLARE @paramList NVARCHAR(100) = N'@id INT';

IF NOT OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName)) IS NULL
BEGIN
   PRINT @object + ' is valid!'
   
   IF (SELECT COUNT(*) FROM sys.columns
       WHERE [object_id] = OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName))
         AND [name] = @idKey) > 0
   BEGIN
       SET @sql = REPLACE(@sql, '@object', QUOTENAME(@schemaName) + '.' +          
                  QUOTENAME(@tableName));
       SET @sql = REPLACE(@sql, '@idkey', QUOTENAME(@idKey));
       IF @isDebug = 1
	   PRINT @sql;
       EXEC sp_executesql @sql, @paramList, @id
   END
   ELSE
       PRINT 'Invalid column name ' + @idKey + ' for object ' + @object
END
ELSE
BEGIN
   PRINT 'Invalid object name ' + @object
   -- if you need to do anything else, insert it here
END
GO

Hasil dari script ini ada pada Gambar 7 di bawah ini.

Anda dapat mencoba ini dengan tabel lain. Cukup ubah @objek , @idkey , dan @id nilai variabel.

Tidak Ada Ketentuan untuk Debug

Kesalahan bisa terjadi. Jadi, Anda perlu mengetahui string SQL dinamis yang dihasilkan untuk menemukan akar masalahnya. Kami bukan peramal atau pesulap untuk menebak bentuk string SQL dinamis. Jadi, Anda memerlukan tanda debug.

Perhatikan pada Gambar 7 sebelumnya bahwa string SQL dinamis dicetak di tab Pesan SSMS. Kami menambahkan @isDebug variabel BIT dan atur ke 1 dalam kode. Ketika nilainya 1, string SQL dinamis akan dicetak. Ini bagus jika Anda perlu men-debug skrip atau prosedur tersimpan seperti ini. Setel kembali ke nol saat Anda selesai melakukan debugging. Jika ini adalah prosedur tersimpan, jadikan tanda ini sebagai parameter opsional dengan nilai default nol.

Untuk melihat string SQL dinamis, Anda dapat menggunakan 2 metode yang mungkin.

  • Gunakan PRINT jika string kurang dari atau sama dengan 8000 karakter.
  • Atau gunakan SELECT jika string lebih dari 8000 karakter.

SQL Dinamis Berkinerja Buruk Digunakan di SP_EXECUTESQL

SP_EXECUTESQL bisa lambat jika Anda menetapkan kueri yang berjalan lambat untuk itu. Periode. Ini belum melibatkan masalah dengan parameter sniffing.

Jadi, mulailah statis dengan kode yang ingin Anda jalankan secara dinamis. Kemudian, periksa Rencana Eksekusi dan STATISTIK IO. Lihat apakah ada indeks yang hilang yang perlu Anda buat. Setel lebih awal.

Intinya dalam SP_EXECUTESQL

Menggunakan SP_EXECUTESQL seperti menggunakan senjata yang ampuh. Tetapi orang yang menggunakannya harus terampil dalam hal itu. Padahal ini juga bukan ilmu roket. Jika Anda seorang pemula hari ini, itu bisa menjadi hal yang wajar seiring waktu dengan latihan.

Daftar gotcha ini tidak lengkap. Tapi itu mencakup yang umum. Jika Anda memerlukan informasi lebih lanjut, lihat tautan berikut:

  • Kutukan dan Berkah SQL Dinamis, oleh Erland Sommarskog
  • SP_EXECUTESQL (Transact-SQL), oleh Microsoft

Seperti ini? Kemudian silakan bagikan di platform media sosial favorit Anda. Anda juga dapat berbagi dengan kami kiat teruji waktu Anda di bagian Komentar.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Bagaimana cara melakukan IF...THEN dalam SQL SELECT?

  2. Bagaimana Fungsi STR() Bekerja di SQL Server (T-SQL)

  3. Bagaimana cara memasukkan catatan dengan hanya nilai default?

  4. Cara Menemukan Hari Terakhir Bulan Ini di SQL Server

  5. OBJECTPROPERTY() vs OBJECTPROPERTYEX() di SQL Server:Apa Perbedaannya?