Database
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> Database

Bạn có mắc phải những sai lầm này khi sử dụng SQL CURSOR không?

Đối với một số người, đó là câu hỏi sai. CURSOR SQL IS Lỗi lầm. Ma quỷ là trong các chi tiết! Bạn có thể đọc tất cả các loại báng bổ trong toàn bộ thế giới blog SQL dưới tên của SQL CURSOR.

Nếu bạn cũng cảm thấy như vậy, điều gì đã khiến bạn đi đến kết luận này?

Nếu đó là từ một người bạn và đồng nghiệp đáng tin cậy, tôi không thể trách bạn. Nó xảy ra. Đôi khi rất nhiều. Nhưng nếu ai đó thuyết phục bạn bằng bằng chứng thì đó lại là một câu chuyện khác.

Chúng tôi chưa từng gặp nhau trước đây. Bạn không biết tôi như một người bạn. Nhưng tôi hy vọng rằng tôi có thể giải thích nó bằng các ví dụ và thuyết phục bạn rằng SQL CURSOR có vị trí của nó. Nó không nhiều, nhưng chỗ nhỏ đó trong mã của chúng tôi có các quy tắc.

Nhưng trước tiên, hãy để tôi kể cho bạn nghe câu chuyện của tôi.

Tôi bắt đầu lập trình với cơ sở dữ liệu bằng xBase. Đó là thời đại học cho đến khi tôi học lập trình chuyên nghiệp hai năm đầu tiên. Tôi đang nói với bạn điều này bởi vì trước đây, chúng ta thường xử lý dữ liệu theo tuần tự, không phải theo lô định sẵn như SQL. Khi tôi học SQL, nó giống như một sự thay đổi mô hình. Công cụ cơ sở dữ liệu quyết định cho tôi với các lệnh dựa trên tập hợp của nó mà tôi đã phát hành. Khi tôi tìm hiểu về SQL CURSOR, tôi có cảm giác như mình đã quay lại với cách cũ nhưng thoải mái.

Nhưng một số đồng nghiệp cấp cao đã cảnh báo tôi, "Hãy tránh SQL CURSOR bằng mọi giá!" Tôi có một vài lời giải thích bằng lời nói, và chỉ có thế.

SQL CURSOR có thể không tốt nếu bạn sử dụng nó không đúng cách. Giống như dùng búa để cắt gỗ, điều đó thật nực cười. Tất nhiên, sai lầm có thể xảy ra và đó là nơi chúng ta sẽ tập trung vào.

1. Sử dụng SQL CURSOR khi đặt lệnh dựa trên sẽ thực hiện

Tôi không thể nhấn mạnh điều này đủ, nhưng ĐÂY là trung tâm của vấn đề. Khi tôi lần đầu tiên biết SQL CURSOR là gì, một bóng đèn sáng lên. “Vòng lặp! Tôi biết điều đó!" Tuy nhiên, không phải cho đến khi nó khiến tôi đau đầu và các tiền bối đã mắng mỏ tôi.

Bạn thấy đấy, cách tiếp cận của SQL là dựa trên bộ. Bạn đưa ra một lệnh INSERT từ các giá trị bảng và nó sẽ thực hiện công việc mà không có vòng lặp trên mã của bạn. Như tôi đã nói trước đó, đó là công việc của công cụ cơ sở dữ liệu. Vì vậy, nếu bạn buộc một vòng lặp thêm bản ghi vào bảng, bạn đang bỏ qua quyền đó. Nó sẽ trở nên xấu xí.

Trước khi thử một ví dụ vô lý, hãy chuẩn bị dữ liệu:


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%'

Câu lệnh đầu tiên sẽ tạo ra 500 bản ghi dữ liệu. Cái thứ hai sẽ nhận được một tập hợp con của nó. Sau đó, chúng tôi đã sẵn sàng. Chúng tôi sẽ chèn dữ liệu bị thiếu từ TestTable vào TestTable2 sử dụng SQL CURSOR. Xem bên dưới:


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

Đó là cách lặp bằng cách sử dụng SQL CURSOR để chèn từng bản ghi bị thiếu. Khá dài phải không?

Bây giờ, hãy thử một cách tốt hơn - phương pháp thay thế dựa trên bộ. Đây là:


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)

Đó là ngắn gọn, gọn gàng và nhanh chóng. Nhanh như thế nào? Xem Hình 1 bên dưới:

Sử dụng xEvent Profiler trong SQL Server Management Studio, tôi đã so sánh các số liệu thời gian CPU, thời lượng và các lần đọc logic. Như bạn có thể thấy trong Hình 1, việc sử dụng lệnh set-based để CHÈN các bản ghi sẽ thắng trong bài kiểm tra hiệu suất. Những con số nói cho mình. Sử dụng SQL CURSOR tiêu tốn nhiều tài nguyên và thời gian xử lý hơn.

Do đó, trước khi bạn sử dụng SQL CURSOR, hãy thử viết một lệnh dựa trên tập hợp trước. Nó sẽ mang lại hiệu quả tốt hơn về lâu dài.

Nhưng nếu bạn cần SQL CURSOR để hoàn thành công việc thì sao?

2. Không sử dụng các tùy chọn SQL CURSOR thích hợp

Một sai lầm khác thậm chí tôi đã mắc phải trong quá khứ là không sử dụng các tùy chọn thích hợp trong DECLARE CURSOR. Có các tùy chọn cho phạm vi, mô hình, đồng thời và nếu có thể cuộn hoặc không. Những đối số này là tùy chọn và rất dễ dàng bỏ qua chúng. Tuy nhiên, nếu SQL CURSOR là cách duy nhất để thực hiện nhiệm vụ, bạn cần phải rõ ràng với ý định của mình.

Vì vậy, hãy tự hỏi bản thân:

  • Khi đi qua vòng lặp, bạn sẽ chỉ điều hướng các hàng về phía trước hay di chuyển đến hàng đầu tiên, cuối cùng, trước đó hoặc tiếp theo? Bạn cần xác định xem CURSOR là chỉ chuyển tiếp hay có thể cuộn. Đó là DECLARE CURSOR FORWARD_ONLY hoặc DECLARE CURSOR SCROLL .
  • Bạn có định cập nhật các cột trong CURSOR không? Sử dụng READ_ONLY nếu không thể cập nhật được.
  • Bạn có cần các giá trị mới nhất khi duyệt qua vòng lặp không? Sử dụng STATIC nếu các giá trị không quan trọng dù mới nhất hay không. Sử dụng DYNAMIC nếu các giao dịch khác cập nhật các cột hoặc xóa các hàng bạn sử dụng trong CURSOR và bạn cần các giá trị mới nhất. Lưu ý :DYNAMIC sẽ đắt.
  • CURSOR là toàn cầu cho kết nối hoặc cục bộ cho lô hoặc một thủ tục được lưu trữ? Chỉ định xem ĐỊA PHƯƠNG hay TOÀN CẦU.

Để biết thêm thông tin về các đối số này, hãy tra cứu tài liệu tham khảo từ Microsoft Docs.

Ví dụ

Hãy thử một ví dụ so sánh ba CURSOR cho thời gian CPU, số lần đọc logic và thời lượng sử dụng xEvents Profiler. Đầu tiên sẽ không có tùy chọn thích hợp sau khi DECLARE CURSOR. Thứ hai là THỐNG KÊ ĐỊA PHƯƠNG FORWARD_ONLY READ_ONLY. Cuối cùng là LOtyuiCAL FAST_FORWARD.

Đây là điều đầu tiên:

-- 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

Tất nhiên, có một lựa chọn tốt hơn đoạn mã trên. Nếu mục đích chỉ là tạo một tập lệnh từ các bảng người dùng hiện có, thì SELECT sẽ làm được. Sau đó, dán đầu ra vào một cửa sổ truy vấn khác.

Nhưng nếu bạn cần tạo một tập lệnh và chạy nó ngay lập tức, đó là một câu chuyện khác. Bạn phải đánh giá tập lệnh đầu ra xem nó có đánh thuế máy chủ của bạn hay không. Xem Sai lầm # 4 sau.

Để cho bạn thấy sự so sánh của ba CURSOR với các tùy chọn khác nhau, điều này sẽ làm được.

Bây giờ, hãy có một mã tương tự nhưng với 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

Như bạn có thể thấy ở trên, sự khác biệt duy nhất so với mã trước đó là ĐỊA CHỈ TƯƠNG ĐƯƠNG_ONLY READ_ONLY đối số.

Thứ ba sẽ có ĐỊA ĐIỂM FAST_FORWARD. Bây giờ, theo Microsoft, FAST_FORWARD là một CURSOR FORWARD_ONLY, READ_ONLY với tính năng tối ưu hóa được bật. Chúng tôi sẽ xem điều này sẽ diễn ra như thế nào với hai phần đầu tiên.

