Sqlserver
 sql >> Baza danych >  >> RDS >> Sqlserver

10 wskazówek SP_EXECUTESQL, których należy unikać, aby uzyskać lepszy dynamiczny SQL

Czy wiesz, jak potężne może być narzędzie takie jak dynamiczny SQL? Użyj go w niewłaściwy sposób, a możesz pozwolić komuś przejąć Twoją bazę danych. Poza tym może być zbyt dużo złożoności. Ten artykuł ma na celu przedstawienie pułapek podczas używania SP_EXECUTESQL i oferuje 10 najczęstszych błędów, których należy unikać.

SP_EXECUTESQL to jeden ze sposobów uruchamiania poleceń SQL osadzonych w ciągu. Budujesz ten ciąg dynamicznie za pomocą kodu. Dlatego nazywamy to dynamicznym SQL. Oprócz serii instrukcji możesz również przekazać do niego listę parametrów i wartości. W rzeczywistości te parametry i wartości różnią się od polecenia EXEC. EXEC nie akceptuje parametrów dla dynamicznego SQL. Jednak SP_EXECUTESQL wykonujesz za pomocą EXEC!

Dla nowicjusza dynamicznego SQL, oto jak to wywołać.

EXEC sp_executesql <command string>[, <input or output parameters list>, <parameter value1>, <parameter value n>]

Tworzysz ciąg poleceń, który zawiera poprawne instrukcje SQL. Opcjonalnie można przekazać listę parametrów wejściowych lub wyjściowych oraz ich typów danych. I na koniec przekazujesz listę wartości oddzielonych przecinkami. Jeśli przekazujesz parametry, musisz przekazać wartości. Później zobaczysz zarówno dobre, jak i złe przykłady tego, gdy będziesz czytać dalej.

Korzystanie z SP_EXECUTESQL, gdy go nie potrzebujesz

Zgadza się. Jeśli go nie potrzebujesz, nie używaj go. Jeśli to staje się 10 przykazaniami SP_EXECUTESQL, to jest to pierwsze. Dzieje się tak, ponieważ ta procedura systemowa może być łatwo nadużywana. Ale skąd wiesz?

Odpowiedz na to:Czy jest problem, jeśli polecenie w twoim dynamicznym SQL staje się statyczne? Jeśli nie masz nic do powiedzenia w tej sprawie, nie potrzebujesz tego. Zobacz przykład.

DECLARE @sql NVARCHAR(100) = N'SELECT ProductID, Name FROM Production.Product ' +
			      'WHERE ProductID = @ProductID';
DECLARE @paramsList NVARCHAR(100) = N'@ProductID INT';
DECLARE @param1Value INT = 1;

EXEC sp_executesql @sql, @paramsList, @param1Value
GO

Jak widać, użycie SP_EXECUTESQL jest zakończone ciągiem poleceń, parametrem i wartością. Ale czy tak musi być? Na pewno nie. Dobrze mieć to:

DECLARE @productID INT = 1;

SELECT ProductID, Name
FROM Production.Product
WHERE ProductID = @productID;

Ale mogę się wstydzić, gdy zobaczysz przykłady w dalszej części artykułu. Ponieważ zaprzeczę temu, co twierdzę w tym pierwszym punkcie. Zobaczysz krótkie dynamiczne instrukcje SQL, które są lepsze niż statyczne. Wytrzymaj więc ze mną, ponieważ przykłady będą tylko dowodem przedstawionych tu punktów. W przypadku pozostałych przykładów udawaj przez chwilę, że patrzysz na kod przeznaczony dla dynamicznego SQL.

Obiekty i zmienne spoza zakresu

Uruchamianie serii poleceń SQL w ciągu znaków przy użyciu SP_EXECUTESQL przypomina tworzenie bezimiennej procedury składowanej i jej uruchamianie. Wiedząc o tym, obiekty takie jak tabele tymczasowe i zmienne poza ciągiem poleceń będą poza zakresem. Z tego powodu wystąpią błędy wykonawcze.

W przypadku korzystania ze zmiennych SQL

Sprawdź to.

DECLARE @extraText VARCHAR(10) = 'Name is '; -- note this variable
DECLARE @sql NVARCHAR(100) = N'SELECT @extraText + FirstName + SPACE(1) + LastName
                               FROM Person.Person WHERE BusinessEntityID = @BusinessEntityID';
