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

10 SP_EXECUTESQL Cần tránh để có SQL động tốt hơn

Bạn có biết một công cụ như SQL động có thể mạnh như thế nào không? Sử dụng nó sai cách và bạn có thể cho phép ai đó tiếp quản cơ sở dữ liệu của mình. Thêm vào đó, có thể có quá nhiều phức tạp. Bài viết này nhằm mục đích giới thiệu những cạm bẫy khi sử dụng SP_EXECUTESQL và đưa ra 10 lỗi phổ biến nhất cần tránh.

SP_EXECUTESQL là một trong những cách bạn có thể chạy các lệnh SQL được nhúng trong một chuỗi. Bạn xây dựng chuỗi này một cách động thông qua mã. Đó là lý do tại sao chúng tôi gọi đây là SQL động. Ngoài một loạt các câu lệnh, bạn cũng có thể chuyển vào đó một danh sách các tham số và giá trị. Trên thực tế, các tham số và giá trị này khác với lệnh EXEC. EXEC không chấp nhận các tham số cho SQL động. Tuy nhiên, bạn thực thi SP_EXECUTESQL bằng EXEC!

Đối với người mới làm quen với SQL động, đây là cách bạn gọi điều này.

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

Bạn tạo chuỗi lệnh bao gồm các câu lệnh SQL hợp lệ. Theo tùy chọn, bạn có thể chuyển danh sách các tham số đầu vào hoặc đầu ra và kiểu dữ liệu của chúng. Và cuối cùng, bạn chuyển một danh sách các giá trị được phân tách bằng dấu phẩy. Nếu bạn truyền các tham số, bạn cần phải truyền các giá trị. Sau đó, bạn sẽ thấy cả ví dụ đúng và sai về điều này khi bạn đọc tiếp.

Sử dụng SP_EXECUTESQL khi bạn không cần thiết

Đúng rồi. Nếu bạn không cần nó, đừng sử dụng nó. Nếu điều này trở thành 10 điều răn của SP_EXECUTESQL, thì đây là điều đầu tiên. Đó là bởi vì thủ tục hệ thống này có thể dễ dàng bị lạm dụng. Nhưng làm sao bạn biết?

Trả lời điều này:Có vấn đề gì nếu lệnh trong SQL động của bạn trở thành tĩnh không? Nếu bạn không có gì để nói về điểm này, thì bạn không cần nó. Xem ví dụ.

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

Như bạn có thể thấy, việc sử dụng SP_EXECUTESQL hoàn tất với một chuỗi lệnh, tham số và giá trị. Nhưng nó có cần phải theo cách này không? Chắc chắn là không. Hoàn toàn ổn khi có điều này:

DECLARE @productID INT = 1;

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

Nhưng tôi có thể lúng túng khi bạn xem thêm các ví dụ trong bài viết. Bởi vì tôi sẽ mâu thuẫn với những gì tôi đang tuyên bố ở điểm đầu tiên này. Bạn sẽ thấy các câu lệnh SQL động ngắn tốt hơn tĩnh. Vì vậy, hãy chịu đựng với tôi vì các ví dụ sẽ chỉ chứng minh những điểm được nêu ở đây. Đối với các ví dụ còn lại, hãy giả vờ một lúc rằng bạn đang xem mã dành cho SQL động.

Đối tượng và biến ngoài phạm vi

Chạy một loạt lệnh SQL trong một chuỗi bằng SP_EXECUTESQL giống như tạo một thủ tục được lưu trữ không tên và chạy nó. Biết được điều này, các đối tượng như bảng tạm thời và các biến bên ngoài chuỗi lệnh sẽ nằm ngoài phạm vi. Do đó, lỗi thời gian chạy sẽ xảy ra.

Khi sử dụng các biến SQL

Kiểm tra cái này.

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

Biến @ extraText là vô hình đối với các lệnh được thực thi. Thay vì điều này, một chuỗi ký tự hoặc biến được khai báo bên trong chuỗi SQL động sẽ tốt hơn nhiều. Dù sao, kết quả là:

Bạn có thấy lỗi đó trong Hình 1 không? Nếu bạn cần chuyển một giá trị bên trong chuỗi SQL động, hãy thêm một tham số khác.