Làm thế nào để họ so sánh? Xem Hình 2:

Cái tốn ít thời gian và thời lượng CPU hơn là LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Cũng lưu ý rằng SQL Server có các giá trị mặc định nếu bạn không chỉ định các đối số như STATIC hoặc READ_ONLY. Có một hậu quả khủng khiếp đối với điều đó như bạn sẽ thấy trong phần tiếp theo.

Những gì sp_describe_cursor đã tiết lộ

sp_describe_cursor là một thủ tục được lưu trữ từ cái chính cơ sở dữ liệu mà bạn có thể sử dụng để lấy thông tin từ CURSOR đang mở. Và đây là những gì nó tiết lộ từ loạt truy vấn đầu tiên không có tùy chọn CURSOR. Xem Hình 3 để biết kết quả của sp_describe_cursor :

Quá mức cần thiết? Bạn đặt cược. CURSOR từ loạt truy vấn đầu tiên là:

  • chung cho kết nối hiện có.
  • động, có nghĩa là nó theo dõi các thay đổi trong bảng # lệnh để cập nhật, xóa và chèn.
  • lạc quan, có nghĩa là SQL Server đã thêm một cột bổ sung vào bảng tạm thời được gọi là CWT. Đây là cột tổng kiểm tra để theo dõi các thay đổi trong giá trị của bảng # lệnh.
  • có thể cuộn, nghĩa là bạn có thể lướt qua hàng trước, hàng tiếp theo, hàng trên cùng hoặc hàng dưới cùng trong con trỏ.

Phi lý? Tôi hoàn toàn đồng ý. Tại sao bạn cần kết nối toàn cầu? Tại sao bạn cần theo dõi các thay đổi đối với bảng tạm thời #commands? Chúng tôi đã cuộn bất kỳ nơi nào khác ngoài bản ghi tiếp theo trong CURSOR?

Vì Máy chủ SQL xác định điều này cho chúng tôi, vòng lặp CURSOR trở thành một sai lầm khủng khiếp.

Bây giờ bạn đã nhận ra tại sao việc chỉ định rõ ràng các tùy chọn SQL CURSOR lại rất quan trọng. Vì vậy, từ bây giờ, hãy luôn chỉ định các đối số CURSOR này nếu bạn cần sử dụng CURSOR.

Kế hoạch Thực thi Tiết lộ thêm

Kế hoạch thực thi thực tế có điều gì đó để nói thêm về những gì sẽ xảy ra mỗi khi lệnh FETCH NEXT FROM command_builder INTO @command được thực thi. Trong Hình 4, một hàng được chèn vào Chỉ mục được phân cụm CWT_PrimaryKey trong tempdb bảng CWT :

Viết xảy ra với tempdb vào mỗi FETCH NEXT. Ngoài ra, còn nhiều hơn thế nữa. Hãy nhớ rằng CURSOR là TỐI ƯU trong Hình 3? Các thuộc tính của Quét chỉ mục theo cụm ở phần ngoài cùng bên phải của kế hoạch tiết lộ cột bổ sung không xác định được gọi là Chk1002 :

Đây có thể là cột Checksum? Kế hoạch XML xác nhận rằng đây thực sự là trường hợp:

Bây giờ, hãy so sánh Kế hoạch thực thi thực tế của FETCH NEXT khi CURSOR là LOCAL STATIC FORWARD_ONLY READ_ONLY:

Nó sử dụng tempdb quá, nhưng nó đơn giản hơn nhiều. Trong khi đó, Hình 8 cho thấy Kế hoạch thực thi khi LOCAL FAST_FORWARD được sử dụng:

Bài học rút ra

Một trong những cách sử dụng thích hợp của SQL CURSOR là tạo các tập lệnh hoặc chạy một số lệnh quản trị hướng tới một nhóm các đối tượng cơ sở dữ liệu. Ngay cả khi có những lần sử dụng nhỏ, tùy chọn đầu tiên của bạn là sử dụng LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR hoặc LOCAL FAST_FORWARD. Người có kế hoạch tốt hơn và đọc hợp lý sẽ giành chiến thắng.

Sau đó, thay thế bất kỳ cái nào trong số này bằng cái thích hợp khi cần thiết. Nhưng bạn biết không? Theo kinh nghiệm cá nhân của tôi, tôi chỉ sử dụng CURSOR chỉ đọc cục bộ với tính năng chỉ chuyển tiếp. Tôi không bao giờ cần đặt CURSOR toàn cầu và có thể cập nhật được.

