Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Apakah Anda Membuat Kesalahan Ini Saat Menggunakan SQL CURSOR?

Bagi sebagian orang, itu pertanyaan yang salah. KURSOR SQL ADALAH kesalahan. Iblis ada dalam detailnya! Anda dapat membaca segala macam penistaan ​​di seluruh blogosphere SQL atas nama SQL CURSOR.

Jika Anda merasakan hal yang sama, apa yang membuat Anda sampai pada kesimpulan ini?

Jika itu dari teman dan kolega tepercaya, saya tidak bisa menyalahkan Anda. Itu terjadi. Kadang banyak. Tapi jika seseorang meyakinkan Anda dengan bukti, itu lain cerita.

Kami belum pernah bertemu sebelumnya. Anda tidak mengenal saya sebagai teman. Tapi saya harap saya bisa menjelaskannya dengan contoh dan meyakinkan Anda bahwa SQL CURSOR memiliki tempatnya. Tidak banyak, tetapi tempat kecil dalam kode kita itu memiliki aturan.

Tapi pertama-tama, izinkan saya menceritakan kisah saya.

Saya mulai pemrograman dengan database menggunakan xBase. Itu kembali di perguruan tinggi sampai dua tahun pertama saya pemrograman profesional. Saya memberi tahu Anda ini karena dulu, kami biasa memproses data secara berurutan, bukan dalam kumpulan yang ditetapkan seperti SQL. Ketika saya belajar SQL, itu seperti perubahan paradigma. Mesin database memutuskan untuk saya dengan perintah berbasis set yang saya keluarkan. Ketika saya belajar tentang SQL CURSOR, rasanya seperti saya kembali dengan cara lama tapi nyaman.

Tetapi beberapa rekan senior memperingatkan saya, "Hindari SQL CURSOR dengan cara apa pun!" Saya mendapat beberapa penjelasan verbal, dan hanya itu.

SQL CURSOR bisa menjadi buruk jika Anda menggunakannya untuk pekerjaan yang salah. Seperti menggunakan palu untuk memotong kayu, itu konyol. Tentu saja, kesalahan bisa terjadi, dan di situlah fokus kami.

1. Menggunakan SQL CURSOR Saat Perintah Berbasis Set Akan Dilakukan

Saya tidak bisa cukup menekankan ini, tetapi INI adalah inti masalahnya. Ketika saya pertama kali mengetahui apa itu SQL CURSOR, sebuah bola lampu menyala. “Lingkaran! Saya tahu itu!" Namun, tidak sampai membuat saya sakit kepala dan senior saya memarahi saya.

Anda lihat, pendekatan SQL berbasis set. Anda mengeluarkan perintah INSERT dari nilai tabel, dan itu akan melakukan pekerjaan tanpa loop pada kode Anda. Seperti yang saya katakan sebelumnya, ini adalah pekerjaan mesin database. Jadi, jika Anda memaksa loop untuk menambahkan catatan ke tabel, Anda melewati otoritas itu. Ini akan menjadi jelek.

Sebelum kita mencoba contoh konyol, mari kita siapkan datanya:


SELECT TOP (500)
  val = ROW_NUMBER() OVER (ORDER BY sod.SalesOrderDetailID)
, modified = GETDATE()
, status = 'inserted'
INTO dbo.TestTable
FROM AdventureWorks.Sales.SalesOrderDetail sod
CROSS JOIN AdventureWorks.Sales.SalesOrderDetail sod2

SELECT
 tt.val
,GETDATE() AS modified
,'inserted' AS status
INTO dbo.TestTable2
FROM dbo.TestTable tt
WHERE CAST(val AS VARCHAR) LIKE '%2%'

Pernyataan pertama akan menghasilkan 500 record data. Yang kedua akan mendapatkan subset dari itu. Kemudian, kami siap. Kami akan memasukkan data yang hilang dari TestTable ke dalam TestTable2 menggunakan KURSOR SQL. Lihat di bawah:


DECLARE @val INT

DECLARE test_inserts CURSOR FOR 
	SELECT val FROM TestTable tt
	WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

OPEN test_inserts
FETCH NEXT FROM test_inserts INTO @val
WHILE @@fetch_status = 0
BEGIN
	INSERT INTO TestTable2
	(val, modified, status)
	VALUES
	(@val, GETDATE(),'inserted')

	FETCH NEXT FROM test_inserts INTO @val
