Пользовательские запросы: динамический 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));
Phrancis
источник
Какова цель use ...вашего сценария? Это важно для правильного выполнения последующего запроса? Я спрашиваю, потому что, если изменение текущей базы данных является одним из ожидаемых результатов вашего запроса, динамическая версия SQL изменит его только в области динамического запроса, а не во внешней области, в отличие от варианта SQLCMD (который, из Конечно, имеет только одну сферу).
Андрей М
Это useутверждение, вероятно, можно было бы опустить, так как область действия не будет изменена во время этого конкретного сценария в любом случае. У меня есть небольшое количество случаев использования, когда будут выполняться межсерверные поиски, но это может выходить за рамки этого поста.
Фрэнсис

Ответы:

13

Просто чтобы убрать это с дороги:

  • Технически говоря, оба эти параметра являются «динамическими» / специальными запросами, которые не анализируются / проверяются до их отправки. И те, и другие подвержены SQL-инъекциям, поскольку они не параметризованы (хотя со сценариями SQLCMD, если вы передаете переменную из сценария CMD, у вас есть возможность заменить 'на '', что может или не может работать в зависимости от того, где переменные используются).

  • У каждого подхода есть свои плюсы и минусы:

    • Сценарии SQL в SSMS можно легко редактировать (что хорошо, если это необходимо), а работать с результатами проще, чем с выводом из SQLCMD. С другой стороны, пользователь находится в среде IDE, поэтому легко облажаться с SQL, а среда IDE позволяет легко вносить самые разнообразные изменения, не зная SQL для этого.
    • Запуск сценариев через SQLCMD.EXE не позволяет пользователю легко вносить изменения (без редактирования сценария в редакторе и последующего его сохранения). Это замечательно, если пользователи не должны менять сценарии. Этот метод также позволяет регистрировать каждое его выполнение. С другой стороны, если есть необходимость регулярно редактировать сценарии, то это будет довольно громоздко. Или, если пользователям необходимо просканировать 100-тысячные строки набора результатов и / или скопировать эти результаты в Excel или что-то в этом роде, то это также сложно сделать в этом подходе.

Если ваши сотрудники службы поддержки не выполняют специальные запросы и просто заполняют эти переменные, им не нужно находиться в SSMS, где они могут редактировать эти сценарии и вносить нежелательные изменения.

Я хотел бы создать сценарии CMD, чтобы запросить у пользователя нужные значения переменных, а затем вызвать SQLCMD.EXE с этими значениями. Сценарий CMD может даже записывать выполнение в файл, вместе с отметкой времени и представленными значениями переменных.

Создайте один сценарий CMD для каждого сценария SQL и поместите его в общую сетевую папку. Пользователь дважды щелкает по сценарию CMD, и он просто работает.

Вот пример, который:

  • запрашивает у пользователя имя сервера (пока нет проверки на ошибки)
  • запрашивает у пользователя имя базы данных
    • если оставить это поле пустым, он перечислит базы данных на указанном сервере и запросит снова
    • если имя базы данных неверно, пользователю будет предложено снова
  • запрашивает у пользователя OrderIDsSeparatedByCommas
    • если пусто, снова предлагает пользователю
  • запускает скрипт SQL, передавая значение %OrderIDsSeparatedByCommas%как переменную SQLCMD$(OrderIDsSeparatedByCommas)
  • записывает дату выполнения, время, имя_сервера, имя_базы_данных и OrderIDsSeparatedByCommas в файл журнала с именем для входа в Windows, выполняющего сценарий (таким образом, если каталог журнала является сетевым, и его используют несколько пользователей, запись не будет конкуренция за файл журнала, как если бы имя пользователя было зарегистрировано в файле для каждой записи)
    • если каталог файла журнала не существует, он будет создан

Тестовый сценарий 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 (заданных -iпереключателем командной строки для SQLCMD.EXE ) может быть полезно иметь полный путь, но не совсем уверенный.

Соломон Руцкий
источник