Consultas compartilhadas pelo usuário: SQL dinâmico vs. SQLCMD

15

Preciso refatorar e documentar uma série de foo.sqlconsultas que serão compartilhadas por uma equipe de suporte técnico da DB (para configurações do cliente e coisas assim). Existem tipos de tickets que vêm regularmente onde cada cliente tem seus próprios servidores e bancos de dados, mas, caso contrário, o esquema é o mesmo em todos os aspectos.

Os procedimentos armazenados não são uma opção no momento atual. Estou discutindo se usará SQLCMD dinâmico ou, também não usei muito, pois sou um pouco novo no SQL Server.

Script SQLCMD Sinto-me definitivamente "mais limpo" para mim e mais fácil de ler e fazer pequenas alterações nas consultas, conforme necessário, mas também força o usuário a ativar o modo SQLCMD. A dinâmica é mais difícil, pois o destaque da sintaxe é a perda devido à consulta da escrita usando manipulação de string.

Eles estão sendo editados e executados usando o Management Studio 2012, versão SQL 2008R2. Quais são alguns dos prós / contras de qualquer método ou algumas das "práticas recomendadas" do SQL Server em um método ou outro? Um deles é "mais seguro" que o outro?

Exemplo dinâmico:

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

Exemplo de 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));
Phrancis
fonte
Qual é o objetivo do use ...seu script? É importante para a execução correta da consulta subsequente? Estou perguntando, porque se a alteração do banco de dados atual é um dos resultados esperados da sua consulta, a versão dinâmica do SQL somente a altera no escopo da consulta dinâmica, não no escopo externo, ao contrário da variação SQLCMD (que, de claro, tem apenas um escopo).
Andriy M
A usedeclaração provavelmente pode ficar de fora, pois o escopo não será alterado durante esse script específico. Eu tenho um pequeno número de casos de uso em que haverá pesquisas entre servidores, mas isso pode estar além do escopo desta postagem.
Phrancis

Respostas:

13

Apenas para tirar isso do caminho:

  • Tecnicamente falando, essas duas opções são consultas "dinâmicas" / ad hoc que não são analisadas / validadas até serem enviadas. E ambos são suscetíveis a injeção SQL uma vez que eles não estão parametrizados (embora com os scripts SQLCMD, se você estiver passando em uma variável de um script CMD, em seguida, você tem a oportunidade de substituir 'com '', que pode ou não funcionar, dependendo de onde o variáveis ​​estão sendo usadas).

  • Existem prós e contras em cada abordagem:

    • Os scripts SQL no SSMS podem ser facilmente editados (o que é ótimo se for um requisito) e trabalhar com resultados é mais fácil do que com a saída do SQLCMD. Por outro lado, o usuário está em um IDE, por isso é fácil atrapalhar o SQL, e o IDE facilita fazer uma grande variedade de alterações sem conhecer o SQL para fazê-lo.
    • A execução de scripts via SQLCMD.EXE não permite que o usuário faça alterações facilmente (sem editar o script em um editor e salvá-lo primeiro). Isso é ótimo se os usuários não devem alterar os scripts. Este método também permite registrar cada execução dele. No lado negativo, se houver a necessidade de editar rotineiramente os scripts, isso seria bastante complicado. Ou, se os usuários precisarem varrer 100 mil linhas de um conjunto de resultados e / ou copiar esses resultados para o Excel ou algo assim, isso também será difícil nessa abordagem.

Se o pessoal de suporte não estiver fazendo consultas ad hoc e apenas preenchendo essas variáveis, elas não precisam estar no SSMS, onde podem editar esses scripts e fazer alterações indesejadas.

Eu criaria scripts CMD para solicitar ao usuário os valores variáveis ​​desejados e depois chamar SQLCMD.EXE com esses valores. O script CMD pode até registrar a execução em um arquivo, completo com registro de data e hora e valores variáveis ​​enviados.

Crie um script CMD por script SQL e coloque em uma pasta compartilhada em rede. Um usuário clica duas vezes no script CMD e ele simplesmente funciona.

Aqui está um exemplo que:

  • solicita ao usuário o nome do servidor (nenhum erro de verificação ainda)
  • solicita ao usuário o nome do banco de dados
    • se deixado em branco, listará os bancos de dados no servidor especificado e será solicitado novamente
    • se o nome do banco de dados for inválido, o usuário será solicitado novamente
  • solicita ao usuário os OrderIDsSeparatedByCommas
    • se estiver em branco, solicita ao usuário novamente
  • executa o script SQL, transmitindo o valor de %OrderIDsSeparatedByCommas%como a variável SQLCMD$(OrderIDsSeparatedByCommas)
  • registra a data de execução, hora, ServerName, DatabaseName e OrderIDsSeparatedByCommas em um arquivo de log nomeado para o logon do Windows executando o script (dessa forma, se o diretório de log for rede e houver várias pessoas usando isso, não haverá gravação) contenção no arquivo de log como poderia existir se o USERNAME fosse registrado no arquivo por entrada)
    • se o diretório do arquivo de log não existir, ele será criado

Teste o script SQL (nomeado: FixProblemX.sql ):

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

Script CMD (nomeado: 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

Certifique-se de editar a ScriptLogPathvariável na parte superior do script.

Além disso, os scripts SQL (especificados pela opção da -ilinha de comandos para SQLCMD.EXE ) podem se beneficiar de ter um caminho completo, mas não totalmente certo.

Solomon Rutzky
fonte