Khi sử dụng các bảng tạm thời

Các bảng tạm thời trong SQL Server cũng tồn tại trong phạm vi của một mô-đun. Vậy, bạn nghĩ gì về mã này?

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

Đoạn mã trên đang thực hiện liên tiếp 2 thủ tục được lưu trữ. Bảng tạm thời được tạo từ SQL động đầu tiên sẽ không thể truy cập được vào bảng thứ hai. Kết quả là bạn sẽ nhận được Tên đối tượng không hợp lệ #TempNames lỗi.

Lỗi SQL Server QUOTENAME

Dưới đây là một ví dụ khác thoạt nhìn có vẻ ổn nhưng sẽ gây ra lỗi Tên đối tượng không hợp lệ. Xem mã bên dưới.

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

Để hợp lệ cho SELECT, hãy đặt lược đồ và bảng bằng dấu ngoặc vuông như sau: [Schema]. [Table] . Hoặc không bao gồm chúng (Trừ khi tên bảng bao gồm một hoặc nhiều khoảng trắng). Trong ví dụ trên, không có dấu ngoặc vuông nào được sử dụng cho cả bảng và lược đồ. Thay vì sử dụng @Table như một tham số, nó đã trở thành một trình giữ chỗ cho REPLACE. QUOTENAME đã được sử dụng.

QUOTENAME bao quanh một chuỗi có dấu phân cách. Điều này cũng tốt cho việc xử lý các dấu ngoặc kép trong chuỗi SQL động. Dấu ngoặc vuông là dấu phân cách mặc định. Vì vậy, trong ví dụ trên, bạn nghĩ QUOTENAME đã làm gì? Kiểm tra Hình 2 bên dưới.

Câu lệnh SQL PRINT đã giúp chúng tôi gỡ lỗi sự cố bằng cách in chuỗi SQL động. Bây giờ chúng ta biết vấn đề. Cách tốt nhất để khắc phục điều này là gì? Một giải pháp là đoạn mã dưới đây:

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

Hãy giải thích điều này.

Đầu tiên, @Table được sử dụng làm trình giữ chỗ cho REPLACE. Nó sẽ tìm kiếm sự xuất hiện của @Table và thay thế nó bằng các giá trị chính xác. Tại sao? Nếu điều này được sử dụng như một tham số, một lỗi sẽ xảy ra. Đó cũng là lý do sử dụng REPLACE.

Sau đó, chúng tôi sử dụng PARSENAME. Chuỗi mà chúng tôi đã chuyển cho hàm này sẽ tách nó thành lược đồ và bảng. PARSENAME (@ tableName, 2) sẽ nhận được lược đồ. Trong khi PARSENAME (@ tableName, 1) sẽ nhận được bảng.

Cuối cùng, QUOTENAME sẽ bao gồm các tên bảng và lược đồ riêng biệt sau khi PARSENAME được thực hiện xong. Kết quả là [Person]. [Person] . Bây giờ, nó hợp lệ.

Tuy nhiên, một cách tốt hơn để làm sạch chuỗi SQL động bằng tên đối tượng sẽ được hiển thị sau.

Thực thi SP_EXECUTESQL với câu lệnh NULL

Có những ngày bạn buồn bã hoặc thất vọng. Sai lầm có thể được thực hiện trên đường đi. Sau đó, thêm một chuỗi SQL động dài vào hỗn hợp và NULL. Và kết quả?

Không có gì.

SP_EXECUTESQL sẽ cho bạn một kết quả trống. Thế nào? Bằng cách nối một NULL do nhầm lẫn. Hãy xem xét ví dụ dưới đây:

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

Lúc đầu, mã có vẻ tốt. Tuy nhiên, đôi mắt đại bàng trong chúng ta sẽ nhận thấy @crlf Biến đổi. Giá trị của nó? Biến không được khởi tạo. Vì vậy, nó là KHÔNG.

Nhưng điểm của biến đó là gì? Trong phần sau, bạn sẽ biết tầm quan trọng của việc định dạng và gỡ lỗi. Bây giờ, hãy tập trung vào vấn đề trước mắt.

