動的SQLは、実行時に構築および実行されるステートメントであり、通常、動的に生成されたSQL文字列部分、入力パラメーター、またはその両方が含まれます。
動的に生成されたSQLコマンドを作成して実行するには、さまざまな方法を使用できます。現在の記事では、それらを調査し、それらのプラス面とマイナス面を定義し、いくつかの頻繁なシナリオでクエリを最適化するための実用的なアプローチを示します。
動的SQLを実行するには、次の2つの方法を使用します。 EXEC コマンドとsp_executesql ストアドプロシージャ。
EXEC/EXECUTEコマンドの使用
最初の例では、 AdventureWorksから単純な動的SQLステートメントを作成します。 データベース。この例には、連結された文字列変数@AddressPartを通過し、最後のコマンドで実行される1つのフィルターがあります。
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)
文字列連結によって構築されたクエリは、SQLインジェクションの脆弱性をもたらす可能性があることに注意してください。このトピックに精通することを強くお勧めします。この種の開発アーキテクチャを使用することを計画している場合、特に公開されているWebアプリケーションで使用する場合は、非常に便利です。
次に、文字列連結のNULL値を処理する必要があります 。たとえば、前の例の@AddressPartインスタンス変数は、この値が渡された場合、SQLステートメント全体を無効にする可能性があります。
この潜在的な問題を処理する最も簡単な方法は、ISNULL関数を使用して有効なSQLステートメントを作成することです :
SET @SQLExec = 'SELECT * FROM Person.Address WHERE AddressLine1 LIKE ''%' + ISNULL(@AddressPart, ‘ ‘) + '%'''
重要! EXECコマンドは、キャッシュされた実行プランを再利用するようには設計されていません。実行ごとに新しいものが作成されます。
これを示すために、同じクエリを2回実行しますが、入力パラメータの値は異なります。次に、両方の場合の実行プランを比較します。
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 *%';
拡張プロシージャsp_executesqlの使用
このプロシージャを使用するには、SQLステートメント、そこで使用されるパラメータの定義、およびそれらの値を指定する必要があります。構文は次のとおりです。
sp_executesql @SQLStatement, N'@ParamNameDataType' , @Parameter1 = 'Value1'
ステートメントとパラメータを渡す方法を示す簡単な例から始めましょう:
EXECUTE sp_executesql
N'SELECT *
FROM Person.Address
WHERE AddressLine1 LIKE ''%'' + @AddressPart + ''%''', -- SQL Statement
N'@AddressPart NVARCHAR(50)', -- Parameter definition
@AddressPart = 'a'; -- Parameter value
EXECコマンドとは異なり、 sp_executesql 拡張ストアドプロシージャは、同じステートメントで異なるパラメータを使用して実行された場合、実行プランを再利用します。したがって、 sp_executesqlを使用することをお勧めします。 EXEC以上 コマンド :
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
これまで、スクリプトで動的SQLを使用していました。ただし、これらの構成をカスタムプログラミングオブジェクト(ユーザーストアドプロシージャ)で実行すると、実際のメリットが明らかになります。
さまざまな入力プロシージャパラメータ値に基づいて、AdventureWorksデータベースで人を検索するプロシージャを作成しましょう。ユーザー入力から、動的SQLコマンドを作成して実行し、呼び出し元のユーザーアプリケーションに結果を返します。
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
sp_executesqlのOUTPUTパラメータ
sp_executesqlを使用できます OUTPUTパラメータを使用して、SELECTステートメントによって返される値を保存します。以下の例に示すように、これはクエリによって出力変数@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
sp_executesqlプロシージャによるSQLインジェクションに対する保護
SQLインジェクションのリスクを大幅に減らすために実行する必要がある2つの簡単なアクティビティがあります。まず、テーブル名を角かっこで囲みます。次に、データベースにテーブルが存在するかどうかをコードでチェックインします。これらの方法は両方とも、以下の例に示されています。
単純なストアドプロシージャを作成し、有効なパラメータと無効なパラメータを使用して実行しています:
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'
EXECコマンドとsp_executesqlストアドプロシージャの機能比較
EXECコマンド | sp_executesqlストアドプロシージャ |
キャッシュプランの再利用なし | キャッシュプランの再利用 |
SQLインジェクションに対して非常に脆弱です | SQLインジェクションに対する脆弱性がはるかに少ない |
出力変数なし | 出力変数をサポートします |
パラメータ化なし | パラメータ化をサポート |
結論
この投稿では、SQLServerに動的SQL機能を実装する2つの方法を示しました。 sp_executesqlを使用する方がよい理由を学びました 利用可能な場合は手順。また、EXECコマンドを使用する際の特異性と、SQLインジェクションを防ぐためにユーザー入力をサニタイズする必要があることを明確にしました。
SQL Server Management Studio v18(およびそれ以降)でのストアドプロシージャの正確で快適なデバッグには、人気のあるdbForgeSQLCompleteソリューションの一部である専用のT-SQLデバッガー機能を使用できます。