END

CLOSE test_inserts
DEALLOCATE test_inserts

Itulah cara melakukan looping menggunakan SQL CURSOR untuk menyisipkan record yang hilang satu per satu. Cukup panjang bukan?

Sekarang, mari kita coba cara yang lebih baik – alternatif berbasis himpunan. Ini dia:


INSERT INTO TestTable2
(val, modified, status)
SELECT val, GETDATE(), status
FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

Itu singkat, rapi, dan cepat. Seberapa cepat? Lihat Gambar 1 di bawah ini:

Menggunakan xEvent Profiler di SQL Server Management Studio, saya membandingkan angka waktu CPU, durasi, dan pembacaan logis. Seperti yang Anda lihat pada Gambar 1, menggunakan perintah set-based untuk INSERT record memenangkan uji kinerja. Angka-angka berbicara sendiri. Menggunakan SQL CURSOR menghabiskan lebih banyak sumber daya dan waktu pemrosesan.

Oleh karena itu, sebelum Anda menggunakan SQL CURSOR, cobalah untuk menulis perintah berbasis set terlebih dahulu. Ini akan memberikan hasil yang lebih baik dalam jangka panjang.

Tetapi bagaimana jika Anda membutuhkan SQL CURSOR untuk menyelesaikan pekerjaan?

2. Tidak Menggunakan Opsi SQL CURSOR yang Sesuai

Kesalahan lain yang bahkan saya buat di masa lalu adalah tidak menggunakan opsi yang sesuai di DECLARE CURSOR. Ada opsi untuk ruang lingkup, model, konkurensi, dan apakah dapat digulir atau tidak. Argumen ini opsional, dan mudah untuk mengabaikannya. Namun, jika SQL CURSOR adalah satu-satunya cara untuk melakukan tugas tersebut, Anda harus eksplisit dengan niat Anda.

Jadi, tanyakan pada diri Anda:

  • Saat melintasi loop, apakah Anda akan menavigasi baris ke depan saja, atau pindah ke baris pertama, terakhir, sebelumnya, atau berikutnya? Anda perlu menentukan apakah CURSOR hanya untuk diteruskan atau dapat digulir. Itu MENNYATAKAN CURSOR FORWARD_ONLY atau MENYATAKAN GULIR KURSOR .
  • Apakah Anda akan memperbarui kolom di CURSOR? Gunakan READ_ONLY jika tidak dapat diperbarui.
  • Apakah Anda memerlukan nilai terbaru saat melintasi loop? Gunakan STATIC jika nilainya tidak masalah apakah terbaru atau tidak. Gunakan DINAMIS jika transaksi lain memperbarui kolom atau menghapus baris yang Anda gunakan di CURSOR, dan Anda memerlukan nilai terbaru. Catatan :DINAMIS akan mahal.
  • Apakah CURSOR global untuk koneksi atau lokal untuk batch atau prosedur tersimpan? Tentukan apakah LOKAL atau GLOBAL.

Untuk informasi selengkapnya tentang argumen ini, lihat referensi dari Microsoft Docs.

Contoh

Mari kita coba contoh membandingkan tiga CURSOR untuk waktu CPU, pembacaan logis, dan durasi menggunakan xEvents Profiler. Yang pertama tidak akan memiliki opsi yang sesuai setelah DECLARE CURSOR. Yang kedua adalah LOCAL STATIC FORWARD_ONLY READ_ONLY. Yang terakhir adalah LOtyuiCAL FAST_FORWARD.

Ini yang pertama:

-- NOTE: Don't just COPY and PASTE this code then run in your machine. Read and assess.

-- DECLARE CURSOR with no options
SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR FOR 
  SELECT
	Command
  FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

Ada opsi yang lebih baik daripada kode di atas, tentu saja. Jika tujuannya hanya untuk menghasilkan skrip dari tabel pengguna yang ada, SELECT akan melakukannya. Kemudian, tempelkan output ke jendela kueri lain.

Tetapi jika Anda perlu membuat skrip dan menjalankannya sekaligus, itu cerita yang berbeda. Anda harus mengevaluasi skrip keluaran apakah itu akan membebani server Anda atau tidak. Lihat Kesalahan #4 nanti.

