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

Thực thi SQL động trong SQL Server

SQL động là một câu lệnh được xây dựng và thực thi trong thời gian chạy, thường chứa các phần chuỗi SQL được tạo động, tham số đầu vào hoặc cả hai.

Có nhiều phương pháp khác nhau để tạo và chạy các lệnh SQL được tạo động. Bài viết hiện tại sẽ khám phá chúng, xác định khía cạnh tích cực và tiêu cực của chúng, đồng thời chứng minh các phương pháp tiếp cận thực tế để tối ưu hóa các truy vấn trong một số trường hợp thường xuyên.

Chúng tôi sử dụng hai cách để thực thi SQL động: EXEC lệnh và sp_executesql thủ tục được lưu trữ.

Sử dụng lệnh EXEC / EXECUTE

Đối với ví dụ đầu tiên, chúng tôi tạo một câu lệnh SQL động đơn giản từ AdventureWorks cơ sở dữ liệu. Ví dụ có một bộ lọc được chuyển qua biến chuỗi được nối @AddressPart và được thực thi trong lệnh cuối cùng:

USE AdventureWorks2019

-- Declare variable to hold generated SQL statement
DECLARE @SQLExec NVARCHAR(4000) 
DECLARE @AddressPart NVARCHAR(50) = 'a'

-- Build dynamic SQL
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + @AddressPart + '%'''

-- Execute dynamic SQL 
EXEC (@SQLExec)

Lưu ý rằng các truy vấn được xây dựng bằng cách nối chuỗi có thể tạo ra các lỗ hổng SQL injection. Tôi thực sự khuyên bạn nên làm quen với chủ đề này. Nếu bạn định sử dụng kiểu kiến ​​trúc phát triển này, đặc biệt là trong một ứng dụng web công khai, nó sẽ hữu ích hơn nhiều.

Tiếp theo, chúng ta nên xử lý các giá trị NULL trong nối chuỗi . Ví dụ:biến cá thể @AddressPart từ ví dụ trước có thể làm mất hiệu lực toàn bộ câu lệnh SQL nếu được chuyển giá trị này.

Cách dễ nhất để xử lý vấn đề tiềm ẩn này là sử dụng hàm ISNULL để tạo câu lệnh SQL hợp lệ :

SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''


Quan trọng! Lệnh EXEC không được thiết kế để sử dụng lại các kế hoạch thực thi đã lưu trong bộ nhớ cache! Nó sẽ tạo một cái mới cho mỗi lần thực thi.

Để chứng minh điều này, chúng tôi sẽ thực hiện cùng một truy vấn hai lần, nhưng với một giá trị khác của tham số đầu vào. Sau đó, chúng tôi so sánh các kế hoạch thực hiện trong cả hai trường hợp:

USE AdventureWorks2019

-- Case 1
DECLARE @SQLExec NVARCHAR(4000) 
DECLARE @AddressPart NVARCHAR(50) = 'a'
 
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + @AddressPart + '%'''

EXEC (@SQLExec)

-- Case 2
SET @AddressPart = 'b'
 
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + @AddressPart + '%'''

EXEC (@SQLExec)

-- Compare plans
SELECT chdpln.objtype
,      chdpln.cacheobjtype
,      chdpln.usecounts
,      sqltxt.text
  FROM sys.dm_exec_cached_plans as chdpln
       CROSS APPLY sys.dm_exec_sql_text(chdpln.plan_handle) as sqltxt
 WHERE sqltxt.text LIKE 'SELECT *%';

Sử dụng Thủ tục mở rộng sp_executesql

Để sử dụng thủ tục này, chúng ta cần cung cấp cho nó một câu lệnh SQL, định nghĩa các tham số được sử dụng trong nó và giá trị của chúng. Cú pháp như sau:

sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'

Hãy bắt đầu với một ví dụ đơn giản cho thấy cách chuyển một câu lệnh và các tham số:

EXECUTE sp_executesql  
               N'SELECT *  
                     FROM Person.Address
	       WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
              N'@AddressPart NVARCHAR(50)',  -- Parameter definition
             @AddressPart = 'a';  -- Parameter value

Không giống như lệnh EXEC, sp_executesql thủ tục được lưu trữ mở rộng sử dụng lại các kế hoạch thực thi nếu được thực hiện với cùng một câu lệnh nhưng các tham số khác nhau. Do đó, tốt hơn là nên sử dụng sp_executesql qua EXEC lệnh :

EXECUTE sp_executesql  
               N'SELECT *  
                     FROM Person.Address
	       WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
              N'@AddressPart NVARCHAR(50)',  -- Parameter definition
             @AddressPart = 'a';  -- Parameter value

EXECUTE sp_executesql  
               N'SELECT *  
                     FROM Person.Address
	       WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
              N'@AddressPart NVARCHAR(50)',  -- Parameter definition
             @AddressPart = 'b';  -- Parameter value

SELECT chdpln.objtype
,      chdpln.cacheobjtype
,      chdpln.usecounts
,      sqltxt.text
  FROM sys.dm_exec_cached_plans as chdpln
       CROSS APPLY sys.dm_exec_sql_text(chdpln.plan_handle) as sqltxt
  WHERE sqltxt.text LIKE '%Person.Address%';

SQL động trong các thủ tục được lưu trữ

Cho đến bây giờ chúng tôi đã sử dụng SQL động trong các tập lệnh. Tuy nhiên, lợi ích thực sự trở nên rõ ràng khi chúng tôi thực thi các cấu trúc này trong các đối tượng lập trình tùy chỉnh - thủ tục do người dùng lưu trữ.

Hãy tạo một thủ tục tìm kiếm một người trong cơ sở dữ liệu AdventureWorks, dựa trên các giá trị tham số thủ tục đầu vào khác nhau. Từ đầu vào của người dùng, chúng tôi sẽ xây dựng lệnh SQL động và thực thi nó để trả về kết quả để gọi ứng dụng người dùng:

CREATE OR ALTER PROCEDURE [dbo].[test_dynSQL]  
(
  @FirstName		 NVARCHAR(100) = NULL	
 ,@MiddleName        NVARCHAR(100) = NULL	
 ,@LastName			 NVARCHAR(100) = NULL	
)
AS          
BEGIN      
SET NOCOUNT ON;  
 
DECLARE @SQLExec    	NVARCHAR(MAX)
DECLARE @Parameters		NVARCHAR(500)
 
SET @Parameters = '@FirstName NVARCHAR(100),
  		            @MiddleName NVARCHAR(100),
			@LastName NVARCHAR(100)
			'
 
SET @SQLExec = 'SELECT *
	 	           FROM Person.Person
		         WHERE 1 = 1
		        ' 
IF @FirstName IS NOT NULL AND LEN(@FirstName) > 0 
   SET @SQLExec = @SQLExec + ' AND FirstName LIKE ''%'' + @FirstName + ''%'' '

IF @MiddleName IS NOT NULL AND LEN(@MiddleName) > 0 
                SET @SQLExec = @SQLExec + ' AND MiddleName LIKE ''%'' 
                                                                    + @MiddleName + ''%'' '

IF @LastName IS NOT NULL AND LEN(@LastName) > 0 
 SET @SQLExec = @SQLExec + ' AND LastName LIKE ''%'' + @LastName + ''%'' '

EXEC sp_Executesql @SQLExec
	         ,             @Parameters
 , @[email protected],  @[email protected],  
                                                @[email protected]
 
END 
GO

EXEC [dbo].[test_dynSQL] 'Ke', NULL, NULL

Tham số OUTPUT trong sp_executesql