Đầu tiên, nối một biến NULL với chuỗi SQL động sẽ dẫn đến NULL. Sau đó, PRINT sẽ in trống. Cuối cùng, SP_EXECUTESQL sẽ chạy tốt với chuỗi SQL động NULL. Nhưng nó không trả lại gì.

NULL có thể mê hoặc chúng ta trong một ngày vốn đã tồi tệ. Hãy nghỉ ngơi một chút. Thư giãn. Sau đó, hãy trở lại với đầu óc tỉnh táo hơn.

Giá trị tham số nội tuyến

Lộn xộn.

Đó là cách các giá trị nội tuyến đối với chuỗi SQL động sẽ trông như thế nào. Sẽ có rất nhiều dấu ngoặc kép cho chuỗi và ngày tháng. Nếu không cẩn thận, O’Briens và O’Neils cũng sẽ gây ra lỗi. Và vì SQL động là một chuỗi, bạn phải CHUYỂN ĐỔI hoặc ĐÚC các giá trị thành chuỗi. Đây là một ví dụ.

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;

Tôi thấy các chuỗi động lộn xộn hơn thế này. Lưu ý các dấu nháy đơn, CHUYỂN ĐỔI và ĐÚC. Nếu các tham số được sử dụng, điều này có thể trông đẹp hơn. Bạn có thể xem nó bên dưới

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

Xem? Ít dấu ngoặc kép hơn, không có CHUYỂN ĐỔI và ĐÚC, đồng thời cũng rõ ràng hơn.

Nhưng có một tác dụng phụ nguy hiểm hơn của các giá trị nội dòng.

SQL Injection

Nếu chúng ta sống trong một thế giới mà tất cả mọi người đều tốt, thì SQL injection sẽ không bao giờ được nghĩ đến. Nhưng đó không phải là trường hợp. Ai đó có thể đưa mã SQL độc hại vào của bạn. Làm thế nào điều này có thể xảy ra?

Đây là tình huống chúng ta sẽ sử dụng trong ví dụ của mình:

  • Các giá trị được hợp nhất với chuỗi SQL động như ví dụ của chúng tôi trước đó. Không có tham số.
  • Chuỗi SQL động có dung lượng lên đến 2GB bằng NVARCHAR (MAX). Rất nhiều không gian để tiêm mã độc.
  • Tồi tệ nhất, chuỗi SQL động được thực thi với quyền cao hơn.
  • Phiên bản SQL Server chấp nhận xác thực SQL.

Điều này có quá nhiều không? Đối với hệ thống do một người quản lý, điều này có thể xảy ra. Không ai kiểm tra anh ta dù sao. Đối với các công ty lớn hơn, đôi khi vẫn tồn tại một bộ phận CNTT lỏng lẻo về bảo mật.

Đây có còn là mối đe dọa ngày nay không? Đó là, theo nhà cung cấp dịch vụ đám mây Akamai trong báo cáo Trạng thái Internet / Bảo mật của họ. Từ tháng 11 năm 2017 đến tháng 3 năm 2019, SQL injection đại diện cho gần 2/3 tổng số các cuộc tấn công ứng dụng web. Đó là mức cao nhất trong số các mối đe dọa đã được kiểm tra. Rất tệ.

Bạn có muốn tận mắt chứng kiến ​​không?

Thực hành chèn SQL: Ví dụ xấu

Hãy thực hiện một số nội dung SQL trong ví dụ này. Bạn có thể thử điều này trong AdventureWorks của riêng bạn cơ sở dữ liệu. Nhưng hãy đảm bảo rằng xác thực SQL được cho phép và bạn chạy nó với các quyền nâng cao.

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

Đoạn mã trên không đại diện cho mã thực tế của một công ty hiện có. Nó thậm chí không thể gọi được bằng một ứng dụng. Nhưng điều này minh họa cho hành động xấu xa. Vậy, chúng ta có gì ở đây?

Đầu tiên, mã được chèn vào sẽ tạo tài khoản SQL trông giống như sa , nhưng không phải vậy. Và như sa , cái này có sysadmin quyền. Cuối cùng, điều này sẽ được sử dụng để truy cập cơ sở dữ liệu bất kỳ lúc nào với đầy đủ các đặc quyền. Bất cứ điều gì có thể xảy ra sau khi điều này được thực hiện:ăn cắp, xóa, giả mạo dữ liệu của công ty, bạn đặt tên cho nó.