DECLARE @paramList NVARCHAR(100) = N'@BusinessEntityID INT';
DECLARE @param1Value INT = 1;

EXEC sp_executesql @sql, @paramList, @BusinessEntityId = @param1Value;
GO

Zmienna @extraText jest niewidoczny dla wykonywanych poleceń. Zamiast tego znacznie lepszy jest łańcuch literowy lub zmienna zadeklarowana wewnątrz dynamicznego ciągu SQL. W każdym razie wynik jest następujący:

Czy widziałeś ten błąd na rysunku 1? Jeśli chcesz przekazać wartość wewnątrz dynamicznego ciągu SQL, dodaj kolejny parametr.

Podczas korzystania z tabel tymczasowych

Tabele tymczasowe w SQL Server również istnieją w zakresie modułu. Więc co myślisz o tym kodzie?

DECLARE @sql NVARCHAR(200) = N'SELECT BusinessEntityID, LastName, FirstName, MiddleName
                               INTO #TempNames
                               FROM Person.Person
                               WHERE BusinessEntityID BETWEEN 1 and 100';

EXEC sp_executesql @sql;
EXEC sp_executesql N'SELECT * FROM #TempNames'
GO

Powyższy kod wykonuje kolejno 2 procedury składowane. Tabela tymczasowa utworzona z pierwszego dynamicznego SQL nie będzie dostępna dla drugiego. W rezultacie otrzymasz nieprawidłową nazwę obiektu #TempNames błąd.

Błąd SQL Server QUOTENAME

Oto kolejny przykład, który na pierwszy rzut oka będzie wyglądał dobrze, ale spowoduje błąd nieprawidłowej nazwy obiektu. Zobacz poniższy kod.

DECLARE @sql NVARCHAR(100) = N'SELECT * FROM @Table';
DECLARE @tableName NVARCHAR(20) = 'Person.Person';

SET @sql = REPLACE(@sql,'@Table',QUOTENAME(@tableName));

PRINT @sql;
EXEC sp_executesql @sql;
GO

Aby była poprawna dla SELECT, umieść schemat i tabelę w nawiasach kwadratowych w następujący sposób:[Schema].[Tabela] . Lub w ogóle ich nie zamykaj (chyba że nazwa tabeli zawiera jedną lub więcej spacji). W powyższym przykładzie nie użyto nawiasów kwadratowych ani w tabeli, ani w schemacie. Zamiast używać @Table jako parametr stał się symbolem zastępczym dla REPLACE. Wykorzystano QUOTENAME.

QUOTENAME zawiera ciąg znaków z ogranicznikiem. Jest to również dobre do radzenia sobie z pojedynczymi cudzysłowami w dynamicznym ciągu SQL. Nawias kwadratowy jest domyślnym ogranicznikiem. Jak myślisz, co w powyższym przykładzie zrobiło QUOTENAME? Sprawdź Rysunek 2 poniżej.

Instrukcja SQL PRINT pomogła nam rozwiązać problem, wyświetlając dynamiczny ciąg SQL. Teraz znamy problem. Jaki jest najlepszy sposób, aby to naprawić? Jednym z rozwiązań jest poniższy kod:

DECLARE @sql NVARCHAR(100) = N'SELECT COUNT(*) FROM @Table';
DECLARE @tableName NVARCHAR(20) = 'Person.Person';

SET @sql = REPLACE(@sql,'@Table',QUOTENAME(PARSENAME(@tableName,2)) + '.'
                               + QUOTENAME(PARSENAME(@tableName,1)));
PRINT @sql;
EXEC sp_executesql @sql;
GO

Wyjaśnijmy to.

Najpierw @Stół jest używany jako symbol zastępczy dla REPLACE. Wyszuka wystąpienie @Table i zastąp go poprawnymi wartościami. Czemu? Jeśli zostanie użyty jako parametr, wystąpi błąd. To jest również powód używania REPLACE.

Następnie użyliśmy PARSENAME. Ciąg, który przekazaliśmy do tej funkcji, oddzieli go od schematu i tabeli. PARSENAME(@tableName,2) otrzyma schemat. Podczas gdy PARSENAME(@tableName,1) dostanie stół.

