用户共享的查询:动态SQL与SQLCMD


15

我必须重构和记录许多foo.sql查询,这些查询将由数据库技术支持团队共享(针对客户配置等)。每个客户都有自己的服务器和数据库的票证类型经常出现,但其他方面的架构是相同的。

当前无法选择存储过程。我正在讨论是使用动态还是使用SQLCMD,因为我在SQL Server上有些新手,所以我也没有使用太多。

我觉得SQLCMD脚本对我来说绝对看起来更“干净”,并且更易于阅读和根据需要对查询进行较小的更改,但同时也迫使用户启用SQLCMD模式。由于使用字符串操作编写查询会导致语法高亮丢失,因此动态处理更加困难。

这些正在使用Management Studio 2012(SQL版本2008R2)进行编辑和运行。每种方法的优缺点是什么,或者一种方法或另一种方法的某些SQL Server“最佳实践”是什么?其中一个比另一个更“安全”吗?

动态示例:

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

use ...您的脚本的目的是什么?正确执行后续查询是否重要?我问是因为更改当前数据库是否是查询的预期结果之一,所以动态SQL版本只会在动态查询的范围(而不是外部范围)中更改它,这与SQLCMD变体不同(当然,只有一个范围)。
Andriy M

use语句可能会被忽略,因为无论如何在此特定脚本期间范围不会更改。我有少数用例,其中会有跨服务器搜索,但这可能超出了本文的范围。
Phrancis

Answers:


13

只是为了避免这些:

  • 从技术上讲,这两个选项都是“动态” /即席查询,只有在提交后才进行解析/验证。两者都容易受到SQL注入的攻击,因为它们没有参数化(尽管使用SQLCMD脚本,但是如果您要从CMD脚本传递变量,那么您确实有机会替换''',这取决于是否在哪里工作)变量)。

  • 每种方法都有优点和缺点:

    • 可以轻松地编辑SSMS中的SQL脚本(如果需要的话,这非常好),并且处理结果要比使用SQLCMD的输出容易。不利的一面是,用户位于IDE中,因此很容易弄乱SQL,并且IDE使得无需进行SQL更改即可轻松进行各种更改。
    • 通过SQLCMD.EXE运行脚本不允许用户轻松进行更改(无需在编辑器中编辑脚本,然后先保存脚本)。如果不希望用户更改脚本,那么这很好。此方法还允许记录它的每次执行。不利的一面是,如果需要例行编辑脚本,那么将很麻烦。或者,如果用户需要扫描结果集的10万行和/或将这些结果复制到Excel或其他东西,那么采用这种方法也很难。

如果您的支持人员不是在进行临时查询,而是在填写这些变量,那么他们就不必在SSMS中编辑这些脚本并进行不必要的更改。

我将创建CMD脚本来提示用户输入所需的变量值,然后使用这些值调用SQLCMD.EXE。CMD脚本甚至可以将执行记录到文件中,并带有时间戳和已提交的变量值。

为每个SQL脚本创建一个CMD脚本,并将其放置在网络共享文件夹中。用户双击CMD脚本即可使用。

这是一个示例:

  • 提示用户输入服务器名称(尚无错误检查)
  • 提示用户输入数据库名称
    • 如果保留为空白,它将列出指定服务器上的数据库并再次提示
    • 如果数据库名称无效,将再次提示用户
  • 提示用户输入OrderIDsSeparatedByCommas
    • 如果为空白,则再次提示用户
  • 运行SQL脚本,将值传递%OrderIDsSeparatedByCommas%为SQLCMD变量$(OrderIDsSeparatedByCommas)
  • 将执行日期,时间,ServerName,DatabaseName和OrderIDsSeparatedByCommas记录到以运行该脚本的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命令行开关指定可能会受益于具有完全限定的路径,但不能完全确定。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.