Przyjąłem nieco inne podejście, głównie po to, aby zobaczyć, jak ta technika mogłaby się porównać do innych, ponieważ posiadanie opcji jest dobre, prawda?
Testowanie
Dlaczego nie zaczniemy od spojrzenia na zestawienie różnych metod. Zrobiłem trzy zestawy testów:
- Pierwszy zestaw działał bez modyfikacji DB
- Drugi zestaw uruchomiono po utworzeniu indeksu do obsługi
TransactionDatezapytań opartych na Production.TransactionHistory.
- Trzeci zestaw przyjął nieco inne założenie. Ponieważ wszystkie trzy testy zostały przeprowadzone na tej samej liście produktów, co zrobić, jeśli będziemy buforować tę listę? Moja metoda używa pamięci podręcznej w pamięci, podczas gdy inne metody używały równoważnej tabeli temp. Indeks pomocniczy utworzony dla drugiego zestawu testów nadal istnieje dla tego zestawu testów.
Dodatkowe szczegóły testu:
- Testy przeprowadzono
AdventureWorks2012na SQL Server 2012, SP2 (edycja dla programistów).
- Dla każdego testu oznaczyłem etykietę, z której odpowiedzi wziąłem zapytanie i które to zapytanie.
- Użyłem opcji „Odrzuć wyniki po wykonaniu” w Opcjach zapytania | Wyniki
- Uwaga: w przypadku pierwszych dwóch zestawów testów
RowCountswydaje się, że dla mojej metody jest on wyłączony. Wynika to z tego, że moja metoda jest ręczną implementacją tego, co CROSS APPLYsię dzieje: uruchamia zapytanie początkowe Production.Producti zwraca 161 wierszy z powrotem, które następnie wykorzystuje do zapytań Production.TransactionHistory. W związku z tym RowCountwartości moich wpisów są zawsze o 161 większe niż innych wpisów. W trzecim zestawie testów (z buforowaniem) liczba wierszy jest taka sama dla wszystkich metod.
- Użyłem SQL Server Profiler do przechwytywania statystyk zamiast polegać na planach wykonania. Aaron i Mikael wykonali już świetną robotę, pokazując plany swoich zapytań i nie ma potrzeby kopiowania tych informacji. Celem mojej metody jest sprowadzenie zapytań do tak prostej formy, że tak naprawdę nie miałoby to znaczenia. Istnieje dodatkowy powód korzystania z Profiler, ale zostanie on wspomniany później.
- Zamiast używać
Name >= N'M' AND Name < N'S'konstrukcji, zdecydowałem się użyć Name LIKE N'[M-R]%', a SQL Server traktuje je tak samo.
Wyniki
Brak indeksu pomocniczego
Jest to zasadniczo gotowe urządzenie AdventureWorks2012. We wszystkich przypadkach moja metoda jest wyraźnie lepsza niż niektóre inne, ale nigdy nie jest tak dobra jak pierwsza lub 2 metody.
Test 1

CTE Aarona jest tutaj wyraźnie zwycięzcą.
Przetestuj 2

CTE Aarona (ponownie), a druga apply row_number()metoda Mikaela jest bliska drugiej.
Test 3

CTE Aarona (ponownie) jest zwycięzcą.
Podsumowanie
Gdy nie ma włączonego indeksu pomocniczego TransactionDate, moja metoda jest lepsza niż robienie standardu CROSS APPLY, ale nadal zdecydowanie korzystam z metody CTE.
Z indeksem wspierającym (bez buforowania)
Do tego zestawu testów dodałem oczywisty indeks, TransactionHistory.TransactionDateponieważ wszystkie zapytania są sortowane na tym polu. Mówię „oczywiste”, ponieważ większość innych odpowiedzi również zgadza się w tej kwestii. A ponieważ wszystkie zapytania wymagają najnowszych dat, TransactionDatepole należy zamówić DESC, więc po prostu złapałem CREATE INDEXoświadczenie u dołu odpowiedzi Mikaela i dodałem wyraźne FILLFACTOR:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Po wprowadzeniu tego indeksu wyniki dość się zmieniają.
Test 1

Tym razem to moja metoda wychodzi na przód, przynajmniej w zakresie odczytów logicznych. CROSS APPLYMetoda, wcześniej najgorszy wykonawca na teście 1, wygrywa na czas trwania, a nawet bije metodę CTE na logicznych odczytów.
Test 2

