Kiedy jesteś głęboko zaangażowany w rozwiązywanie problemów w programie SQL Server, czasami chcesz poznać dokładną kolejność wykonywania zapytań. Widzę to w przypadku bardziej skomplikowanych procedur składowanych, które zawierają warstwy logiki, lub w scenariuszach, w których jest dużo nadmiarowego kodu. Zdarzenia rozszerzone są tutaj naturalnym wyborem, ponieważ są zwykle używane do przechwytywania informacji o wykonywaniu zapytań. Często możesz użyć identyfikatora sesji i znacznika czasu, aby zrozumieć kolejność zdarzeń, ale jest opcja sesji dla XE, która jest jeszcze bardziej niezawodna:Śledź przyczynowość.
Gdy włączysz śledzenie przyczynowości dla sesji, do każdego zdarzenia zostanie dodany identyfikator GUID i numer sekwencyjny, których można następnie użyć do prześledzenia kolejności występowania zdarzeń. Koszty ogólne są minimalne i mogą być znaczną oszczędnością czasu w wielu scenariuszach rozwiązywania problemów.
Konfiguracja
Korzystając z bazy danych WideWorldImporters, utworzymy procedurę składowaną do użycia:
DROP PROCEDURE IF EXISTS [Sales].[usp_CustomerTransactionInfo]; GO CREATE PROCEDURE [Sales].[usp_CustomerTransactionInfo] @CustomerID INT AS SELECT [CustomerID], SUM([AmountExcludingTax]) FROM [Sales].[CustomerTransactions] WHERE [CustomerID] = @CustomerID GROUP BY [CustomerID]; SELECT COUNT([OrderID]) FROM [Sales].[Orders] WHERE [CustomerID] = @CustomerID GO
Następnie utworzymy sesję wydarzenia:
CREATE EVENT SESSION [TrackQueries] ON SERVER
ADD EVENT sqlserver.sp_statement_completed(
WHERE ([sqlserver].[is_system]=(0))),
ADD EVENT sqlserver.sql_statement_completed(
WHERE ([sqlserver].[is_system]=(0)))
ADD TARGET package0.event_file
(
SET filename=N'C:\temp\TrackQueries',max_file_size=(256)
)
WITH
(
MAX_MEMORY = 4096 KB,
EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY = 30 SECONDS,
MAX_EVENT_SIZE = 0 KB,
MEMORY_PARTITION_MODE = NONE,
TRACK_CAUSALITY = ON,
STARTUP_STATE = OFF
); Zamierzamy również uruchamiać zapytania ad hoc, więc przechwytujemy zarówno sp_statement_completed (wypowiedzi ukończone w ramach procedury składowanej), jak i sql_statement_completed (ukończone instrukcje, które nie znajdują się w procedurze składowanej). Zauważ, że opcja TRACK_CAUSALITY dla sesji jest ustawiona na ON. Ponownie, to ustawienie jest specyficzne dla sesji zdarzenia i musi być włączone przed jej uruchomieniem. Nie możesz włączyć tego ustawienia w locie, na przykład możesz dodawać lub usuwać zdarzenia i cele podczas trwania sesji.
Aby rozpocząć sesję wydarzenia za pomocą interfejsu użytkownika, kliknij go prawym przyciskiem myszy i wybierz opcję Rozpocznij sesję.
Testowanie
W Management Studio uruchomimy następujący kod:
EXEC [Sales].[usp_CustomerTransactionInfo] 490;
SELECT [c].[CustomerID], [c].[CustomerName], [p].[FullName], [o].[OrderID]
FROM [Application].[People] [p]
JOIN [Sales].[Customers] [c] ON [p].[PersonID] = [c].[PrimaryContactPersonID]
JOIN [Sales].[Orders] [o] ON [c].[CustomerID] = [o].[CustomerID]
WHERE [p].[FullName] = 'Naseem Radan'; Oto nasze wyjście XE:

Zauważ, że pierwsze wykonane zapytanie, które jest podświetlone, to SELECT @@SPID i ma identyfikator GUID FDCCB1CF-CA55-48AA-8FBA-7F5EBF870674. Nie wykonaliśmy tego zapytania, wystąpiło ono w tle i chociaż sesja XE jest skonfigurowana do odfiltrowywania zapytań systemowych, to – z jakiegokolwiek powodu – jest nadal przechwytywane.
Następne cztery wiersze reprezentują kod, który faktycznie wykonaliśmy. Istnieją dwa zapytania z procedury składowanej, sama procedura składowana, a następnie nasze zapytanie ad hoc. Wszystkie mają ten sam GUID, ACBFFD99-2400-4AFF-A33F-351821667B24. Obok identyfikatora GUID znajduje się identyfikator sekwencji (seq), a zapytania są ponumerowane od jednego do czterech.
W naszym przykładzie nie użyliśmy GO do rozdzielenia instrukcji na różne partie. Zwróć uwagę, jak zmienia się wyjście, gdy to robimy:
EXEC [Sales].[usp_CustomerTransactionInfo] 490;
GO
SELECT [c].[CustomerID], [c].[CustomerName], [p].[FullName], [o].[OrderID]
FROM [Application].[People] [p]
JOIN [Sales].[Customers] [c] ON [p].[PersonID] = [c].[PrimaryContactPersonID]
JOIN [Sales].[Orders] [o] ON [c].[CustomerID] = [o].[CustomerID]
WHERE [p].[FullName] = 'Naseem Radan';
GO

