Zapytania współdzielone przez użytkownika: Dynamiczny SQL vs. SQLCMD

15

Muszę przeformułować i udokumentować szereg foo.sqlzapytań, które będą udostępniane przez zespół wsparcia technicznego DB (w przypadku konfiguracji klienta i tym podobnych). Istnieją rodzaje biletów, które przychodzą regularnie, gdy każdy klient ma własne serwery i bazy danych, ale poza tym schemat jest taki sam we wszystkich obszarach.

Procedury przechowywane nie są obecnie dostępne. Zastanawiam się, czy użyć dynamicznego, czy SQLCMD, nie używałem ich zbyt często, ponieważ jestem trochę nowy w SQL Server.

Skrypty SQLCMD Wydaje mi się, że zdecydowanie „dla mnie” wygląda na czystsze, łatwiejsze do odczytania i wprowadzania w razie potrzeby niewielkich zmian w zapytaniach, ale także zmusza użytkownika do włączenia trybu SQLCMD. Dynamika jest trudniejsza, ponieważ wyróżnianie składni jest utratą z powodu pisania zapytań przy użyciu operacji na łańcuchach.

Są one edytowane i uruchamiane za pomocą Management Studio 2012, wersja SQL 2008R2. Jakie są zalety / wady jednej z metod lub niektóre „najlepsze praktyki” programu SQL Server dotyczące jednej lub drugiej metody? Czy jeden z nich jest „bezpieczniejszy” niż drugi?

Dynamiczny przykład:

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

Przykład 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
źródło
Jaki jest cel use ...twojego skryptu? Czy ważne jest prawidłowe wykonanie następnego zapytania? Pytam, ponieważ jeśli zmiana bieżącej bazy danych jest jednym z oczekiwanych wyników zapytania, dynamiczna wersja SQL zmieni ją tylko w zakresie zapytania dynamicznego, a nie w zakresie zewnętrznym, w przeciwieństwie do wariantu SQLCMD (który z oczywiście ma tylko jeden zakres).
Andriy M,
useOświadczenie mogło być prawdopodobnie pominięte, jako zakres nie zostanie zmieniony podczas tego konkretnego scenariusza jakikolwiek. Mam niewielką liczbę przypadków użycia, w których będą przeprowadzane wyszukiwania między serwerami, ale może to wykraczać poza zakres tego postu.
Phrancis

Odpowiedzi:

13

Aby to usunąć:

  • Technicznie rzecz biorąc, obie te opcje to zapytania „dynamiczne” / ad hoc, które nie są analizowane / sprawdzane przed przesłaniem. I oba są podatne na SQL Injection, ponieważ nie są parametryzowane (choć ze skryptami Sqlcmd, jeśli przechodzą w zmiennej ze skryptu CMD potem masz możliwość wymiany 'z '', co może lub może nie działać w zależności od tego, gdzie używane są zmienne).

  • Każde podejście ma wady i zalety:

    • Skrypty SQL w SSMS można łatwo edytować (co jest świetne, jeśli jest to wymagane), a praca z wynikami jest łatwiejsza niż w przypadku danych wyjściowych z SQLCMD. Z drugiej strony, użytkownik jest w środowisku IDE, więc łatwo jest popsuć SQL, a IDE ułatwia dokonywanie różnorodnych zmian bez znajomości SQL.
    • Uruchamianie skryptów za pomocą SQLCMD.EXE nie pozwala użytkownikowi na łatwe wprowadzanie zmian (bez edycji skryptu w edytorze, a następnie zapisania go w pierwszej kolejności). Jest to świetne, jeśli użytkownicy nie powinni zmieniać skryptów. Ta metoda pozwala także na rejestrowanie każdego jej wykonania. Z drugiej strony, jeśli istnieje potrzeba rutynowej edycji skryptów, byłoby to dość kłopotliwe. Lub, jeśli użytkownicy muszą zeskanować 100 000 wierszy zestawu wyników i / lub skopiować te wyniki do Excela lub czegoś takiego, to również jest trudne w tym podejściu.

Jeśli twoi pracownicy pomocy technicznej nie wykonują zapytań ad hoc i po prostu wypełniają te zmienne, nie muszą znajdować się w SSMS, gdzie mogą edytować te skrypty i wprowadzać niepożądane zmiany.

Utworzyłbym skrypty CMD, aby zapytać użytkownika o pożądane wartości zmiennych, a następnie wywołać SQLCMD.EXE z tymi wartościami. Skrypt CMD może nawet zapisać wykonanie do pliku, wraz ze znacznikiem czasu i przesłanymi wartościami zmiennych.

Utwórz jeden skrypt CMD dla każdego skryptu SQL i umieść w udostępnionym sieciowym folderze. Użytkownik dwukrotnie klika skrypt CMD i po prostu działa.

Oto przykład:

  • pyta użytkownika o nazwę serwera (nie ma jeszcze błędu sprawdzania tego)
  • monituje użytkownika o nazwę bazy danych
    • jeśli pozostawione puste, wyświetli listę baz danych na określonym serwerze i zostanie ponownie wyświetlony monit
    • jeśli nazwa bazy danych jest nieprawidłowa, użytkownik zostanie ponownie zapytany
  • monituje użytkownika o podanie OrderIDsSeparatedByCommas
    • jeśli puste, monituje użytkownika ponownie
  • uruchamia skrypt SQL, przekazując wartość %OrderIDsSeparatedByCommas%jako zmiennej SQLCMD$(OrderIDsSeparatedByCommas)
  • rejestruje datę wykonania, godzinę, nazwę serwera, nazwę bazy danych i ID zamówieniaSeparatedByCommas do pliku dziennika o nazwie Windows Login z uruchomionym skryptem (w ten sposób, jeśli katalog dziennika jest w sieci i korzysta z niego wiele osób, nie będzie żadnego zapisu rywalizacja o plik dziennika, jak w przypadku, gdyby NAZWA UŻYTKOWNIKA była zalogowana w pliku według pozycji)
    • jeśli katalog pliku dziennika nie istnieje, zostanie utworzony

Testuj skrypt SQL (o nazwie: FixProblemX.sql ):

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

Skrypt CMD (o nazwie: 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

Zmodyfikuj ScriptLogPathzmienną u góry skryptu.

Również skrypty SQL (określone przez -iprzełącznik wiersza polecenia dla SQLCMD.EXE ) mogą skorzystać z posiadania w pełni kwalifikowanej ścieżki, ale nie do końca pewni.

Solomon Rutzky
źródło