Mã này sẽ chạy? Chắc chắn! Và một khi đúng như vậy, siêu tài khoản sẽ được tạo ra một cách âm thầm. Và tất nhiên, địa chỉ của Zheng Mu sẽ xuất hiện trong tập kết quả. Mọi thứ khác vẫn bình thường. Bạn có nghĩ là mờ ám không?

Sau khi bạn chạy đoạn mã trên, hãy thử chạy đoạn mã này nữa:

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

Nếu nó trả về 1, anh ấy tham gia, bất cứ ai đây Là. Ngoài ra, bạn có thể kiểm tra nó trong Nhật ký bảo mật của máy chủ SQL trong SQL Server Management Studio.

Vậy, bạn đã nhận được gì?

Đáng sợ phải không? (Nếu điều này là thật, thì đúng là như vậy.)

Thực hành SQL Injection:Ví dụ hay

Bây giờ, hãy thay đổi mã một chút bằng cách sử dụng các tham số. Các điều kiện khác vẫn như cũ.

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

Có 2 điểm khác biệt trong kết quả so với ví dụ đầu tiên.

  • Đầu tiên, địa chỉ của Zheng Mu sẽ không xuất hiện. Tập kết quả trống.
  • Sau đó, tài khoản gia hạn không được tạo. Sử dụng IS_SRVROLEMEMBER sẽ trả về NULL.

Điều gì đã xảy ra?

Vì các tham số được sử dụng nên giá trị của @firstName ‘Zheng”; TẠO ĐĂNG NHẬP Sà CÓ MẬT KHẨU =”12345”; ALT ’ . Đây được coi là một giá trị theo nghĩa đen và chỉ được cắt ngắn còn 50 ký tự. Kiểm tra tham số tên trong đoạn mã trên. Đó là NVARCHAR (50). Đó là lý do tại sao tập kết quả trống. Không có người nào có tên như vậy trong cơ sở dữ liệu.

Đây chỉ là một ví dụ về SQL injection và một cách để tránh nó. Có nhiều liên quan hơn đến việc làm thực tế. Nhưng tôi hy vọng tôi đã làm rõ lý do tại sao các giá trị nội tuyến trong SQL động lại kém.

Đánh hơi tham số

Bạn đã từng gặp phải tình trạng chạy chậm thủ tục được lưu trữ từ một ứng dụng, nhưng khi bạn thử chạy nó trong SSMS, nó lại trở nên nhanh chóng? Điều đó gây khó hiểu vì bạn đã sử dụng các giá trị thông số chính xác được sử dụng trong ứng dụng.

Đó là đánh giá thông số đang hoạt động. SQL Server tạo một kế hoạch thực thi lần đầu tiên thủ tục được lưu trữ được chạy hoặc biên dịch lại. Sau đó, sử dụng lại kế hoạch cho lần chạy tiếp theo. Điều đó nghe có vẻ tuyệt vời vì SQL Server không cần phải tạo lại kế hoạch mọi lúc. Nhưng đôi khi một giá trị tham số khác cần một kế hoạch khác để chạy nhanh.

Đây là phần trình diễn sử dụng SP_EXECUTESQL và SQL tĩnh đơn giản.

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

thứ này rất đơn giản. Kiểm tra kế hoạch thực hiện trong Hình 3.

Bây giờ, hãy thử cùng một truy vấn bằng cách sử dụng SQL tĩnh.

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

Xem Hình 4, sau đó so sánh với Hình 3.

Trong Hình 3, Tìm kiếm chỉ mục Vòng lặp lồng nhau được sử dụng. Nhưng trong Hình 4, đó là Quét chỉ mục theo cụm . Mặc dù không có hình phạt hiệu suất rõ ràng tại thời điểm này, nhưng điều này cho thấy khả năng đánh giá thông số không chỉ là tưởng tượng.

Điều này có thể rất khó chịu khi truy vấn trở nên chậm. Bạn có thể phải sử dụng các kỹ thuật như biên dịch lại hoặc sử dụng gợi ý truy vấn để tránh nó. Bất kỳ điều nào trong số này đều có nhược điểm.

