web-dev-qa-db-ja.com

ユーザー共有クエリ:動的SQLとSQLCMD

DBテクニカルサポートのチームが共有するいくつかのfoo.sqlクエリをリファクタリングして文書化する必要があります(お客様の構成などの場合)。各顧客が独自のサーバーとデータベースを持っている定期的に来るチケットのタイプがありますが、それ以外はスキーマは全体で同じです。

ストアドプロシージャは、現時点ではオプションではありません。動的とSQLCMDのどちらを使用するかについて議論しています。SQLServerは少し新しいので、どちらもあまり使用していません。

SQLCMDスクリプティング間違いなく "見た目"がきれいになり、必要に応じてクエリを読みやすくし、クエリに小さな変更を加えるのも簡単ですが、ユーザーはSQLCMDモードを有効にする必要があります。クエリは文字列操作を使用して記述されているため、構文の強調表示が失われるため、動的はより困難です。

これらは、Management Studio 2012、SQLバージョン2008R2を使用して編集および実行されています。どちらの方法の長所/短所、またはいずれかの方法でのSQL Serverの「ベストプラクティス」は何ですか。それらの1つは他より「安全」ですか?

動的な例:

declare @ServerName varchar(50) = 'REDACTED';
declare @DatabaseName varchar(50) = 'REDACTED';
declare @OrderIdsSeparatedByCommas varchar(max) = '597336, 595764, 594594';