Ngoài việc sử dụng các đối số này, thời gian thực hiện cũng quan trọng.

3. Sử dụng SQL CURSOR trên các giao dịch hàng ngày

Tôi không phải là quản trị viên. Nhưng tôi có ý tưởng về một máy chủ bận trông như thế nào từ các công cụ của DBA (hoặc từ việc người dùng hét lên bao nhiêu decibel). Trong hoàn cảnh này, bạn có muốn thêm gánh nặng không?

Nếu bạn đang cố gắng tạo mã của mình bằng CURSOR cho các giao dịch hàng ngày, hãy suy nghĩ lại. CURSOR rất tốt cho việc chạy một lần trên một máy chủ ít bận hơn với bộ dữ liệu nhỏ. Tuy nhiên, vào một ngày bận rộn điển hình, CURSOR có thể:

  • Khóa các hàng, đặc biệt nếu đối số đồng thời SCROLL_LOCKS được chỉ định rõ ràng.
  • Nguyên nhân do sử dụng CPU cao.
  • Sử dụng tempdb rộng rãi.

Hãy tưởng tượng bạn có một vài trong số này chạy đồng thời vào một ngày bình thường.

Chúng ta sắp kết thúc nhưng còn một sai lầm nữa mà chúng ta cần nói đến.

4. Không đánh giá tác động SQL CURSOR mang lại

Bạn biết rằng các tùy chọn CURSOR là tốt. Bạn có nghĩ rằng chỉ định chúng là đủ? Bạn đã thấy kết quả ở trên. Nếu không có công cụ, chúng tôi sẽ không đưa ra kết luận đúng.

Hơn nữa, có mã bên trong CURSOR . Tùy thuộc vào những gì nó làm, nó bổ sung nhiều hơn vào tài nguyên được tiêu thụ. Chúng có thể đã có sẵn cho các quy trình khác. Toàn bộ cơ sở hạ tầng, phần cứng của bạn và cấu hình SQL Server sẽ bổ sung thêm vào câu chuyện.

Còn về khối lượng dữ liệu ? Tôi chỉ sử dụng SQL CURSOR trên vài trăm bản ghi. Nó có thể khác với bạn. Ví dụ đầu tiên chỉ lấy 500 bản ghi vì đó là con số mà tôi sẽ đồng ý chờ đợi. 10.000 hoặc thậm chí 1000 đã không cắt nó. Họ đã thể hiện rất tệ.

Cuối cùng, dù ít hay nhiều, ví dụ, việc kiểm tra các lần đọc logic có thể tạo ra sự khác biệt.

Điều gì sẽ xảy ra nếu bạn không kiểm tra Kế hoạch thực thi, số lần đọc hợp lý hoặc thời gian đã trôi qua? Những điều khủng khiếp nào có thể xảy ra ngoài việc SQL Server bị đóng băng? Chúng ta chỉ có thể tưởng tượng tất cả các loại kịch bản ngày tận thế. Bạn hiểu rõ.

Kết luận

SQL CURSOR hoạt động bằng cách xử lý dữ liệu từng hàng. Nó có vị trí của nó, nhưng nó có thể tồi tệ nếu bạn không cẩn thận. Nó giống như một công cụ hiếm khi ra khỏi hộp công cụ.

Vì vậy, điều đầu tiên, hãy thử giải quyết vấn đề bằng cách sử dụng các lệnh dựa trên bộ. Nó trả lời hầu hết các nhu cầu SQL của chúng ta. Và nếu bạn đã từng sử dụng SQL CURSOR, hãy sử dụng nó với các tùy chọn phù hợp. Ước tính tác động với Kế hoạch thực thi, THỐNG KÊ IO và Trình lập hồ sơ xEvent. Sau đó, chọn thời điểm thích hợp để thực thi.

Tất cả điều này sẽ làm cho việc sử dụng SQL CURSOR của bạn tốt hơn một chút.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Mô hình Mối quan hệ Đảng. Làm thế nào để mô hình hóa các mối quan hệ

  2. Các vấn đề về cấu hình nhật ký giao dịch

  3. ScaleGrid ra mắt Hỗ trợ Google Cloud Platform (GCP) cho Lưu trữ cơ sở dữ liệu được quản lý

  4. KHÓA NGOẠI LỆ SQL

  5. Làm thế nào để bạn làm cho cơ sở dữ liệu của bạn nói được nhiều ngôn ngữ?