Na koniec, QUOTENAME uwzględni osobno nazwy schematów i tabel po zakończeniu PARSENAME. Wynik to [Osoba].[Osoba] . Teraz jest ważny.

Jednak lepszy sposób oczyszczenia dynamicznego ciągu SQL za pomocą nazw obiektów zostanie pokazany później.

Wykonywanie SP_EXECUTESQL z instrukcją NULL

Są dni, kiedy jesteś zdenerwowany lub przygnębiony. Po drodze można popełnić błędy. Następnie dodaj długi dynamiczny ciąg SQL do miksu i wartości NULL. A wynik?

Nic.

SP_EXECUTESQL da ci pusty wynik. Jak? Przez pomyłkową konkatenację wartości NULL. Rozważ poniższy przykład:

DECLARE @crlf NCHAR(2);
DECLARE @sql NVARCHAR(200) = N'SELECT' + @crlf +
	' p.Name AS Product' + @crlf +
	',v.Name AS Vendor' + @crlf +
	',v.AccountNumber' + @crlf +
	',p.ListPrice' + @crlf +
	'FROM Purchasing.ProductVendor pv' + @crlf +
	'INNER JOIN Production.Product p ON pv.ProductID = p.ProductID' + @crlf +
	'INNER JOIN Purchasing.Vendor v ON pv.BusinessEntityID = v.BusinessEntityID' + @crlf +
	'WHERE pv.BusinessEntityID = @BusinessEntityID';
DECLARE @paramsList NVARCHAR(100) = N'@BusinessEntityID INT';
DECLARE @BusinessEntityID INT = 1500;

PRINT @sql;
EXEC sp_executesql @sql, @paramsList, @BusinessEntityID
GO

Na początku kod wygląda dobrze. Jednak orle oczy wśród nas zauważą @crlf zmienny. Jego wartość? Zmienna nie została zainicjowana. Więc jest NULL.

Ale jaki jest sens tej zmiennej? W dalszej części dowiesz się, jak ważne jest formatowanie i debugowanie. Na razie skupmy się na aktualnym punkcie.

Po pierwsze, połączenie zmiennej NULL z dynamicznym ciągiem SQL da w wyniku NULL. Następnie PRINT wydrukuje puste. Wreszcie SP_EXECUTESQL będzie działał poprawnie z dynamicznym ciągiem SQL NULL. Ale nic nie zwraca.

NULL-y potrafią nas zahipnotyzować w już zły dzień. Zrób sobie krótką przerwę. Zrelaksować się. Następnie wróć z jaśniejszym umysłem.

Wstawianie wartości parametrów

Brudny.

Tak będzie wyglądać wstawianie wartości do dynamicznego ciągu SQL. Będzie wiele pojedynczych cudzysłowów dla ciągów i dat. Jeśli nie będziesz ostrożny, O’Briens i O’Neils również spowodują błędy. A ponieważ dynamiczny SQL jest ciągiem, musisz KONWERTOWAĆ lub RZUTOWAĆ wartości na ciąg. Oto przykład.

DECLARE @shipDate DATETIME = '06/11/2011';
 DECLARE @productID INT = 750;
 DECLARE @sql NVARCHAR(1000);
 SET @sql = N'SELECT
  soh.ShipDate
 ,sod.ProductID
 ,SUM(sod.OrderQty) AS TotalQty
 ,SUM(sod.LineTotal) AS LineTotal
 FROM Sales.SalesOrderHeader soh
 INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
 WHERE soh.ShipDate BETWEEN ' + '''' + CONVERT(VARCHAR(10), @shipDate, 101) + '''' + ' AND DATEADD(MONTH,1,' + '''' + CONVERT(VARCHAR(10), @shipDate, 101) + ''') ' +
 'AND sod.ProductID = ' + CAST(@productID AS VARCHAR(8)) +
 ' GROUP BY soh.ShipDate, sod.ProductID' +
 ' ORDER BY sod.ProductID';
 
 PRINT @sql;
 EXEC sp_executesql @sql;

Widziałem bardziej bałaganiarskie struny dynamiczne niż to. Zwróć uwagę na pojedyncze cudzysłowy, CONVERT i CAST. Gdyby użyto parametrów, mogłoby to wyglądać lepiej. Możesz to zobaczyć poniżej