Chúng tôi có thể sử dụng sp_executesql với tham số OUTPUT để lưu giá trị được trả về bởi câu lệnh SELECT. Như được hiển thị trong ví dụ bên dưới, điều này cung cấp số hàng được truy vấn trả về cho biến đầu ra @Output:

DECLARE @Output INT

EXECUTE sp_executesql  
        N'SELECT @Output = COUNT(*)
            FROM Person.Address
	       WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
              N'@AddressPart NVARCHAR(50), @Output INT OUT',  -- Parameter definition
             @AddressPart = 'a', @Output = @Output OUT;  -- Parameters

SELECT @Output

Bảo vệ chống lại SQL Injection với thủ tục sp_executesql

Có hai hoạt động đơn giản bạn nên làm để giảm đáng kể nguy cơ bị tiêm SQL. Đầu tiên, đặt tên bảng trong dấu ngoặc. Thứ hai, kiểm tra mã xem các bảng có tồn tại trong cơ sở dữ liệu hay không. Cả hai phương pháp này đều có trong ví dụ dưới đây.

Chúng tôi đang tạo một thủ tục được lưu trữ đơn giản và thực thi nó với các tham số hợp lệ và không hợp lệ:

CREATE OR ALTER PROCEDURE [dbo].[test_dynSQL] 
(
  @InputTableName NVARCHAR(500)
)
AS 
BEGIN 
  DECLARE @AddressPart NVARCHAR(500)
  DECLARE @Output INT
  DECLARE @SQLExec NVARCHAR(1000) 

  IF EXISTS(SELECT 1 FROM sys.objects WHERE type = 'u' AND name = @InputTableName)
  BEGIN

      EXECUTE sp_executesql  
        N'SELECT @Output = COUNT(*)
            FROM Person.Address
	       WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
              N'@AddressPart NVARCHAR(50), @Output INT OUT',  -- Parameter definition
             @AddressPart = 'a', @Output = @Output OUT;  -- Parameters

       SELECT @Output
  END
  ELSE
  BEGIN
     THROW 51000, 'Invalid table name given, possible SQL injection. Exiting procedure', 1 
  END
END


EXEC [dbo].[test_dynSQL] 'Person'
EXEC [dbo].[test_dynSQL] 'NoTable'

So sánh tính năng của Lệnh EXEC và thủ tục được lưu trữ sp_executesql

Lệnh EXEC quy trình được lưu trữ sp_executesql
Không sử dụng lại gói bộ nhớ đệm Tái sử dụng gói bộ nhớ cache
Rất dễ bị chèn SQL Ít bị tấn công bởi SQL injection
Không có biến đầu ra Hỗ trợ các biến đầu ra
Không tham số hóa Hỗ trợ tham số hóa

Kết luận

Bài đăng này trình bày hai cách triển khai chức năng SQL động trong SQL Server. Chúng tôi đã tìm hiểu lý do tại sao tốt hơn nên sử dụng sp_executesql thủ tục nếu nó có sẵn. Ngoài ra, chúng tôi đã làm rõ đặc điểm cụ thể của việc sử dụng lệnh EXEC và các yêu cầu khử trùng đầu vào của người dùng để ngăn chặn việc đưa vào SQL.

Để gỡ lỗi chính xác và thoải mái các thủ tục được lưu trữ trong SQL Server Management Studio v18 (và cao hơn), bạn có thể sử dụng tính năng T-SQL Debugger chuyên biệt, một phần của giải pháp dbForge SQL Complete phổ biến.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Làm cách nào để thay đổi cơ sở dữ liệu mặc định của tôi trong SQL Server mà không cần sử dụng MS SQL Server Management Studio?

  2. Truy vấn SELECT với điều kiện CASE và SUM ()

  3. Cách hàm RIGHT () hoạt động trong SQL Server (T-SQL)

  4. Cách trả về kết quả truy vấn dưới dạng danh sách được phân tách bằng dấu phẩy trong SQL Server - STRING_AGG ()

  5. SQL thay thế tất cả các NULL