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
TransactionDate
zapytań 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
AdventureWorks2012
na 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
RowCounts
wydaje się, że dla mojej metody jest on wyłączony. Wynika to z tego, że moja metoda jest ręczną implementacją tego, co CROSS APPLY
się dzieje: uruchamia zapytanie początkowe Production.Product
i zwraca 161 wierszy z powrotem, które następnie wykorzystuje do zapytań Production.TransactionHistory
. W związku z tym RowCount
wartoś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.TransactionDate
ponieważ 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, TransactionDate
pole należy zamówić DESC
, więc po prostu złapałem CREATE INDEX
oś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 APPLY
Metoda, 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.Product
wszystkich odniesień w tych innych metodach we wszystkich trzech testach. To DaysToManufacture
pole 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 ProductID
s, aw jednym przypadku także DaysToManufacture
, w oparciu o Name
rozpoczęcie od pewnych liter) od zapytań „szczegółowych” (tj. Otrzymujących TransactionID
s i TransactionDate
s). 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 ProductID
wszystkich 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 DaysToManufacture
atrybutu produktu”. Ta pod-metoda oznacza, że każdy ProductID
otrzyma 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 ProductID
jako @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ę ProductID
jako @ProductID
, jeśli istnieje duża różnorodność dystrybucji danych, możliwe jest buforowanie planu, który ma negatywny wpływ na inne ProductID
wartoś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.Product
każ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 TransactionHistory
tabeli, abyśmy nie marnowali zasoby tam) i buforuj tę listę. Lista powinna zawierać DaysToManufacture
pole. Użycie tej opcji powoduje nieco wyższe początkowe trafienie w Odczyty logiczne dla pierwszego wykonania, ale po tym pytana jest tylko TransactionHistory
tabela.
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
, Name
iDaysToManufacture
), ż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 static
Listy ogólnej (tj. _GlobalProducts
W poniższym kodzie). Wydaje się, że dodanie do zbiorów nie narusza readonly
możliwości, stąd ten kod działa, gdy zespół ma PERMISSON_SET
wś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, TransactionHistory
zastosowano następującą definicję:
ProductID ASC, TransactionDate DESC
W tym czasie zdecydowałem się zrezygnować z dołączenia TransactionId DESC
na końcu, stwierdzając, że chociaż może to pomóc w Testie 3 (który określa rozstrzyganie ostatnich - TransactionId
cóż, 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 DESC
i stwierdził, że CROSS APPLY
metoda 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ą TransactionId
i 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 APPLY
pokonano 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 APPLY
nadal 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 Product
tabela ma tylko 504 wiersze i TransactionHistory
ma 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.
APPLY TOP
czyROW_NUMBER()
? Co może być więcej do powiedzenia na ten temat?Krótkie podsumowanie różnic i naprawdę krótko, pokażę tylko plany dla opcji 2 i dodałem indeks
Production.TransactionHistory
.row_number()
Zapytanie :.apply top
Wersja:Główną różnicą między nimi jest to, że
apply top
filtry w górnym wyrażeniu poniżej zagnieżdżonych pętli łączą się, gdyrow_number
wersja filtruje po złączeniu. Oznacza to, że jest więcej odczytówProduction.TransactionHistory
niż jest to naprawdę konieczne.Jeśli istniałby tylko sposób na popchnięcie operatorów odpowiedzialnych za wyliczanie wierszy do dolnej gałęzi przed złączeniem, wówczas
row_number
wersja mogłaby działać lepiej.Więc wpisz
apply row_number()
wersję.Jak widać,
apply row_number()
jest prawie taki sam, jakapply top
tylko nieco bardziej skomplikowany. Czas wykonania jest mniej więcej taki sam lub nieco dłuższy.Dlaczego więc zadałem sobie trud znalezienia odpowiedzi, która nie jest lepsza niż to, co już mamy? Cóż, masz jeszcze jedną rzecz do wypróbowania w prawdziwym świecie, a tak naprawdę jest różnica w odczytach. Taki, dla którego nie mam wyjaśnienia *.
W tym momencie równie dobrze mogę wrzucić drugą
row_number()
wersję, która w niektórych przypadkach może być właściwą drogą. Te niektóre przypadki byłyby wtedy, gdy spodziewasz się, że faktycznie potrzebujesz większości wierszy,Production.TransactionHistory
ponieważ tutaj otrzymujesz połączenie scalająceProduction.Product
i wyliczoneProduction.TransactionHistory
.Aby uzyskać powyższy kształt bez operatora sortowania, należy również zmienić indeks pomocniczy, aby uporządkować,
TransactionDate
malejąco.* Edycja: Dodatkowe logiczne odczyty wynikają z zagnieżdżonego pobierania wstępnego pętli używanego z aplikatorem . Możesz to wyłączyć za pomocą niezaszyfrowanego TF 8744 (i / lub 9115 w późniejszych wersjach), aby uzyskać taką samą liczbę logicznych odczytów. Pobieranie wstępne może być zaletą alternatywnej aplikacji w odpowiednich okolicznościach. - Paul White
źródło
Zazwyczaj używam kombinacji CTE i funkcji okienkowania. Możesz uzyskać tę odpowiedź, używając czegoś takiego:
W przypadku części dodatkowej kredytu, w której różne grupy mogą chcieć zwrócić inną liczbę wierszy, można użyć oddzielnej tabeli. Powiedzmy, używając kryteriów geograficznych, takich jak stan:
Aby to osiągnąć, gdy wartości mogą być różne, musisz dołączyć CTE do tabeli stanu podobnej do tej:
źródło