DECLARE @shipDate DATETIME = '06/11/2011';
 DECLARE @productID INT = 750;
 DECLARE @sql NVARCHAR(1000);
 DECLARE @paramList NVARCHAR(500) = N'@shipDate DATETIME, @productID INT';
 SET @sql = N'SELECT
  soh.ShipDate
 ,sod.ProductID
 ,SUM(sod.OrderQty) AS TotalQty
 ,SUM(sod.LineTotal) AS LineTotal
 FROM Sales.SalesOrderHeader soh
 INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
 WHERE soh.ShipDate BETWEEN @shipDate AND DATEADD(MONTH,1,@shipDate)
 AND sod.ProductID = @productID
  GROUP BY soh.ShipDate, sod.ProductID
  ORDER BY sod.ProductID';

PRINT @sql;
EXEC sp_executesql @sql, @paramList, @shipDate, @productID
GO

Widzieć? Mniej pojedynczych cytatów, bez CONVERT i CAST, a także czystsze.

Ale istnieje jeszcze bardziej niebezpieczny efekt uboczny wartości wbudowanych.

Iniekcja SQL

Gdybyśmy żyli w świecie, w którym wszyscy ludzie są dobrzy, nigdy nie pomyślano by o wstrzykiwaniu SQL. Ale tak nie jest. Ktoś może wprowadzić do Twojego złośliwego kodu SQL. Jak to się może stać?

Oto scenariusz, którego użyjemy w naszym przykładzie:

  • Wartości są łączone z dynamicznym ciągiem SQL, tak jak w poprzednim przykładzie. Brak parametrów.
  • Dynamiczny ciąg SQL ma do 2 GB przy użyciu NVARCHAR(MAX). Dużo miejsca na wstrzyknięcie złośliwego kodu.
  • Najgorsze, dynamiczny ciąg SQL jest wykonywany z podwyższonymi uprawnieniami.
  • Instancja SQL Server akceptuje uwierzytelnianie SQL.

Czy to za dużo? W przypadku systemów zarządzanych przez jednego człowieka może się to zdarzyć. I tak nikt go nie sprawdza. W przypadku większych firm czasami istnieje dział IT, który nie dba o bezpieczeństwo.

Czy to nadal zagrożenie dzisiaj? Tak jest według dostawcy usług w chmurze Akamai w swoim raporcie o stanie Internetu / bezpieczeństwa. W okresie od listopada 2017 r. do marca 2019 r. wstrzyknięcie SQL stanowiło prawie dwie trzecie wszystkich ataków na aplikacje internetowe. To najwyższe ze wszystkich zbadanych zagrożeń. Bardzo źle.

Chcesz to zobaczyć na własne oczy?

Praktyka wstrzykiwania SQL:Zły przykład

Zróbmy w tym przykładzie wstrzyknięcie SQL. Możesz tego spróbować we własnym AdventureWorks Baza danych. Ale upewnij się, że uwierzytelnianie SQL jest dozwolone i uruchamiasz je z podwyższonymi uprawnieniami.

DECLARE @lastName NVARCHAR(MAX) = 'Mu';
DECLARE @firstName NVARCHAR(MAX) = 'Zheng''; CREATE LOGIN sà WITH PASSWORD=''12345''; ALTER SERVER ROLE sysadmin ADD MEMBER sà; --';
DECLARE @crlf NCHAR(2) = nchar(13) + nchar(10);

DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @crlf +
' p.LastName ' + @crlf +
',p.FirstName ' + @crlf +
',a.AddressLine1 ' + @crlf +
',a.AddressLine2 ' + @crlf +
',a.City ' + @crlf +
'FROM Person.Person p ' + @crlf +
'INNER JOIN Person.BusinessEntityAddress bea ON p.BusinessEntityID = bea.BusinessEntityID ' + @crlf +
'INNER JOIN Person.Address a ON bea.AddressID = a.AddressID ' + @crlf +
'WHERE p.LastName = ' + NCHAR(39) + @lastName + NCHAR(39) + ' ' + @crlf +
'AND p.FirstName = '  + NCHAR(39) + @firstName + NCHAR(39);

-- SELECT @sql;	-- uncomment if you want to see what's in @sql					
EXEC sp_executesql @sql;
GO

