ข้อความค้นหาที่ผู้ใช้แบ่งปัน: ไดนามิก SQL กับ SQLCMD


15

ฉันต้องรีแฟคเตอร์และจัดทำเอกสารfoo.sqlแบบสอบถามจำนวนหนึ่งซึ่งจะใช้ร่วมกันโดยทีมสนับสนุนด้านเทคนิคของ DB (สำหรับการกำหนดค่าลูกค้าและสิ่งต่างๆเช่นนั้น) มีตั๋วหลายประเภทที่มาเป็นประจำที่ลูกค้าแต่ละรายมีเซิร์ฟเวอร์และฐานข้อมูลของตัวเอง แต่ไม่เช่นนั้นสคีมาจะเหมือนกันทั่วกระดาน

กระบวนงานที่เก็บไว้ไม่ใช่ตัวเลือกในเวลาปัจจุบัน ฉันกำลังถกเถียงกันว่าจะใช้ไดนามิกหรือ 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คำสั่งอาจจะออกเป็นขอบเขตจะไม่ได้รับการเปลี่ยนแปลงในช่วงสคริปต์นี้โดยเฉพาะอย่างยิ่งนะ ฉันมีกรณีการใช้งานจำนวนน้อยซึ่งจะมีการค้นหาข้ามเซิร์ฟเวอร์ แต่นั่นอาจเกินขอบเขตของโพสต์นี้
58

คำตอบ:


13

เพียงเพื่อให้สิ่งเหล่านี้พ้นไป:

  • ในทางเทคนิคแล้วตัวเลือกทั้งสองนี้เป็นข้อความค้นหา "ไดนามิก" / ad hoc ที่ไม่ได้แยกวิเคราะห์ / ตรวจสอบจนกว่าจะมีการส่ง และทั้งคู่มีความอ่อนไหวต่อการฉีด SQL เนื่องจากไม่มีการกำหนดพารามิเตอร์ (แม้ว่าจะใช้สคริปต์ SQLCMD หากคุณผ่านตัวแปรจากสคริปต์ CMD คุณก็มีโอกาสที่จะแทนที่'ด้วย''ซึ่งอาจหรืออาจไม่ทำงานขึ้นอยู่กับตำแหน่ง กำลังใช้ตัวแปร)

  • มีข้อดีข้อเสียของแต่ละวิธี:

    • สคริปต์ SQL ใน SSMS สามารถแก้ไขได้อย่างง่ายดาย (ซึ่งยอดเยี่ยมถ้านั่นเป็นข้อกำหนด) และการทำงานกับผลลัพธ์นั้นง่ายกว่าด้วยผลลัพธ์จาก SQLCMD ด้านล่างผู้ใช้อยู่ใน IDE จึงง่ายต่อการเลอะ SQL และ IDE ทำให้ง่ายต่อการเปลี่ยนแปลงที่หลากหลายโดยไม่ต้องรู้ว่า SQL ทำอะไร
    • การเรียกใช้สคริปต์ผ่าน SQLCMD.EXE ไม่อนุญาตให้ผู้ใช้ทำการเปลี่ยนแปลงได้อย่างง่ายดาย (โดยไม่ต้องแก้ไขสคริปต์ในเครื่องมือแก้ไขจากนั้นให้บันทึกก่อน) สิ่งนี้ดีมากหากผู้ใช้ไม่ควรเปลี่ยนสคริปต์ วิธีนี้ยังช่วยให้สามารถบันทึกการดำเนินการแต่ละครั้งได้ ด้านล่างหากมีความจำเป็นต้องแก้ไขสคริปต์เป็นประจำก็จะค่อนข้างยุ่งยาก หรือหากผู้ใช้ต้องการสแกนแถวผลลัพธ์ขนาด 100k และ / หรือคัดลอกผลลัพธ์เหล่านั้นไปยัง Excel หรือบางอย่างนั่นก็เป็นวิธีที่ยากเช่นกัน

หากกลุ่มสนับสนุนของคุณไม่ได้ทำการสืบค้นแบบเฉพาะกิจและเพียงกรอกตัวแปรเหล่านั้นพวกเขาก็ไม่จำเป็นต้องอยู่ใน SSMS ซึ่งพวกเขาสามารถแก้ไขสคริปต์เหล่านั้นและทำการเปลี่ยนแปลงที่ไม่ต้องการได้

ฉันจะสร้างสคริปต์ CMD เพื่อแจ้งให้ผู้ใช้สำหรับค่าตัวแปรที่ต้องการแล้วโทรSQLCMD.EXEด้วยค่าเหล่านั้น สคริปต์ CMD ยังสามารถบันทึกการดำเนินการไปยังไฟล์พร้อมด้วยการประทับเวลาและค่าตัวแปรที่ส่ง

สร้างหนึ่ง CMD script ต่อสคริปต์ SQL และวางในโฟลเดอร์แชร์เครือข่าย ผู้ใช้ดับเบิลคลิกที่สคริปต์ CMD และใช้งานได้

นี่คือตัวอย่างที่:

  • แจ้งให้ผู้ใช้สำหรับชื่อเซิร์ฟเวอร์ (ยังไม่มีการตรวจสอบข้อผิดพลาดในนั้น)
  • แจ้งให้ผู้ใช้สำหรับชื่อฐานข้อมูล
    • ถ้าเว้นว่างไว้มันจะแสดงรายการฐานข้อมูลบนเซิร์ฟเวอร์ที่ระบุและแจ้งให้อีกครั้ง
    • หากชื่อฐานข้อมูลไม่ถูกต้องผู้ใช้จะได้รับแจ้งอีกครั้ง
  • แจ้งให้ผู้ใช้สำหรับ OrderIDsSeparatedByCommas
    • หากว่างให้แจ้งผู้ใช้อีกครั้ง
  • รันสคริปต์ SQL โดยส่งผ่านค่า%OrderIDsSeparatedByCommas%เป็นตัวแปร SQLCMD$(OrderIDsSeparatedByCommas)
  • บันทึกวันที่ดำเนินการ, เวลา, ชื่อเซิร์ฟเวอร์, ชื่อฐานข้อมูลและ 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 (ระบุโดย-iสวิตช์บรรทัดคำสั่งสำหรับSQLCMD.EXE ) อาจได้รับประโยชน์จากการมีเส้นทางแบบเต็ม แต่ไม่แน่ใจทั้งหมด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.