Vom Benutzer freigegebene Abfragen: Dynamic SQL vs. SQLCMD

15

Ich muss eine Reihe von Änderungen vornehmen und dokumentieren foo.sql Abfragen , die von einem Team des technischen Supports von DB geteilt werden (für Kundenkonfigurationen und ähnliches). Es gibt Arten von Tickets, bei denen jeder Kunde seine eigenen Server und Datenbanken hat, ansonsten ist das Schema auf der ganzen Linie gleich.

Gespeicherte Prozeduren sind derzeit keine Option. Ich überlege, ob ich Dynamic oder SQLCMD verwenden soll. Ich habe von beidem nicht viel verwendet, da ich bei SQL Server ein bisschen neu bin.

SQLCMD-Skripte Ich bin der Meinung, dass sie auf jeden Fall "sauberer" aussehen und nach Bedarf leichter zu lesen und geringfügige Änderungen an den Abfragen vorzunehmen sind, aber sie zwingen den Benutzer, den SQLCMD-Modus zu aktivieren. Dynamisch ist schwieriger, da die Syntaxhervorhebung aufgrund von Abfragen, die mithilfe von Zeichenfolgenmanipulation geschrieben werden, verloren geht.

Diese werden mit Management Studio 2012, SQL-Version 2008R2, bearbeitet und ausgeführt. Was sind einige der Vor- / Nachteile einer der beiden Methoden oder einige der "Best Practices" von SQL Server für die eine oder die andere Methode? Ist einer von ihnen "sicherer" als der andere?

Dynamisches Beispiel:

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-Beispiel:

: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
quelle
Was ist der Zweck use ...in Ihrem Skript? Ist es wichtig, dass die nachfolgende Abfrage korrekt ausgeführt wird? Ich frage, weil, wenn das Ändern der aktuellen Datenbank eines der erwarteten Ergebnisse Ihrer Abfrage ist, die dynamische SQL-Version dies nur im Bereich der dynamischen Abfrage ändert, nicht im äußeren Bereich, im Gegensatz zur SQLCMD-Variante (welche von Natürlich hat nur ein Anwendungsbereich).
Andriy M
Die useAnweisung könnte wahrscheinlich weggelassen werden, da der Gültigkeitsbereich in diesem speziellen Skript sowieso nicht geändert wird. Ich habe eine kleine Anzahl von Anwendungsfällen, in denen serverübergreifende Suchvorgänge durchgeführt werden, die jedoch möglicherweise den Rahmen dieses Beitrags sprengen.
Phrancis

Antworten:

13

Nur um diese aus dem Weg zu räumen:

  • Technisch gesehen handelt es sich bei beiden Optionen um "dynamische" / Ad-hoc-Abfragen, die erst nach Übermittlung analysiert / validiert werden. Und beide sind für SQL Injection anfällig , da sie nicht parametriert werden (wenn auch mit dem SQLCMD - Skripts, wenn Sie in einer Variablen von einem CMD - Skript sind vorbei , dann haben Sie die Möglichkeit , ersetzen 'mit '', die Arbeit kann oder auch nicht , je nachdem , wo die Variablen werden verwendet).

  • Jeder Ansatz hat Vor- und Nachteile:

    • SQL-Skripte in SSMS können einfach bearbeitet werden (was sehr hilfreich ist, wenn dies erforderlich ist), und die Arbeit mit Ergebnissen ist einfacher als mit der Ausgabe von SQLCMD. Auf der anderen Seite befindet sich der Benutzer in einer IDE, so dass es einfach ist, das SQL durcheinander zu bringen, und die IDE macht es einfach, eine Vielzahl von Änderungen vorzunehmen, ohne die SQL zu kennen, um dies zu tun.
    • Das Ausführen von Skripten über SQLCMD.EXE ermöglicht es dem Benutzer nicht, einfach Änderungen vorzunehmen (ohne das Skript in einem Editor zu bearbeiten und dann zuerst zu speichern). Dies ist großartig, wenn die Benutzer die Skripte nicht ändern sollen. Mit dieser Methode können Sie auch jede Ausführung protokollieren. Andererseits wäre es ziemlich umständlich, wenn die Skripte routinemäßig bearbeitet werden müssten. Wenn Benutzer 100.000 Zeilen einer Ergebnismenge durchsuchen und / oder diese Ergebnisse nach Excel oder etwas anderem kopieren müssen, ist dies bei diesem Ansatz ebenfalls schwierig.

Wenn Ihre Supportmitarbeiter keine Ad-hoc-Abfragen durchführen und nur diese Variablen ausfüllen, müssen sie sich nicht in SSMS befinden, wo sie diese Skripte bearbeiten und unerwünschte Änderungen vornehmen können.

Ich würde CMD-Skripte erstellen, um den Benutzer zur Eingabe der gewünschten Variablenwerte aufzufordern, und dann SQLCMD.EXE mit diesen Werten aufrufen . Das CMD-Skript könnte sogar die Ausführung in einer Datei protokollieren, einschließlich des Zeitstempels und der übermittelten Variablenwerte.

Erstellen Sie ein CMD-Skript pro SQL-Skript und legen Sie es in einem freigegebenen Netzwerkordner ab. Ein Benutzer klickt doppelt auf das CMD-Skript und es funktioniert einfach.

Hier ist ein Beispiel, das:

  • fordert den Benutzer zur Eingabe des Servernamens auf (diesbezüglich ist noch kein Fehler aufgetreten)
  • Fordert den Benutzer zur Eingabe des Datenbanknamens auf
    • Wenn Sie dieses Feld leer lassen, werden die Datenbanken auf dem angegebenen Server aufgelistet und erneut aufgefordert
    • Wenn der Datenbankname ungültig ist, wird der Benutzer erneut aufgefordert
  • Fordert den Benutzer zur Eingabe der OrderIDsSeparatedByCommas auf
    • Wenn leer, wird der Benutzer erneut aufgefordert
  • %OrderIDsSeparatedByCommas%Führt das SQL-Skript aus und übergibt den Wert von als SQLCMD-Variable$(OrderIDsSeparatedByCommas)
  • Protokolliert das Ausführungsdatum, die Ausführungszeit, ServerName, DatabaseName und OrderIDsSeparatedByCommas in einer Protokolldatei, die für die Windows-Anmeldung angegeben ist, in der das Skript ausgeführt wird Konflikt in der Protokolldatei, als ob der USERNAME in der Datei pro Eintrag protokolliert werden würde)
    • Wenn das Protokolldateiverzeichnis nicht vorhanden ist, wird es erstellt

Test-SQL-Skript (Name: FixProblemX.sql ):

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

CMD-Skript (Name: 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

Stellen Sie sicher, dass Sie die ScriptLogPathVariable oben im Skript bearbeiten .

Die SQL-Skripte (die von der -iBefehlszeilenoption für SQLCMD.EXE angegeben werden ) verfügen möglicherweise über einen vollständig qualifizierten Pfad, sind sich jedoch nicht ganz sicher.

Solomon Rutzky
quelle