Powyższy kod nie reprezentuje rzeczywistego kodu istniejącej firmy. Nie można go nawet wywołać przez aplikację. Ale to ilustruje zły uczynek. Więc co tu mamy?

Najpierw wstrzyknięty kod utworzy konto SQL, które wygląda jak sa , ale nie jest. I jak sa , to ma sysadmina uprawnienia. W końcu zostanie to wykorzystane do uzyskania dostępu do bazy danych w dowolnym momencie z pełnymi uprawnieniami. Wszystko jest możliwe, gdy to zostanie zrobione:kradzież, usunięcie, manipulowanie danymi firmowymi, nazwij to.

Czy ten kod będzie działał? Zdecydowanie! A kiedy to nastąpi, superkonto zostanie utworzone po cichu. I oczywiście adres Zheng Mu pojawi się w zestawie wyników. Wszystko inne jest normalne. Shady, nie sądzisz?

Po uruchomieniu powyższego kodu spróbuj również uruchomić to:

SELECT IS_SRVROLEMEMBER('sysadmin','sà')

Jeśli zwróci 1, jest w tym jest. Alternatywnie możesz to sprawdzić w loginach bezpieczeństwa SQL Server w SQL Server Management Studio.

Więc co dostałeś?

Przerażające, prawda? (Jeśli to prawda, to tak.)

Praktyka wstrzykiwania SQL:dobry przykład

Teraz zmieńmy nieco kod za pomocą parametrów. Pozostałe warunki są nadal takie same.

DECLARE @lastName NVARCHAR(MAX) = 'Mu';
DECLARE @firstName NVARCHAR(MAX) = 'Zheng''; CREATE LOGIN sà WITH PASSWORD=''12345''; ALTER SERVER ROLE sysadmin ADD MEMBER sà; --';
DECLARE @crlf NCHAR(2) = nchar(13) + nchar(10);

DECLARE @sql NVARCHAR(MAX) = N'SELECT ' + @crlf +
' p.LastName ' + @crlf +
',p.FirstName ' + @crlf +
',a.AddressLine1 ' + @crlf +
',a.AddressLine2 ' + @crlf +
',a.City ' + @crlf +
'FROM Person.Person p ' + @crlf +
'INNER JOIN Person.BusinessEntityAddress bea ON p.BusinessEntityID = bea.BusinessEntityID ' + @crlf +
'INNER JOIN Person.Address a ON bea.AddressID = a.AddressID ' + @crlf +
'WHERE p.LastName = @lastName' + @crlf +
'AND p.FirstName = @firstName';

DECLARE @paramList NVARCHAR(300) = N'@lastName NVARCHAR(50), @firstName NVARCHAR(50)';

-- SELECT @sql;	-- uncomment if you want to see what's in @sql
EXEC sp_executesql @sql, @paramList, @lastName, @firstName;
GO

W wyniku są 2 różnice w porównaniu z pierwszym przykładem.

  • Po pierwsze, adres Zheng Mu się nie pojawi. Zestaw wyników jest pusty.
  • W takim razie konto renegata nie jest tworzone. Użycie IS_SRVROLEMEMBER zwróci NULL.

Co się stało?

Ponieważ używane są parametry, wartość @firstName jest „Zheng”; UTWÓRZ LOGIN sà Z HASŁEM=”12345”; ALT” . Jest to traktowane jako wartość dosłowna i obcinane tylko do 50 znaków. Sprawdź pierwszy parametr nazwy w powyższym kodzie. To NVARCHAR(50). Dlatego zestaw wyników jest pusty. W bazie danych nie ma osoby o takim imieniu.

To tylko jeden przykład iniekcji SQL i jeden sposób na uniknięcie tego. Więcej jest zaangażowanych w robienie rzeczy rzeczywistych. Ale mam nadzieję, że wyjaśniłem, dlaczego wartości wbudowane w dynamicznym SQL są złe.

Wyszukiwanie parametrów

Czy doświadczyłeś wolno działającej procedury składowanej z aplikacji, ale gdy próbowałeś uruchomić ją w programie SSMS, stała się szybka? To zdumiewające, ponieważ użyłeś dokładnych wartości parametrów używanych w aplikacji.