Tym razem jest to pierwsza apply row_number()metoda Mikaela, która zwyciężyła, patrząc na lektury, podczas gdy wcześniej była to jedna z najgorzej wykonujących. A teraz moja metoda znajduje się na bardzo drugim miejscu, gdy patrzę na Reads. W rzeczywistości, poza metodą CTE, wszystkie pozostałe są dość zbliżone pod względem odczytów.
Test 3

Tutaj CTE jest nadal zwycięzcą, ale teraz różnica między innymi metodami jest ledwo zauważalna w porównaniu z drastyczną różnicą, która istniała przed utworzeniem indeksu.
Wniosek
Zastosowanie mojej metody jest teraz bardziej widoczne, choć jest mniej odporne na brak odpowiednich indeksów.
Z obsługą indeksu i buforowania
Do tego zestawu testów wykorzystałem buforowanie, bo cóż, dlaczego nie? Moja metoda pozwala na użycie buforowania w pamięci, do którego inne metody nie mają dostępu. Żeby być uczciwym, stworzyłem następującą tabelę temp, która była używana zamiast Product.Productwszystkich odniesień w tych innych metodach we wszystkich trzech testach. To DaysToManufacturepole jest używane tylko w teście nr 2, ale łatwiej było zachować spójność we wszystkich skryptach SQL, aby używać tej samej tabeli i nie zaszkodziło, aby było tam.
CREATE TABLE #Products
(
ProductID INT NOT NULL PRIMARY KEY,
Name NVARCHAR(50) NOT NULL,
DaysToManufacture INT NOT NULL
);
INSERT INTO #Products (ProductID, Name, DaysToManufacture)
SELECT p.ProductID, p.Name, p.DaysToManufacture
FROM Production.Product p
WHERE p.Name >= N'M' AND p.Name < N'S'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = p.ProductID
);
ALTER TABLE #Products REBUILD WITH (FILLFACTOR = 100);
Test 1

Wszystkie metody wydają się czerpać równe korzyści z buforowania, a moja metoda wciąż wychodzi na przód.
Test 2

Tutaj widzimy teraz różnicę w składzie, ponieważ moja metoda wychodzi zaledwie na przód, tylko 2 Odczytuje lepiej niż pierwsza apply row_number()metoda Mikaela , podczas gdy bez buforowania moja metoda była opóźniona o 4 Odczyty.
Test 3