Chuỗi SQL động chưa được định dạng trong SP_EXECUTESQL

Điều gì có thể xảy ra với mã này?

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

Nó ngắn gọn và đơn giản. Nhưng hãy xem Hình 5 bên dưới.

Lỗi xảy ra nếu bạn không để ý đến một khoảng trắng giữa từ khóa và đối tượng khi tạo chuỗi SQL động. Giống như trong Hình 5, trong đó ProductCount bí danh cột và từ khóa FROM không có khoảng trắng ở giữa. Nó trở nên khó hiểu khi một phần của chuỗi chảy xuống dòng mã tiếp theo. Nó khiến bạn nghĩ rằng cú pháp là đúng.

Cũng lưu ý rằng chuỗi được sử dụng 2 dòng trong cửa sổ mã, nhưng đầu ra của PRINT hiển thị 1 dòng. Hãy tưởng tượng nếu đây là một chuỗi lệnh rất, rất dài. Thật khó để tìm ra sự cố cho đến khi bạn định dạng đúng chuỗi từ tab Tin nhắn.

Để giải quyết vấn đề này, hãy thêm ký tự xuống dòng và nguồn cấp dữ liệu dòng. Bạn có thể nhận thấy một biến @crlf từ các ví dụ trước. Định dạng chuỗi SQL động của bạn với khoảng trắng và một dòng mới sẽ làm cho chuỗi SQL động dễ đọc hơn. Điều này cũng rất tốt cho việc gỡ lỗi.

Hãy xem xét một câu lệnh SELECT với JOIN. Nó cần một vài dòng mã như ví dụ bên dưới.

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

Để định dạng chuỗi, hãy chọn @crlf biến được đặt thành NCHAR (13), một ký tự xuống dòng và NCHAR (10), một nguồn cấp dữ liệu dòng. Nó được nối với mỗi dòng để ngắt một chuỗi dài của câu lệnh SELECT. Để xem kết quả trong tab Tin nhắn, chúng tôi sử dụng PRINT. Kiểm tra kết quả đầu ra trong Hình 6 bên dưới.

Cách bạn tạo chuỗi SQL động là tùy thuộc vào bạn. Bất cứ điều gì phù hợp với bạn để làm cho nó rõ ràng, dễ đọc và dễ gỡ lỗi khi đến thời điểm.

Tên đối tượng không được vệ sinh

Bạn có cần đặt động tên bảng, dạng xem hoặc cơ sở dữ liệu vì bất kỳ lý do gì không? Sau đó, bạn cần phải "làm sạch" hoặc xác thực các tên đối tượng này để tránh lỗi.

Trong ví dụ của chúng tôi, chúng tôi sẽ sử dụng tên bảng, mặc dù nguyên tắc xác nhận cũng có thể áp dụng cho các khung nhìn. Cách bạn giải quyết vấn đề tiếp theo sẽ khác.

Trước đó, chúng tôi sử dụng PARSENAME để tách tên lược đồ khỏi tên bảng. Nó cũng có thể được sử dụng nếu chuỗi có tên máy chủ và cơ sở dữ liệu. Nhưng trong ví dụ này, chúng tôi sẽ chỉ sử dụng lược đồ và tên bảng. Tôi để phần còn lại cho tâm trí tuyệt vời của bạn. Điều này sẽ hoạt động bất kể bạn đặt tên bảng của mình có hoặc không có khoảng trắng. Khoảng trắng trên bảng hoặc tên dạng xem đều hợp lệ. Vì vậy, nó hoạt động cho dbo.MyFoodCravings hoặc [dbo]. [My Food Cravings] .

VÍ DỤ

Hãy tạo một bảng.

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

Sau đó, chúng tôi tạo một tập lệnh sẽ sử dụng SP_EXECUTESQL. Thao tác này sẽ chạy câu lệnh DELETE chung cho bất kỳ bảng nào được cung cấp khóa 1 cột. Điều đầu tiên cần làm là phân tích cú pháp tên đối tượng đầy đủ.

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

Bằng cách này, chúng tôi tách lược đồ khỏi bảng. Để xác thực thêm, chúng tôi sử dụng OBJECT_ID. Nếu nó không phải là NULL, thì nó hợp lệ.

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