Nadal mamy taką samą liczbę wierszy, ale teraz mamy trzy identyfikatory GUID. Jeden dla SELECT @@SPID (E8D136B8-092F-439D-84D6-D4EF794AE753), jeden dla trzech zapytań reprezentujących procedurę składowaną (F962B9A4-0665-4802-9E6C-B217634D8787) i jeden dla zapytania ad hoc (5DD6A5FE -9702-4DE5-8467-8D7CF55B5D80).
To najprawdopodobniej zobaczysz, patrząc na dane z aplikacji, ale zależy to od tego, jak aplikacja działa. Jeśli korzysta z puli połączeń, a połączenia są regularnie resetowane (co jest oczekiwane), każde połączenie będzie miało swój własny identyfikator GUID.
Możesz go odtworzyć, korzystając z przykładowego kodu PowerShell poniżej:
while(1 -eq 1)
{
$SqlConn = New-Object System.Data.SqlClient.SqlConnection;
$SqlConn.ConnectionString = "Data Source=Hedwig\SQL2017;Initial Catalog=WideWorldImporters;Integrated Security=True;Application Name = MyCoolApp";
$SQLConn.Open()
$SqlCmd = $SqlConn.CreateCommand();
$SqlCmd.CommandText = "SELECT TOP 1 CustomerID FROM Sales.Customers ORDER BY NEWID();"
$SqlCmd.CommandType = [System.Data.CommandType]::Text;
$SqlReader = $SqlCmd.ExecuteReader();
$Results = New-Object System.Collections.ArrayList;
while ($SqlReader.Read())
{
$Results.Add($SqlReader.GetSqlInt32(0)) | Out-Null;
}
$SqlReader.Close();
$Value = Get-Random -InputObject $Results;
$SqlCmd = $SqlConn.CreateCommand();
$SqlCmd.CommandText = "Sales.usp_CustomerTransactionInfo"
$SqlCmd.CommandType = [System.Data.CommandType]::StoredProcedure;
$SqlParameter = $SqlCmd.Parameters.AddWithValue("@CustomerID", $Value);
$SqlParameter.SqlDbType = [System.Data.SqlDbType]::Int;
$SqlCmd.ExecuteNonQuery();
$SqlConn.Close();
$Names = New-Object System.Collections.Generic.List``1[System.String]
$SqlConn = New-Object System.Data.SqlClient.SqlConnection
$SqlConn.ConnectionString = "Data Source=Hedwig\SQL2017;Initial Catalog=WideWorldImporters;User Id=aw_webuser;Password=12345;Application Name=AdventureWorks Online Ordering;Workstation ID=AWWEB01";
$SqlConn.Open();
$SqlCmd = $SqlConn.CreateCommand();
$SqlCmd.CommandText = "SELECT FullName FROM Application.People ORDER BY NEWID();";
$dr = $SqlCmd.ExecuteReader();
while($dr.Read())
{
$Names.Add($dr.GetString(0));
}
$SqlConn.Close();
$name = Get-Random -InputObject $Names;
$query = [String]::Format("SELECT [c].[CustomerID], [c].[CustomerName], [p].[FullName], [o].[OrderID]
FROM [Application].[People] [p]
JOIN [Sales].[Customers] [c] ON [p].[PersonID] = [c].[PrimaryContactPersonID]
JOIN [Sales].[Orders] [o] ON [c].[CustomerID] = [o].[CustomerID]
WHERE [p].[FullName] = '{0}';", $name);
$SqlConn = New-Object System.Data.SqlClient.SqlConnection
$SqlConn.ConnectionString = "Data Source=Hedwig\SQL2017;Initial Catalog=WideWorldImporters;User Id=aw_webuser;Password=12345;Application Name=AdventureWorks Online Ordering;Workstation ID=AWWEB01";
$SqlConn.Open();
$SqlCmd = $sqlconnection.CreateCommand();
$SqlCmd.CommandText = $query
$SqlCmd.ExecuteNonQuery();
$SqlConn.Close();
} Oto przykład wyjścia zdarzeń rozszerzonych po krótkim czasie działania kodu:

Istnieją cztery różne identyfikatory GUID dla naszych pięciu instrukcji, a jeśli spojrzysz na powyższy kod, zobaczysz, że wykonano cztery różne połączenia. Jeśli zmienisz sesję zdarzenia tak, aby zawierała zdarzenie rpc_completed, możesz zobaczyć wpisy z exec sp_reset_connection.
Twoje dane wyjściowe XE będą zależeć od kodu i aplikacji; Wspomniałem na początku, że jest to przydatne przy rozwiązywaniu problemów z bardziej złożonymi procedurami składowanymi. Rozważ następujący przykład:
DROP PROCEDURE IF EXISTS [Sales].[usp_CustomerTransactionInfo]; GO CREATE PROCEDURE [Sales].[usp_CustomerTransactionInfo] @CustomerID INT AS SELECT [CustomerID], SUM([AmountExcludingTax]) FROM [Sales].[CustomerTransactions] WHERE [CustomerID] = @CustomerID GROUP BY [CustomerID]; SELECT COUNT([OrderID]) FROM [Sales].[Orders] WHERE [CustomerID] = @CustomerID GO DROP PROCEDURE IF EXISTS [Sales].[usp_GetFullCustomerInfo]; GO CREATE PROCEDURE [Sales].[usp_GetFullCustomerInfo] @CustomerID INT AS SELECT [o].[CustomerID], [o].[OrderDate], [ol].[StockItemID], [ol].[Quantity], [ol].[UnitPrice] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [o].[CustomerID] = @CustomerID ORDER BY [o].[OrderDate] DESC; SELECT [o].[CustomerID], SUM([ol].[Quantity]*[ol].[UnitPrice]) FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID] WHERE [o].[CustomerID] = @CustomerID GROUP BY [o].[CustomerID] ORDER BY [o].[CustomerID] ASC; GO DROP PROCEDURE IF EXISTS [Sales].[usp_GetCustomerData]; GO CREATE PROCEDURE [Sales].[usp_GetCustomerData] @CustomerID INT AS BEGIN SELECT * FROM [Sales].[Customers] EXEC [Sales].[usp_CustomerTransactionInfo] @CustomerID EXEC [Sales].[usp_GetFullCustomerInfo] @CustomerID END GO
Tutaj mamy dwie procedury składowane, usp_TransctionInfo i usp_GetFullCustomerInfo, które są wywoływane przez inną procedurę składowaną, usp_GetCustomerData. Często zdarza się to, a nawet dodatkowe poziomy zagnieżdżenia z procedurami składowanymi. Jeśli wykonamy usp_GetCustomerData z Management Studio, zobaczymy:
EXEC [Sales].[usp_GetCustomerData] 981;