Proszę zobaczyć aktualizację w dół (poniżej linii) . Tutaj znów widzimy różnicę. „Sparametryzowany” smak mojej metody jest teraz ledwo na czele o 2 odczyty w porównaniu z metodą CROSS APPLY Aarona (bez buforowania były one równe). Ale naprawdę dziwne jest to, że po raz pierwszy widzimy metodę, na którą buforowanie ma negatywny wpływ: metodę CTE Aarona (która wcześniej była najlepsza dla testu nr 3). Ale nie zamierzam przypisywać sobie uznania, gdy nie jest to należne, a ponieważ bez buforowania metoda CTE Aarona jest wciąż szybsza niż moja metoda tutaj z buforowaniem, najlepszym podejściem w tej konkretnej sytuacji wydaje się być metoda CTE Aarona.
Podsumowanie Proszę zobaczyć aktualizację w dół (poniżej linii)
Sytuacje, w których wielokrotne wykorzystywanie wyników drugiego zapytania może często (ale nie zawsze) skorzystać z buforowania tych wyników. Ale gdy buforowanie jest zaletą, użycie pamięci dla wspomnianego buforowania ma pewną przewagę nad użyciem tabel tymczasowych.
Metoda
Ogólnie
Oddzieliłem zapytanie „nagłówkowe” (tj. Otrzymując ProductIDs, aw jednym przypadku także DaysToManufacture, w oparciu o Namerozpoczęcie od pewnych liter) od zapytań „szczegółowych” (tj. Otrzymujących TransactionIDs i TransactionDates). Założeniem było wykonanie bardzo prostych zapytań i niedopuszczenie do dezorientacji optymalizatora podczas dołączania do nich. Oczywiście nie zawsze jest to korzystne, ponieważ uniemożliwia optymalizatorowi optymalizację. Ale jak widzieliśmy w wynikach, w zależności od rodzaju zapytania, ta metoda ma swoje zalety.
Różnice między różnymi smakami tej metody to:
Stałe: Prześlij dowolne wartości wymienne jako stałe wbudowane zamiast parametrów. Odnosi się to do ProductIDwszystkich trzech testów, a także do liczby wierszy, które należy zwrócić w teście 2, ponieważ jest to funkcja „pięciokrotności DaysToManufactureatrybutu produktu”. Ta pod-metoda oznacza, że każdy ProductIDotrzyma własny plan wykonania, co może być korzystne, jeśli występuje duża różnorodność dystrybucji danych ProductID. Ale jeśli istnieje niewielka zmienność w dystrybucji danych, koszt wygenerowania dodatkowych planów prawdopodobnie nie będzie tego wart.
Sparametryzowane: Prześlij co najmniej ProductIDjako @ProductID, umożliwiając buforowanie planu wykonania i ponowne użycie. Dostępna jest dodatkowa opcja testu, która również traktuje zmienną liczbę wierszy zwracanych do testu 2 jako parametr.
Optymalizuj nieznane: Jeśli odwołujesz się ProductIDjako @ProductID, jeśli istnieje duża różnorodność dystrybucji danych, możliwe jest buforowanie planu, który ma negatywny wpływ na inne ProductIDwartości, więc dobrze byłoby wiedzieć, czy skorzystanie z tej wskazówki zapytania pomoże.
Produkty w pamięci podręcznej: Zamiast za Production.Productkażdym razem sprawdzać tabelę, tylko w celu uzyskania dokładnie tej samej listy, uruchom zapytanie raz (a gdy już nad tym pracujemy, odfiltruj te ProductID, których nawet nie ma w TransactionHistorytabeli, abyśmy nie marnowali zasoby tam) i buforuj tę listę. Lista powinna zawierać DaysToManufacturepole. Użycie tej opcji powoduje nieco wyższe początkowe trafienie w Odczyty logiczne dla pierwszego wykonania, ale po tym pytana jest tylko TransactionHistorytabela.
konkretnie
Ok, ale tak, um, w jaki sposób możliwe jest wydawanie wszystkich pod-zapytań jako osobnych zapytań bez użycia CURSOR i zrzucania każdego zestawu wyników do tymczasowej tabeli lub zmiennej tabeli? Wyraźne wykonanie metody CURSOR / Temp Table odzwierciedlałoby w sposób oczywisty w Odczytach i Zapisach. Cóż, używając SQLCLR :). Tworząc procedurę przechowywaną SQLCLR, byłem w stanie otworzyć zestaw wyników i zasadniczo przesyłać strumieniowo do niego wyniki każdego zapytania podrzędnego jako ciągły zestaw wyników (a nie wiele zestawów wyników). Poza informacjami o produkcie (tj ProductID, NameiDaysToManufacture), żaden z wyników zapytania podrzędnego nie musiał być nigdzie zapisany (pamięć lub dysk) i został po prostu przekazany jako główny zestaw wyników procedury składowanej SQLCLR. Pozwoliło mi to zrobić proste zapytanie, aby uzyskać informacje o produkcie, a następnie przejść przez nie, wysyłając bardzo proste zapytania TransactionHistory.
I dlatego musiałem użyć SQL Server Profiler do przechwytywania statystyk. Procedura przechowywana SQLCLR nie zwróciła planu wykonania ani przez ustawienie opcji zapytania „Uwzględnij rzeczywisty plan wykonania”, ani przez wydanie SET STATISTICS XML ON;.
Do buforowania informacji o produkcie użyłem readonly staticListy ogólnej (tj. _GlobalProductsW poniższym kodzie). Wydaje się, że dodanie do zbiorów nie narusza readonlymożliwości, stąd ten kod działa, gdy zespół ma PERMISSON_SETwśród SAFE:), nawet jeśli jest to sprzeczne z intuicją.
Wygenerowane zapytania
Zapytania wygenerowane przez tę procedurę przechowywaną SQLCLR są następujące:
Informacje o produkcie
Testuj numery 1 i 3 (bez buforowania)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Numer testu 2 (bez buforowania)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
Testuj numery 1, 2 i 3 (buforowanie)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
Informacje o transakcji
Liczby testowe 1 i 2 (stałe)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Numery testowe 1 i 2 (sparametryzowane)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Numery testowe 1 i 2 (sparametryzowane + OPTYMALIZUJ NIEZNANE)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Test nr 2 (sparametryzowane oba)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Test nr 2 (sparametryzowany + OPTYMALIZUJ NIEZNANY)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Numer testu 3 (stałe)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Numer testu 3 (sparametryzowany)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Numer testu 3 (sparametryzowany + OPTYMALIZUJ NIEZNANY)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Kod
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class ObligatoryClassName
{
private class ProductInfo
{
public int ProductID;
public string Name;
public int DaysToManufacture;
public ProductInfo(int ProductID, string Name, int DaysToManufacture)
{
this.ProductID = ProductID;
this.Name = Name;
this.DaysToManufacture = DaysToManufacture;
return;
}
}
private static readonly List<ProductInfo> _GlobalProducts = new List<ProductInfo>();
private static void PopulateGlobalProducts(SqlBoolean PrintQuery)
{
if (_GlobalProducts.Count > 0)
{
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(String.Concat("I already haz ", _GlobalProducts.Count,
" entries :)"));
}
return;
}
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
SqlDataReader _Reader = null;
try
{
_Connection.Open();
_Reader = _Command.ExecuteReader();
while (_Reader.Read())
{
_GlobalProducts.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
}
catch
{
throw;
}
finally
{
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
return;
}
[Microsoft.SqlServer.Server.SqlProcedure]
public static void GetTopRowsPerGroup(SqlByte TestNumber,
SqlByte ParameterizeProductID, SqlBoolean OptimizeForUnknown,
SqlBoolean UseSequentialAccess, SqlBoolean CacheProducts, SqlBoolean PrintQueries)
{
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
List<ProductInfo> _Products = null;
SqlDataReader _Reader = null;
int _RowsToGet = 5; // default value is for Test Number 1
string _OrderByTransactionID = "";
string _OptimizeForUnknown = "";
CommandBehavior _CmdBehavior = CommandBehavior.Default;
if (OptimizeForUnknown.IsTrue)
{
_OptimizeForUnknown = "OPTION (OPTIMIZE FOR (@ProductID UNKNOWN))";
}
if (UseSequentialAccess.IsTrue)
{
_CmdBehavior = CommandBehavior.SequentialAccess;
}
if (CacheProducts.IsTrue)
{
PopulateGlobalProducts(PrintQueries);
}
else
{
_Products = new List<ProductInfo>();
}
if (TestNumber.Value == 2)
{
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
}
else
{
_Command.CommandText = @"
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
";
if (TestNumber.Value == 3)
{
_RowsToGet = 1;
_OrderByTransactionID = ", th.TransactionID DESC";
}
}
try
{
_Connection.Open();
// Populate Product list for this run if not using the Product Cache
if (!CacheProducts.IsTrue)
{
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_Products.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
_Reader.Close();
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
else
{
_Products = _GlobalProducts;
}
SqlDataRecord _ResultRow = new SqlDataRecord(
new SqlMetaData[]{
new SqlMetaData("ProductID", SqlDbType.Int),
new SqlMetaData("Name", SqlDbType.NVarChar, 50),
new SqlMetaData("TransactionID", SqlDbType.Int),
new SqlMetaData("TransactionDate", SqlDbType.DateTime)
});
SqlParameter _ProductID = new SqlParameter("@ProductID", SqlDbType.Int);
_Command.Parameters.Add(_ProductID);
SqlParameter _RowsToReturn = new SqlParameter("@RowsToReturn", SqlDbType.Int);
_Command.Parameters.Add(_RowsToReturn);
SqlContext.Pipe.SendResultsStart(_ResultRow);
for (int _Row = 0; _Row < _Products.Count; _Row++)
{
// Tests 1 and 3 use previously set static values for _RowsToGet
if (TestNumber.Value == 2)
{
if (_Products[_Row].DaysToManufacture == 0)
{
continue; // no use in issuing SELECT TOP (0) query
}
_RowsToGet = (5 * _Products[_Row].DaysToManufacture);
}
_ResultRow.SetInt32(0, _Products[_Row].ProductID);
_ResultRow.SetString(1, _Products[_Row].Name);
switch (ParameterizeProductID.Value)
{
case 0x01:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC{2}
{1};
", _RowsToGet, _OptimizeForUnknown, _OrderByTransactionID);
_ProductID.Value = _Products[_Row].ProductID;
break;
case 0x02:
_Command.CommandText = String.Format(@"
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
{0};
", _OptimizeForUnknown);
_ProductID.Value = _Products[_Row].ProductID;
_RowsToReturn.Value = _RowsToGet;
break;
default:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = {1}
ORDER BY th.TransactionDate DESC{2};
", _RowsToGet, _Products[_Row].ProductID, _OrderByTransactionID);
break;
}
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_ResultRow.SetInt32(2, _Reader.GetInt32(0));
_ResultRow.SetDateTime(3, _Reader.GetDateTime(1));
SqlContext.Pipe.SendResultsRow(_ResultRow);
}
_Reader.Close();
}
}
catch
{
throw;
}
finally
{
if (SqlContext.Pipe.IsSendingResults)
{
SqlContext.Pipe.SendResultsEnd();
}
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
}
}
Zapytania testowe
Nie ma wystarczająco dużo miejsca, aby opublikować tutaj testy, więc znajdę inną lokalizację.
Konkluzja
W niektórych scenariuszach można użyć SQLCLR do manipulowania niektórymi aspektami zapytań, których nie można wykonać w języku T-SQL. Istnieje również możliwość użycia pamięci do buforowania zamiast tabel tymczasowych, choć należy to robić oszczędnie i ostrożnie, ponieważ pamięć nie jest automatycznie zwalniana z powrotem do systemu. Ta metoda również nie pomaga w zapytaniach ad hoc, ale można ją uczynić bardziej elastyczną, niż pokazałem tutaj, po prostu dodając parametry, aby dostosować więcej aspektów wykonywanych zapytań.
AKTUALIZACJA
Test dodatkowy W
moich oryginalnych testach, które zawierały indeks pomocniczy, TransactionHistoryzastosowano następującą definicję:
ProductID ASC, TransactionDate DESC
W tym czasie zdecydowałem się zrezygnować z dołączenia TransactionId DESCna końcu, stwierdzając, że chociaż może to pomóc w Testie 3 (który określa rozstrzyganie ostatnich - TransactionIdcóż, zakłada się, że „najnowszy” nie jest wyraźnie określony, ale wszyscy wydają się zgodzić się z tym założeniem), prawdopodobnie nie będzie wystarczającej liczby więzi, aby coś zmienić.
Ale potem Aaron ponownie przetestował z dodatkowym indeksem, który zawierał TransactionId DESCi stwierdził, że CROSS APPLYmetoda była zwycięska we wszystkich trzech testach. Różniło się to od moich testów, które wskazały, że metoda CTE była najlepsza dla testu nr 3 (gdy nie użyto buforowania, co odzwierciedla test Aarona). Było jasne, że istniała dodatkowa odmiana, którą należało przetestować.
Usunąłem bieżący indeks pomocniczy, utworzyłem nowy za pomocą TransactionIdi wyczyściłem pamięć podręczną planu (dla pewności):
DROP INDEX [IX_TransactionHistoryX] ON Production.TransactionHistory;
CREATE UNIQUE INDEX [UIX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC, TransactionID DESC)
WITH (FILLFACTOR = 100);
DBCC FREEPROCCACHE WITH NO_INFOMSGS;
Ponownie uruchomiłem Test numer 1 i wyniki były takie same, jak oczekiwano. Następnie ponownie uruchomiłem Test Numer 3 i wyniki rzeczywiście się zmieniły:

Powyższe wyniki dotyczą standardowego testu bez buforowania. Tym razem nie tylko CROSS APPLYpokonano CTE (tak jak wskazał test Aarona), ale proc SQLCLR przejął prowadzenie o 30 odczytów (woo hoo).

Powyższe wyniki dotyczą testu z włączonym buforowaniem. Tym razem wydajność CTE nie ulega pogorszeniu, choć CROSS APPLYnadal go bije. Jednak teraz SQLCLR proc przejmuje prowadzenie przez 23 Odczyty (znowu woo hoo).
Odejdź
Istnieją różne opcje do użycia. Najlepiej wypróbować kilka, ponieważ każda z nich ma swoje mocne strony. Wykonane tutaj testy wykazują raczej niewielką wariancję zarówno odczytów, jak i czasu trwania między najlepszymi i najgorszymi wynikami we wszystkich testach (z indeksem wspierającym); różnica w odczytach wynosi około 350, a czas trwania wynosi 55 ms. Chociaż proces SQLCLR wygrał we wszystkich testach oprócz 1 (pod względem liczby odczytów), zapisanie tylko kilku odczytów zazwyczaj nie jest warte kosztów utrzymania trasy SQLCLR. Ale w AdventureWorks2012 Producttabela ma tylko 504 wiersze i TransactionHistoryma tylko 113.443 wiersze. Różnica w wydajności tych metod prawdopodobnie staje się wyraźniejsza wraz ze wzrostem liczby wierszy.
Chociaż pytanie to dotyczyło konkretnego zestawu wierszy, nie należy zapominać, że najważniejszym czynnikiem wpływającym na wydajność było indeksowanie, a nie konkretny SQL. Dobry indeks musi być na miejscu przed ustaleniem, która metoda jest naprawdę najlepsza.
Najważniejsza lekcja tutaj nie dotyczy CROSS APPLY vs CTE vs SQLCLR: chodzi o TESTOWANIE. Nie zakładaj. Zdobądź pomysły od kilku osób i przetestuj jak najwięcej scenariuszy.