declare @sql_OrderCheckQuery varchar(max) = ('
use {@DatabaseName};
select 
    -- stuff
from 
    {@ServerName}.{@DatabaseName}.[dbo].[client_orders]
        as "Order"
    inner join {@ServerName}.{@DatabaseName}.[dbo].[vendor_client_orders]
        as "VendOrder" on "Order".o_id = "VendOrder".vco_oid
where "VendOrder".vco_oid in ({@OrderIdsSeparatedByCommas});
');
set @sql_OrderCheckQuery = replace( @sql_OrderCheckQuery, '{@ServerName}',   quotename(@ServerName)   );
set @sql_OrderCheckQuery = replace( @sql_OrderCheckQuery, '{@DatabaseName}', quotename(@DatabaseName) );
set @sql_OrderCheckQuery = replace( @sql_OrderCheckQuery, '{@OrderIdsSeparatedByCommas}', @OrderIdsSeparatedByCommas );
print   (@sql_OrderCheckQuery); -- For debugging purposes.
execute (@sql_OrderCheckQuery);

SQLCMDの例:

:setvar ServerName "[REDACTED]";
:setvar DatabaseName "[REDACTED]";
:setvar OrderIdsSeparatedByCommas "597336, 595764, 594594"

use $(DatabaseName)
select 
    --stuff
from 
    $(ServerName).$(DatabaseName).[dbo].[client_orders]
        as "Order"
    inner join $(ServerName).$(DatabaseName).[dbo].[vendor_client_orders]
        as "VendOrder" on "Order".o_id = "VendOrder".vco_oid
where "VendOrder".vco_oid in ($(OrderIdsSeparatedByCommas));
15
Phrancis

これらを邪魔にならないようにするために:

  • 技術的に言えば、これらのオプションは両方とも、送信されるまで解析/検証されない「動的」/アドホッククエリです。また、どちらもパラメータ化されていないため、SQLインジェクションの影響を受けます(SQLCMDスクリプトでは、CMDスクリプトから変数を渡す場合、_'_を_''_で置き換える機会があります)。これは、変数が使用されている場所に応じて機能する場合と機能しない場合があります。

  • それぞれのアプローチには長所と短所があります。

    • SSMSのSQLスクリプトは簡単に編集でき(これが要件である場合に最適です)、結果の操作はSQLCMDからの出力よりも簡単です。マイナス面は、ユーザーがIDEにいるため、SQLを台無しにしたり、IDEを使用したりすると、 SQLがそれを実行することを知らなくても、さまざまな変更。
    • SQLCMD.EXEを介してスクリプトを実行しても、ユーザーは簡単に変更を加えることができません(エディターでスクリプトを編集してから最初に保存する必要はありません)。これは、ユーザーがスクリプトを変更することになっていない場合に最適です。このメソッドでは、実行ごとにログを記録することもできます。欠点としては、スクリプトを定期的に編集する必要がある場合、それはかなり面倒です。または、ユーザーが結果セットの10万行をスキャンし、その結果をExcelなどにコピーする必要がある場合も、このアプローチでは困難です。

サポートスタッフがアドホッククエリを実行せず、それらの変数を入力するだけの場合、それらのスクリプトを編集して不要な変更を加えることができるSSMSにいる必要はありません。

ユーザーに目的の変数値を要求するCMDスクリプトを作成し、それらの値でSQLCMD.EXEを呼び出します。 CMDスクリプトは、タイムスタンプと送信された変数値を完全に含めて、実行をファイルに記録することもできます。

SQLスクリプトごとに1つのCMDスクリプトを作成し、ネットワーク化された共有フォルダーに配置します。ユーザーがCMDスクリプトをダブルクリックすると、それが機能します。

以下はその例です。

  • ユーザーにサーバー名の入力を求めます(まだエラーチェックは行われていません)。
  • ユーザーにデータベース名の入力を求める
    • 空白のままにすると、指定したサーバー上のデータベースが一覧表示され、再度プロンプトが表示されます
    • データベース名が無効な場合、ユーザーに再度プロンプトが表示されます
  • orderIDsSeparatedByCommas の入力をユーザーに求めます
    • 空白の場合、ユーザーに再度プロンプトを表示します
  • sQLスクリプトを実行し、_%OrderIDsSeparatedByCommas%_の値をSQLCMD変数$(OrderIDsSeparatedByCommas)として渡します
  • 実行日時、ServerName、DatabaseName、およびOrde​​rIDsSeparatedByCommasを、スクリプトを実行しているWindowsログイン用に名前が付けられたログファイルに記録します(これにより、ログディレクトリがネットワークで、これを使用している人が複数いる場合、書き込みは行われません。 USERNAMEがエントリごとにファイルに記録された場合に発生する可能性があるようなログファイルの競合)
    • ログファイルディレクトリが存在しない場合は、作成されます

テストSQLスクリプト(名前:FixProblemX.sql):

_SELECT  *
FROM    sys.objects
WHERE   [schema_id] IN ($(OrderIdsSeparatedByCommas));
_

CMDスクリプト(名前:FixProblemX.cmd):

_@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION

SET ScriptLogPath=\\server\share\RunSqlCmdScripts\LogFiles

CLS

SET /P ScriptServerName=Please enter in a Server Name (leave blank to exit): 

IF "%ScriptServerName%" == "" GOTO :ThisIsTheEnd

REM echo %ScriptServerName%

:RequestDatabaseName
ECHO.
SET /P ScriptDatabaseName=Please enter in a Database Name (leave blank to list DBs on %ScriptServerName%): 

IF "%ScriptDatabaseName%" == "" GOTO :GetDatabaseNames

SQLCMD -b -E -W -h-1 -r0 -S %ScriptServerName% -Q "SET NOCOUNT ON; IF (NOT EXISTS(SELECT [name] FROM sys.databases WHERE [name] = N'%ScriptDatabaseName%')) RAISERROR('Invalid DB name!', 16, 1);" 2> nul

IF !ERRORLEVEL! GTR 0 (
    ECHO.
    ECHO That Database Name is invalid. Please try again.

    SET ScriptDatabaseName=
    GOTO :RequestDatabaseName
)

:RequestOrderIDs
ECHO.
SET /P OrderIdsSeparatedByCommas=Please enter in the OrderIDs (separate multiple IDs with commas): 

IF "%OrderIdsSeparatedByCommas%" == "" (

    ECHO.
    ECHO Don't play me like that. You gots ta enter in at least ONE lousy OrderID, right??
    GOTO :RequestOrderIDs
)


REM Finally run SQLCMD!!
SQLCMD -E -W -S %ScriptServerName% -d %ScriptDatabaseName% -i FixProblemX.sql -v OrderIdsSeparatedByCommas=%OrderIdsSeparatedByCommas%

REM Log this execution
SET ScriptLogFile=%ScriptLogPath%\%~n0_%USERNAME%.log
REM echo %ScriptLogFile%

IF NOT EXIST %ScriptLogPath% MKDIR %ScriptLogPath%

ECHO %DATE% %TIME% ServerName=%ScriptServerName%    DatabaseName=[%ScriptDatabaseName%] OrderIdsSeparatedByCommas=%OrderIdsSeparatedByCommas%   >> %ScriptLogFile%

GOTO :ThisIsTheEnd

:GetDatabaseNames
ECHO.
SQLCMD -E -W -h-1 -S %ScriptServerName% -Q "SET NOCOUNT ON; SELECT [name] FROM sys.databases ORDER BY [name];"
ECHO.
GOTO :RequestDatabaseName

:ThisIsTheEnd
PAUSE
_

スクリプトの先頭に向かってScriptLogPath変数を編集してください。

また、SQLスクリプト(_SQLCMD.EXEの_-i_コマンドラインスイッチで指定)mightの利点完全修飾パスを持っているからですが、完全にはわかりません。

13
Solomon Rutzky