To jest parametr wąchania w akcji. SQL Server tworzy plan wykonania przy pierwszym uruchomieniu lub ponownej kompilacji procedury składowanej. Następnie ponownie wykorzystaj plan do następnego biegu. Brzmi świetnie, ponieważ SQL Server nie musi za każdym razem odtwarzać planu. Ale zdarza się, że inna wartość parametru wymaga innego planu, aby działać szybko.

Oto demonstracja użycia SP_EXECUTESQL i zwykłego statycznego SQL.

DECLARE @sql NVARCHAR(150) = N'SELECT Name FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID';
DECLARE @paramList NVARCHAR(100) = N'@ProductSubcategoryID INT';
DECLARE @ProductSubcategoryID INT = 23;

EXEC sp_executesql @sql, @paramList, @ProductSubcategoryID

Ten jest bardzo prosty. Sprawdź plan wykonania na rysunku 3.

Teraz wypróbujmy to samo zapytanie, używając statycznego SQL.

DECLARE @ProductSubcategoryID INT = 23;
SELECT Name FROM Production.Product WHERE ProductSubcategoryID = @ProductSubcategoryID

Sprawdź rysunek 4, a następnie porównaj go z rysunkiem 3.

Na rysunku 3 Poszukiwanie indeksu i pętla zagnieżdżona są używane. Ale na rysunku 4 jest to skanowanie indeksu klastrowego . Chociaż w tym momencie nie ma dostrzegalnego spadku wydajności, pokazuje to, że wąchanie parametrów nie jest tylko wyobraźnią.

Może to być bardzo frustrujące, gdy zapytanie staje się wolniejsze. Możesz w końcu użyć technik, takich jak rekompilacja lub użycie wskazówek dotyczących zapytań, aby tego uniknąć. Każdy z nich ma wady.

Niesformatowany dynamiczny ciąg SQL w SP_EXECUTESQL

Co może być nie tak z tym kodem?

DECLARE @sql NVARCHAR(100) = N'SELECT COUNT(*) AS ProductCount' +
                              'FROM Production.Product';
PRINT @sql;
EXEC sp_executesql @sql;

Jest krótki i prosty. Ale sprawdź Rysunek 5 poniżej.

Błędy pojawiają się, jeśli podczas tworzenia dynamicznego ciągu SQL nie przeszkadza ci pojedyncza spacja między słowami kluczowymi i obiektami. Jak na rysunku 5, gdzie ProductCount alias kolumny i słowo kluczowe FROM nie mają spacji. Staje się to mylące, gdy część ciągu spływa do następnego wiersza kodu. Sprawia, że ​​myślisz, że składnia jest poprawna.

Zauważ również, że łańcuch używał 2 linii w oknie kodu, ale wyjście PRINT pokazuje 1 linię. Wyobraź sobie, że jest to bardzo, bardzo długi ciąg poleceń. Trudno jest znaleźć problem, dopóki poprawnie nie sformatujesz ciągu z karty Wiadomości.

Aby rozwiązać ten problem, dodaj powrót karetki i wysuw wiersza. Prawdopodobnie zauważyłeś zmienną @crlf z poprzednich przykładów. Sformatowanie dynamicznego ciągu SQL za pomocą spacji i nowej linii sprawi, że dynamiczny ciąg SQL będzie bardziej czytelny. Jest to również świetne do debugowania.

Rozważ instrukcję SELECT z JOIN. Wymaga kilku linijek kodu, jak w poniższym przykładzie.

DECLARE @sql NVARCHAR(400)
DECLARE @shipDate DATETIME = '06/11/2011';
DECLARE @paramList NVARCHAR(100) = N'@shipDate DATETIME';
DECLARE @crlf NCHAR(2) = NCHAR(13) + NCHAR(10);

set @sql = N'SELECT ' + @crlf +
 'soh.ShipDate ' + @crlf +
 ',sod.ProductID ' + @crlf +
 ',SUM(sod.OrderQty) AS TotalQty ' + @crlf +
 ',SUM(sod.LineTotal) AS LineTotal ' + @crlf +
 'FROM Sales.SalesOrderHeader soh ' + @crlf +
 'INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID ' + @crlf +
 'WHERE soh.ShipDate = @shipDate' + @crlf +
 'GROUP BY soh.ShipDate, sod.ProductID ' + @crlf +
 'ORDER BY sod.ProductID';

 PRINT @sql;
 EXEC sp_executesql @sql,@paramList,@shipDate
 GO