Tutaj wszystkie wykonania wystąpiły na tym samym identyfikatorze GUID, BF54CD8F-08AF-4694-A718-D0C47DBB9593, i możemy zobaczyć kolejność wykonywania zapytania od jednego do ośmiu przy użyciu kolumny seq. W przypadkach, w których wywoływanych jest wiele procedur składowanych, często zdarza się, że wartość identyfikatora sekwencji sięga setek lub tysięcy.
Wreszcie, w przypadku, gdy patrzysz na wykonanie zapytania i uwzględniłeś przyczynę śledzenia, i znajdujesz zapytanie, które ma niską skuteczność – ponieważ posortowałeś dane wyjściowe na podstawie czasu trwania lub jakiegoś innego wskaźnika, pamiętaj, że możesz znaleźć inne zapytania grupując według GUID:

Dane wyjściowe zostały posortowane według czasu trwania (najwyższa wartość zakreślona na czerwono) i dodałem je do zakładek (na fioletowo) za pomocą przycisku Przełącz zakładkę. Jeśli chcemy zobaczyć inne zapytania dotyczące identyfikatora GUID, pogrupuj według identyfikatora GUID (kliknij prawym przyciskiem myszy nazwę kolumny u góry, a następnie wybierz opcję Grupuj według tej kolumny), a następnie użyj przycisku Następna zakładka, aby wrócić do naszego zapytania:

Teraz możemy zobaczyć wszystkie zapytania, które zostały wykonane w ramach tego samego połączenia i w wykonanej kolejności.
Wniosek
Opcja Śledź przyczynowość może być niezwykle przydatna podczas rozwiązywania problemów z wydajnością zapytań i próby zrozumienia kolejności zdarzeń w SQL Server. Jest to również przydatne podczas konfigurowania sesji zdarzenia, która korzysta z celu pair_matching, aby upewnić się, że dopasowujesz właściwe pole/działanie i znajdujesz właściwe niedopasowane zdarzenie. Ponownie, jest to ustawienie na poziomie sesji, więc należy je zastosować przed rozpoczęciem sesji wydarzenia. W przypadku trwającej sesji zatrzymaj sesję zdarzeń, włącz opcję, a następnie uruchom ją ponownie.