Untuk menunjukkan perbandingan tiga KURSOR dengan opsi berbeda, ini akan berhasil.

Sekarang, mari kita buat kode yang mirip tetapi dengan LOCAL STATIC FORWARD_ONLY READ_ONLY.

--- STATIC LOCAL FORWARD_ONLY READ_ONLY

SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR SELECT
	Command
FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

Seperti yang Anda lihat di atas, satu-satunya perbedaan dari kode sebelumnya adalah LOCAL STATIC FORWARD_ONLY READ_ONLY argumen.

Yang ketiga akan memiliki FAST_FORWARD LOKAL. Sekarang, menurut Microsoft, FAST_FORWARD adalah KURSOR FORWARD_ONLY, READ_ONLY dengan pengoptimalan yang diaktifkan. Kita akan melihat bagaimana ini akan berjalan dengan dua yang pertama.

Bagaimana mereka membandingkan? Lihat Gambar 2:

Salah satu yang membutuhkan waktu dan durasi CPU lebih sedikit adalah LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Perhatikan juga bahwa SQL Server memiliki default jika Anda tidak menentukan argumen seperti STATIC atau READ_ONLY. Ada konsekuensi yang mengerikan seperti yang akan Anda lihat di bagian selanjutnya.

Apa yang diungkapkan sp_describe_cursor

sp_describe_cursor adalah prosedur tersimpan dari master database yang dapat Anda gunakan untuk mendapatkan informasi dari CURSOR yang terbuka. Dan inilah yang terungkap dari kumpulan kueri pertama tanpa opsi KURSOR. Lihat Gambar 3 untuk hasil sp_describe_cursor :

Membunuh banyak? Anda bertaruh. KURSOR dari kumpulan kueri pertama adalah:

  • global ke koneksi yang ada.
  • dinamis, yang berarti melacak perubahan dalam tabel #commands untuk pembaruan, penghapusan, dan penyisipan.
  • optimis, yang berarti bahwa SQL Server menambahkan kolom tambahan ke tabel sementara yang disebut CWT. Ini adalah kolom checksum untuk melacak perubahan nilai tabel #commands.
  • dapat digulir, artinya Anda dapat menelusuri ke baris sebelumnya, berikutnya, atas, atau bawah di kursor.

Absurd? Saya sangat setuju. Mengapa Anda membutuhkan koneksi global? Mengapa Anda perlu melacak perubahan pada tabel sementara #commands? Apakah kami menggulir ke mana pun selain catatan berikutnya di CURSOR?

Saat SQL Server menentukan ini untuk kita, loop CURSOR menjadi kesalahan besar.

Sekarang Anda menyadari mengapa secara eksplisit menentukan opsi SQL CURSOR sangat penting. Jadi, mulai sekarang, selalu tentukan argumen CURSOR ini jika Anda perlu menggunakan CURSOR.

Rencana Eksekusi Mengungkap Lebih Banyak

Rencana Eksekusi Aktual memiliki sesuatu yang lebih untuk dikatakan tentang apa yang terjadi setiap kali FETCH NEXT FROM command_builder INTO @command dieksekusi. Pada Gambar 4, sebuah baris dimasukkan ke dalam Clustered Index CWT_PrimaryKey di tempdb tabel CWT :

Penulisan terjadi pada tempdb pada setiap FETCH NEXT. Selain itu, masih ada lagi. Ingat KURSOR OPTIMIS pada Gambar 3? Properti dari Clustered Index Scan di bagian paling kanan dari rencana mengungkapkan kolom ekstra tidak dikenal yang disebut Chk1002 :

Mungkinkah ini kolom Checksum? XML Plan menegaskan bahwa memang demikian:

Sekarang, bandingkan Rencana Eksekusi Aktual dari FETCH NEXT ketika CURSOR adalah LOCAL STATIC FORWARD_ONLY READ_ONLY:

Ini menggunakan tempdb juga, tapi ini jauh lebih sederhana. Sementara itu, Gambar 8 menunjukkan Execution Plan saat LOCAL FAST_FORWARD digunakan:

Bawa Pulang

Salah satu kegunaan SQL CURSOR yang tepat adalah menghasilkan skrip atau menjalankan beberapa perintah administratif terhadap sekelompok objek database. Bahkan jika ada sedikit kegunaannya, pilihan pertama Anda adalah menggunakan LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR atau LOCAL FAST_FORWARD. Yang memiliki rencana yang lebih baik dan pembacaan yang logis akan menang.