Cũng lưu ý rằng chúng tôi đã sử dụng QUOTENAME. Điều này sẽ đảm bảo rằng các tên bảng có khoảng trắng sẽ không gây ra lỗi bằng cách đặt chúng bằng dấu ngoặc vuông.

Nhưng làm thế nào về việc xác nhận cột chính? Bạn có thể kiểm tra sự tồn tại của cột của bảng mục tiêu trong 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

Bây giờ, đây là tập lệnh đầy đủ cho những gì chúng tôi muốn đạt được.

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

Kết quả của tập lệnh này là trong Hình 7 bên dưới.

Bạn có thể thử điều này với các bảng khác. Chỉ cần thay đổi @object , @idkey @id giá trị biến.

Không có điều khoản cho việc gỡ lỗi

Sai sót có thể xảy ra. Vì vậy, bạn cần biết chuỗi SQL động được tạo ra để tìm ra nguyên nhân gốc rễ. Chúng tôi không phải là thầy bói hay pháp sư để đoán dạng của chuỗi SQL động. Vì vậy, bạn cần một cờ gỡ lỗi.

Lưu ý trong Hình 7 trước đó rằng chuỗi SQL động được in trong tab Thông báo của SSMS. Chúng tôi đã thêm một @isDebug Biến BIT và đặt nó thành 1 trong mã. Khi giá trị là 1, chuỗi SQL động sẽ in. Điều này rất tốt nếu bạn cần gỡ lỗi một tập lệnh hoặc thủ tục được lưu trữ như thế này. Chỉ cần đặt nó về 0 khi bạn gỡ lỗi xong. Nếu đây là một thủ tục được lưu trữ, hãy đặt cờ này thành một tham số tùy chọn với giá trị mặc định bằng không.

Để xem chuỗi SQL động, bạn có thể sử dụng 2 phương pháp khả thi.

  • Sử dụng PRINT nếu chuỗi nhỏ hơn hoặc bằng 8000 ký tự.
  • Hoặc sử dụng SELECT nếu chuỗi dài hơn 8000 ký tự.

SQL động hoạt động kém được sử dụng trong SP_EXECUTESQL

SP_EXECUTESQL có thể chậm nếu bạn gán một truy vấn chạy chậm cho nó. Giai đoạn =Stage. Điều này không liên quan đến vấn đề với tính năng dò tìm tham số.

Vì vậy, hãy bắt đầu tĩnh với mã bạn muốn chạy động. Sau đó, kiểm tra Kế hoạch thực hiện và THỐNG KÊ IO. Xem có thiếu chỉ mục nào mà bạn cần tạo không. Điều chỉnh sớm.

Tóm tắt trong SP_EXECUTESQL

Sử dụng SP_EXECUTESQL giống như sử dụng một vũ khí mạnh mẽ. Nhưng người sử dụng nó cần phải có kỹ năng. Mặc dù đây cũng không phải là khoa học tên lửa. Nếu bạn là một người mới ngày nay, điều đó có thể trở thành lẽ thường trong thời gian thực hành.

Danh sách gotchas này chưa hoàn thành. Nhưng nó bao hàm những cái chung. Nếu bạn cần thêm thông tin, hãy xem các liên kết sau:

  • Lời nguyền và phước lành của SQL động, của Erland Sommarskog
  • SP_EXECUTESQL (Transact-SQL) của Microsoft

Như thế này? Sau đó, hãy chia sẻ nó trên các nền tảng truyền thông xã hội yêu thích của bạn. Bạn cũng có thể chia sẻ với chúng tôi các mẹo đã được kiểm tra theo thời gian của bạn trong phần Nhận xé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. Tập lệnh để lưu dữ liệu varbinary vào đĩa

  2. Tối ưu hóa TempDB:Tránh tắc nghẽn và các vấn đề về hiệu suất

  3. Có cách nào để lặp qua biến bảng trong TSQL mà không sử dụng con trỏ không?

  4. Chèn tất cả các giá trị của một bảng vào một bảng khác trong SQL

  5. Tác động của sự kiện mở rộng query_post_execution_showplan trong SQL Server 2012