Aby sformatować ciąg, @crlf zmienna jest ustawiona na NCHAR(13), powrót karetki i NCHAR(10), wysuw wiersza. Jest łączony z każdym wierszem, aby rozbić długi ciąg instrukcji SELECT. Aby zobaczyć wynik w zakładce Wiadomości, używamy PRINT. Sprawdź dane wyjściowe na rysunku 6 poniżej.

Sposób tworzenia dynamicznego ciągu SQL zależy od Ciebie. Cokolwiek Ci odpowiada, aby było jasne, czytelne i łatwe do debugowania, gdy nadejdzie czas.

Nieoczyszczone nazwy obiektów

Czy z jakiegoś powodu musisz dynamicznie ustawiać nazwy tabel, widoków lub baz danych? Następnie musisz „oczyścić” lub zweryfikować te nazwy obiektów, aby uniknąć błędów.

W naszym przykładzie użyjemy nazwy tabeli, chociaż zasada walidacji może dotyczyć również widoków. Sposób, w jaki sobie z tym poradzisz, będzie inny.

Wcześniej używaliśmy PARSENAME do oddzielenia nazwy schematu od nazwy tabeli. Można go również użyć, jeśli ciąg ma nazwę serwera i bazy danych. Ale w tym przykładzie użyjemy tylko nazw schematów i tabel. Resztę pozostawiam waszym błyskotliwym umysłom. Będzie to działać niezależnie od tego, czy nazwy tabel zawierają spacje, czy bez nich. Spacje w nazwach tabel lub widoków są prawidłowe. Tak więc działa w przypadku dbo.MyFoodCravings lub [dbo].[Moje zachcianki] .

PRZYKŁAD

Stwórzmy tabelę.

CREATE TABLE [dbo].[My Favorite Bikes]
(
	id INT NOT NULL,
	BikeName VARCHAR(50)
)
GO

Następnie tworzymy skrypt, który będzie wykorzystywał SP_EXECUTESQL. Spowoduje to uruchomienie ogólnej instrukcji DELETE dla dowolnej tabeli z kluczem jednokolumnowym. Pierwszą rzeczą do zrobienia jest przeanalizowanie pełnej nazwy obiektu.

DECLARE @object NVARCHAR(128) = '[dbo].[My Favorite Bikes]';
DECLARE @schemaName NVARCHAR(128) = PARSENAME(@object,2);
DECLARE @tableName NVARCHAR(128) = PARSENAME(@object,1);

W ten sposób oddzielamy schemat od tabeli. Do dalszej weryfikacji używamy OBJECT_ID. Jeśli nie jest NULL, to jest ważne.

IF NOT OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName)) IS NULL
BEGIN
	PRINT @object + ' is valid!'
	-- do the rest of your stuff here
END
ELSE
BEGIN
        PRINT 'Invalid object name ' + @object
	-- if you need to do anything else, insert it here
END

Zauważ też, że użyliśmy QUOTENAME. Zapewni to, że nazwy tabel ze spacją nie wywołają błędu, umieszczając je w nawiasach kwadratowych.

Ale co powiesz na walidację kolumny klucza? Możesz sprawdzić istnienie kolumny tabeli docelowej w sys.columns .

IF (SELECT COUNT(*) FROM sys.columns
	    WHERE [object_id] = OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName))
		  AND [name] = @idKey) > 0
BEGIN
     -- add miscellaneous code here, if needed
     EXEC sp_executesql @sql, @paramsList, @id
END
ELSE
BEGIN
     PRINT 'Invalid column name ' + @idKey + ' for object ' + @object
     -- if you need to do anything else, insert it here
END

Oto pełny skrypt tego, co chcemy osiągnąć.

DECLARE @object NVARCHAR(128) = '[dbo].[My Favorite Bikes]';
DECLARE @schemaName NVARCHAR(128) = PARSENAME(@object,2);
DECLARE @tableName NVARCHAR(128) = PARSENAME(@object,1);
DECLARE @isDebug BIT = 1;
DECLARE @idKey NVARCHAR(128) = N'id';

DECLARE @sql NVARCHAR(200) = N'DELETE FROM @object WHERE @idKey = @id';
DECLARE @id INT = 0;
DECLARE @paramList NVARCHAR(100) = N'@id INT';