Kemudian, ganti salah satu dari ini dengan yang sesuai sesuai kebutuhan. Tapi Anda tahu apa? Dalam pengalaman pribadi saya, saya hanya menggunakan CURSOR read-only lokal dengan traversal forward-only. Saya tidak pernah perlu membuat CURSOR global dan dapat diperbarui.

Selain menggunakan argumen ini, waktu eksekusi juga penting.

3. Menggunakan SQL CURSOR pada Transaksi Harian

Saya bukan administrator. Tetapi saya memiliki gambaran tentang seperti apa server yang sibuk dari alat DBA (atau dari berapa banyak desibel yang diteriakkan pengguna). Dalam keadaan seperti ini, apakah Anda ingin menambah beban?

Jika Anda mencoba membuat kode Anda dengan CURSOR untuk transaksi sehari-hari, pikirkan lagi. KURSOR baik-baik saja untuk dijalankan satu kali pada server yang tidak terlalu sibuk dengan kumpulan data kecil. Namun, pada hari sibuk biasa, CURSOR dapat:

  • Kunci baris, terutama jika argumen konkurensi SCROLL_LOCKS ditentukan secara eksplisit.
  • Menyebabkan penggunaan CPU yang tinggi.
  • Gunakan tempdb secara ekstensif.

Bayangkan Anda menjalankan beberapa dari ini secara bersamaan pada hari-hari biasa.

Kita akan segera berakhir tapi ada satu kesalahan lagi yang perlu kita bicarakan.

4. Tidak Menilai Dampak yang Dibawa SQL CURSOR

Anda tahu bahwa opsi CURSOR bagus. Apakah Anda pikir menentukan mereka sudah cukup? Anda sudah melihat hasil di atas. Tanpa alat, kami tidak akan sampai pada kesimpulan yang tepat.

Selanjutnya, ada kode di dalam CURSOR . Bergantung pada apa yang dilakukannya, itu menambah lebih banyak sumber daya yang dikonsumsi. Ini mungkin telah tersedia untuk proses lain. Seluruh infrastruktur Anda, perangkat keras Anda, dan konfigurasi SQL Server akan menambah lebih banyak cerita.

Bagaimana dengan volume data ? Saya hanya menggunakan SQL CURSOR pada beberapa ratus record. Ini bisa berbeda untuk Anda. Contoh pertama hanya mengambil 500 catatan karena itu adalah nomor yang saya setuju untuk menunggu. 10.000 atau bahkan 1000 tidak memotongnya. Performa mereka buruk.

Akhirnya, tidak peduli seberapa kurang atau lebih, memeriksa pembacaan logis, misalnya, dapat membuat perbedaan.

Bagaimana jika Anda tidak memeriksa Rencana Eksekusi, pembacaan logis, atau waktu yang telah berlalu? Hal buruk apa yang bisa terjadi selain pembekuan SQL Server? Kita hanya bisa membayangkan segala macam skenario kiamat. Anda mengerti maksudnya.

Kesimpulan

SQL CURSOR bekerja dengan memproses data baris demi baris. Itu ada tempatnya, tetapi bisa menjadi buruk jika Anda tidak hati-hati. Ini seperti alat yang jarang keluar dari kotak alat.

Jadi, hal pertama, coba selesaikan masalah dengan menggunakan perintah berbasis set. Ini menjawab sebagian besar kebutuhan SQL kami. Dan jika Anda pernah menggunakan SQL CURSOR, gunakan dengan opsi yang tepat. Perkirakan dampaknya dengan rencana Eksekusi, STATISTICS IO, dan xEvent Profiler. Kemudian, pilih waktu yang tepat untuk mengeksekusi.

Semua ini akan membuat penggunaan SQL CURSOR Anda sedikit lebih baik.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Keterampilan dan Pengetahuan Apa yang Dibutuhkan Perancang Basis Data?

  2. APPEND_ONLY_STORAGE_INSERT_POINT Kait

  3. Model Data Organisasi Pernikahan

  4. Pengantar Data Mining

  5. Menjalankan Tugas Pemeliharaan Database SQL Menggunakan SQLCMD