IF NOT OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName)) IS NULL
BEGIN
   PRINT @object + ' is valid!'
   
   IF (SELECT COUNT(*) FROM sys.columns
       WHERE [object_id] = OBJECT_ID(QUOTENAME(@schemaName) + '.' + QUOTENAME(@tableName))
         AND [name] = @idKey) > 0
   BEGIN
       SET @sql = REPLACE(@sql, '@object', QUOTENAME(@schemaName) + '.' +          
                  QUOTENAME(@tableName));
       SET @sql = REPLACE(@sql, '@idkey', QUOTENAME(@idKey));
       IF @isDebug = 1
	   PRINT @sql;
       EXEC sp_executesql @sql, @paramList, @id
   END
   ELSE
       PRINT 'Invalid column name ' + @idKey + ' for object ' + @object
END
ELSE
BEGIN
   PRINT 'Invalid object name ' + @object
   -- if you need to do anything else, insert it here
END
GO

Wynik tego skryptu pokazano na rysunku 7 poniżej.

Możesz spróbować tego z innymi stołami. Po prostu zmień @object , @idkey i @id wartości zmiennych.

Brak możliwości debugowania

Mogą się zdarzyć błędy. Musisz więc znać wygenerowany dynamiczny ciąg SQL, aby znaleźć pierwotną przyczynę. Nie jesteśmy wróżbitami ani magikami odgadującymi formę dynamicznego ciągu SQL. Potrzebujesz więc flagi debugowania.

Zauważ na rysunku 7 wcześniej, że dynamiczny ciąg SQL jest drukowany w zakładce Messages w SSMS. Dodaliśmy @isDebug zmienna BIT i ustaw ją na 1 w kodzie. Gdy wartość wynosi 1, zostanie wydrukowany dynamiczny ciąg SQL. Jest to dobre, jeśli chcesz debugować skrypt lub procedurę składowaną w ten sposób. Po prostu ustaw go z powrotem na zero, gdy skończysz debugować. Jeśli jest to procedura składowana, ustaw tę flagę jako opcjonalny parametr z domyślną wartością zero.

Aby zobaczyć dynamiczny ciąg SQL, możesz użyć 2 możliwych metod.

  • Użyj PRINT, jeśli ciąg jest mniejszy lub równy 8000 znaków.
  • Lub użyj SELECT, jeśli ciąg ma więcej niż 8000 znaków.

Słaba wydajność dynamicznego SQL używanego w SP_EXECUTESQL

SP_EXECUTESQL może działać wolno, jeśli przypiszesz do niego wolno działające zapytanie. Okres. Nie wiąże się to jeszcze z problemami z podsłuchiwaniem parametrów.

Zacznij więc statycznie od kodu, który chcesz uruchomić dynamicznie. Następnie sprawdź Plan realizacji oraz STATISTICS IO. Sprawdź, czy brakuje indeksów, które musisz utworzyć. Dostrój to wcześnie.

Konkluzja w SP_EXECUTESQL

Używanie SP_EXECUTESQL jest jak posługiwanie się potężną bronią. Ale ten, kto ją dzierży, musi być w tym biegły. Chociaż to też nie jest nauka o rakietach. Jeśli jesteś dzisiaj nowicjuszem, z czasem może to stać się zdrowe.

Ta lista problemów nie jest kompletna. Ale obejmuje te wspólne. Jeśli potrzebujesz więcej informacji, sprawdź te linki:

  • Klątwa i błogosławieństwa dynamicznego SQL — Erland Sommarskog
  • SP_EXECUTESQL (Transact-SQL), firmy Microsoft

Lubię to? Następnie udostępnij go na swoich ulubionych platformach społecznościowych. Możesz również podzielić się z nami swoimi sprawdzonymi wskazówkami w sekcji Komentarze.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Entity Framework Code First z synonimami SQL Server

  2. Przekaż tabelę jako parametr do UDF serwera sql

  3. datetime vs datetime2 w SQL Server:jaka jest różnica?

  4. Kolumny daty w SQL-Server (MSSQL-JDBC 3.0) działające w środowisku Java 1.7.0 pobrane jako 2 dni w przeszłości

  5. Jak przekonwertować puste spacje na wartości null za pomocą SQL Server?