Ich habe einen etwas anderen Ansatz gewählt, hauptsächlich um zu sehen, wie sich diese Technik mit den anderen vergleichen lässt, denn Optionen zu haben ist gut, oder?
Die Prüfung
Warum fangen wir nicht damit an, uns nur anzuschauen, wie sich die verschiedenen Methoden gegenüberstehen. Ich habe drei Sätze von Tests gemacht:
- Der erste Satz wurde ohne DB-Änderungen ausgeführt
- Der zweite Satz wurde ausgeführt, nachdem ein Index erstellt wurde, um
TransactionDate
Abfragen zu unterstützen, die auf der Unterstützung basieren Production.TransactionHistory
.
- Der dritte Satz ging von einer etwas anderen Annahme aus. Was passiert, wenn wir diese Liste zwischengespeichert haben, da alle drei Tests mit derselben Produktliste ausgeführt wurden? Meine Methode verwendet einen speicherinternen Cache, während die anderen Methoden eine entsprechende temporäre Tabelle verwendeten. Der für die zweite Gruppe von Tests erstellte unterstützende Index ist für diese Gruppe von Tests noch vorhanden.
Zusätzliche Testdetails:
- Die Tests wurden
AdventureWorks2012
unter SQL Server 2012, SP2 (Developer Edition) ausgeführt.
- Für jeden Test habe ich angegeben, von wem ich die Abfrage genommen habe und welche bestimmte Abfrage es war.
- Ich habe die Option "Ergebnisse nach Ausführung verwerfen" unter Abfrageoptionen | verwendet Ergebnisse.
- Bitte beachten Sie, dass für die ersten beiden Testreihen die
RowCounts
für meine Methode "aus" zu sein scheinen. Dies ist darauf zurückzuführen, dass meine Methode eine manuelle Implementierung des aktuellen CROSS APPLY
Vorgangs ist: Sie führt die erste Abfrage aus Production.Product
und ruft 161 Zeilen zurück, die dann für die Abfragen verwendet werden Production.TransactionHistory
. Daher sind die RowCount
Werte für meine Einträge immer 161 höher als für die anderen Einträge. In der dritten Testreihe (mit Caching) sind die Zeilenzahlen für alle Methoden gleich.
- Ich habe SQL Server Profiler verwendet, um die Statistiken zu erfassen, anstatt mich auf die Ausführungspläne zu verlassen. Aaron und Mikael haben bereits großartige Arbeit geleistet, um die Pläne für ihre Abfragen zu zeigen, und es besteht keine Notwendigkeit, diese Informationen zu reproduzieren. Und die Absicht meiner Methode ist es, die Abfragen auf eine so einfache Form zu reduzieren, dass es nicht wirklich wichtig wäre. Es gibt einen weiteren Grund für die Verwendung von Profiler, der jedoch später erwähnt wird.
- Anstatt das
Name >= N'M' AND Name < N'S'
Konstrukt zu verwenden, habe ich es ausgewählt Name LIKE N'[M-R]%'
und SQL Server behandelt sie gleich.
Die Ergebnisse
Kein unterstützender Index
Dies ist im Wesentlichen ein sofort einsatzbereites AdventureWorks2012. In allen Fällen ist meine Methode eindeutig besser als die der anderen, aber niemals so gut wie die der ersten oder zweiten Methoden.
Test 1
Aarons CTE ist hier eindeutig der Gewinner.
Test 2
Aarons CTE (wieder) und Mikaels zweite apply row_number()
Methode ist eine knappe Sekunde.
Test 3
Aarons CTE (wieder) ist der Gewinner.
Fazit
Wenn es keinen unterstützenden Index gibt, ist TransactionDate
meine Methode besser als ein Standard CROSS APPLY
, aber dennoch ist die Verwendung der CTE-Methode eindeutig der richtige Weg.
Mit unterstützendem Index (kein Caching)
Für diese Tests habe ich den offensichtlichen Index für hinzugefügt, TransactionHistory.TransactionDate
da alle Abfragen in diesem Feld sortiert werden. Ich sage "offensichtlich", da die meisten anderen Antworten in diesem Punkt ebenfalls übereinstimmen. Und da für alle Abfragen die neuesten Daten gewünscht werden, sollte das TransactionDate
Feld sortiert DESC
werden. Deshalb habe ich einfach die CREATE INDEX
Erklärung am Ende von Mikaels Antwort abgerufen und eine explizite hinzugefügt FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Sobald dieser Index vorhanden ist, ändern sich die Ergebnisse erheblich.
Test 1
Dieses Mal ist es meine Methode, die zumindest in Bezug auf logische Lesevorgänge die Nase vorn hat. Die CROSS APPLY
Methode, die zuvor die schlechteste Leistung für Test 1 erbrachte, gewinnt bei Duration und übertrifft sogar die CTE-Methode bei logischen Lesevorgängen.
Test 2
Diesmal ist es Mikaels erste apply row_number()
Methode, die bei Reads als Sieger hervorgeht, während sie zuvor zu den schlechtesten gehört hat. Und jetzt kommt meine Methode beim Lesen auf einen sehr knappen zweiten Platz. Außerhalb der CTE-Methode liegen die restlichen Werte in Bezug auf die Lesezugriffe ziemlich nahe beieinander.
Test 3
Hier ist der CTE immer noch der Gewinner, aber jetzt ist der Unterschied zwischen den anderen Methoden kaum spürbar im Vergleich zu dem drastischen Unterschied, der vor der Erstellung des Index bestand.
Fazit
Die Anwendbarkeit meiner Methode ist jetzt offensichtlicher, obwohl sie weniger widerstandsfähig ist, wenn keine richtigen Indizes vorhanden sind.
Mit unterstützendem Index UND Caching
Für diese Tests habe ich Caching verwendet, weil, na ja, warum nicht? Meine Methode ermöglicht die Verwendung von In-Memory-Caching, auf das die anderen Methoden nicht zugreifen können. Um fair zu sein, habe ich die folgende temporäre Tabelle erstellt, die Product.Product
für alle Referenzen in diesen anderen Methoden in allen drei Tests verwendet wurde. Das DaysToManufacture
Feld wird nur in Test Nummer 2 verwendet. Es war jedoch einfacher, über alle SQL-Skripte hinweg konsistent zu sein, um dieselbe Tabelle zu verwenden, und es tat nicht weh, sie dort zu haben.
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
Alle Methoden scheinen gleichermaßen vom Caching zu profitieren, und meine Methode hat noch immer die Nase vorn.
Test 2
Hier sehen wir jetzt einen Unterschied in der Aufstellung, da meine Methode kaum vorne rauskommt, nur 2 Lesevorgänge besser als Mikaels erste apply row_number()
Methode, während meine Methode ohne Caching um 4 Lesevorgänge zurückblieb.
Test 3
Siehe Update unten (unter der Linie) . Hier sehen wir wieder einen Unterschied. Die "parametrisierte" Variante meiner Methode ist jetzt mit 2 Lesevorgängen kaum noch führend im Vergleich zu Aarons CROSS APPLY-Methode (ohne Zwischenspeicherung waren sie gleich). Aber das wirklich Seltsame ist, dass wir zum ersten Mal eine Methode sehen, die durch das Caching negativ beeinflusst wird: Aarons CTE-Methode (die zuvor die beste für Test Nummer 3 war). Aber ich nehme keine Gutschrift an, wenn sie nicht fällig ist, und da die CTE-Methode von Aaron ohne Caching immer noch schneller ist als meine Methode mit Caching, scheint der beste Ansatz für diese spezielle Situation die CTE-Methode von Aaron zu sein.
Schlussfolgerung Siehe Aktualisierung unten (unter der Zeile)
Situationen, in denen die Ergebnisse einer sekundären Abfrage wiederholt verwendet werden, können häufig (aber nicht immer) davon profitieren, dass diese Ergebnisse zwischengespeichert werden. Wenn das Zwischenspeichern ein Vorteil ist, hat die Verwendung von Speicher für das Zwischenspeichern einen gewissen Vorteil gegenüber der Verwendung von temporären Tabellen.
Die Methode
Allgemein
Ich habe die "Header" -Abfrage (dh das Erhalten der ProductID
s, und in einem Fall auch der DaysToManufacture
, basierend auf dem Name
Beginnen mit bestimmten Buchstaben) von den "Detail" -Abfragen (dh das Erhalten der TransactionID
s und TransactionDate
s) getrennt. Das Konzept bestand darin, sehr einfache Abfragen durchzuführen und zu verhindern, dass der Optimierer beim Beitreten verwirrt wird. Dies ist natürlich nicht immer von Vorteil, da es dem Optimierer auch verbietet, zu optimieren. Wie wir jedoch in den Ergebnissen gesehen haben, hat diese Methode je nach Art der Abfrage ihre Vorzüge.
Der Unterschied zwischen den verschiedenen Geschmacksrichtungen dieser Methode ist:
Konstanten: Übergeben Sie alle ersetzbaren Werte als Inline-Konstanten, anstatt Parameter zu sein. Dies würde sich auf ProductID
alle drei Tests und auch auf die Anzahl der Zeilen beziehen, die in Test 2 zurückgegeben werden sollen, da dies eine Funktion des "fünffachen DaysToManufacture
Produktattributs" ist. Diese Untermethode bedeutet, dass jeder ProductID
seinen eigenen Ausführungsplan erhält, was von Vorteil sein kann, wenn es bei der Datenverteilung große Unterschiede gibt ProductID
. Wenn sich die Datenverteilung jedoch nur geringfügig ändert, lohnen sich die Kosten für die Erstellung der zusätzlichen Pläne wahrscheinlich nicht.
Parametrisiert: Übermitteln Sie mindestens ProductID
as @ProductID
, um Zwischenspeicherung und Wiederverwendung des Ausführungsplans zu ermöglichen. Es gibt eine zusätzliche Testoption, mit der auch die variable Anzahl der Zeilen, die für Test 2 zurückgegeben werden sollen, als Parameter behandelt werden kann.
Zu Optimieren Unknown: Bei einem Verweise auf ProductID
wie @ProductID
, wenn es große Unterschiede der Datenverteilung ist , dann ist es möglich , einen Plan cachen , die eine negative Wirkung auf andere haben ProductID
Werten , so dass es gut wäre , wenn die Nutzung dieses Abfragehinweises zu wissen , hilft jeder.
Cache-Produkte: Anstatt die Production.Product
Tabelle jedes Mal abzufragen, nur um genau dieselbe Liste zu erhalten, führen Sie die Abfrage einmal aus (und filtern Sie, während wir gerade dabei sind, alle ProductID
s aus, die nicht einmal in der TransactionHistory
Tabelle sind, damit wir keine verschwenden Ressourcen dort) und zwischenspeichern diese Liste. Die Liste sollte das DaysToManufacture
Feld enthalten. Bei Verwendung dieser Option wird bei logischen Lesevorgängen bei der ersten Ausführung ein geringfügig höherer Treffer erzielt, danach wird jedoch nur die TransactionHistory
Tabelle abgefragt.
Speziell
Ok, aber wie ist es möglich, alle Unterabfragen als separate Abfragen abzusetzen, ohne einen CURSOR zu verwenden und jede Ergebnismenge auf eine temporäre Tabelle oder Tabellenvariable zu sichern? Wenn Sie die CURSOR / Temp-Tabellenmethode verwenden, wird dies in den Lese- und Schreibvorgängen ganz offensichtlich wiedergegeben. Nun, mit SQLCLR :). Durch das Erstellen einer gespeicherten SQLCLR-Prozedur konnte ich eine Ergebnismenge öffnen und die Ergebnisse jeder Unterabfrage als fortlaufende Ergebnismenge (und nicht als mehrere Ergebnismengen) an diese streamen. Außerhalb der Produktinformation (dh ProductID
, Name
undDaysToManufacture
), musste keines der Unterabfrageergebnisse irgendwo gespeichert werden (Speicher oder Datenträger) und wurde nur als Hauptergebnismenge der gespeicherten Prozedur SQLCLR durchgereicht. Auf diese Weise konnte ich eine einfache Abfrage durchführen, um die Produktinformationen abzurufen, und diese dann durchlaufen, wobei sehr einfache Abfragen für ausgegeben wurden TransactionHistory
.
Aus diesem Grund musste ich SQL Server Profiler verwenden, um die Statistiken zu erfassen. Die gespeicherte SQLCLR-Prozedur hat keinen Ausführungsplan zurückgegeben, entweder durch Festlegen der Abfrageoption "Aktuellen Ausführungsplan einschließen" oder durch Ausgabe SET STATISTICS XML ON;
.
Für das Zwischenspeichern von Produktinformationen habe ich eine readonly static
allgemeine Liste verwendet (dh _GlobalProducts
im folgenden Code). Es scheint, dass das Hinzufügen zu Sammlungen nicht die readonly
Option verletzt , daher funktioniert dieser Code, wenn die Assembly ein PERMISSON_SET
von SAFE
:) hat, auch wenn dies nicht intuitiv ist.
Die generierten Abfragen
Die von dieser gespeicherten SQLCLR-Prozedur erstellten Abfragen lauten wie folgt:
Produktinformation
Testnummern 1 und 3 (kein Caching)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Test Nummer 2 (kein Caching)
;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;
Testnummern 1, 2 und 3 (Caching)
;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;
Transaktionsinfo
Testnummern 1 und 2 (Konstanten)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Testnummern 1 und 2 (parametriert)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Testnummern 1 und 2 (Parametriert + UNBEKANNT OPTIMIEREN)
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 Nummer 2 (beide parametrisiert)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Test Nummer 2 (beide parametrisiert + UNBEKANNT OPTIMIEREN)
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));
Test Nummer 3 (Konstanten)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Test Nummer 3 (parametrisiert)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Test Nummer 3 (Parametriert + UNBEKANNT OPTIMIEREN)
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));
Der Code
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);
}
}
}
}
Die Testabfragen
Es ist nicht genügend Platz vorhanden, um die Tests hier zu veröffentlichen, sodass ich einen anderen Ort finden kann.
Das Fazit
In bestimmten Szenarien kann SQLCLR verwendet werden, um bestimmte Aspekte von Abfragen zu bearbeiten, die in T-SQL nicht ausgeführt werden können. Und es gibt die Möglichkeit, Speicher zum Zwischenspeichern anstelle von temporären Tabellen zu verwenden. Dies sollte jedoch sparsam und vorsichtig erfolgen, da der Speicher nicht automatisch an das System zurückgegeben wird. Diese Methode hilft auch nicht bei Ad-hoc-Abfragen, obwohl sie flexibler gestaltet werden kann, als ich hier gezeigt habe, indem einfach Parameter hinzugefügt werden, um mehr Aspekte der ausgeführten Abfragen anzupassen.
AKTUALISIEREN
Zusätzlicher Test
Meine ursprünglichen Tests, die einen unterstützenden Index für enthielten, TransactionHistory
verwendeten die folgende Definition:
ProductID ASC, TransactionDate DESC
Ich hatte damals beschlossen, auf das Einschließen TransactionId DESC
am Ende zu verzichten , um herauszufinden, dass Test Nummer 3 zwar hilfreich sein könnte (der das Aufbrechen des letzten TransactionId
Tests angibt - nun, es wird davon ausgegangen, dass "das Neueste" vorliegt, da dies nicht ausdrücklich angegeben ist, aber es scheint, dass alle dies tun Um dieser Annahme zuzustimmen, würde es wahrscheinlich nicht genug Bindungen geben, um einen Unterschied zu machen.
Doch dann testete Aaron erneut mit einem unterstützenden Index, der dies beinhaltete, TransactionId DESC
und stellte fest, dass die CROSS APPLY
Methode in allen drei Tests der Gewinner war. Dies war ein Unterschied zu meinen Tests, die zeigten, dass die CTE-Methode für Test Nummer 3 am besten war (wenn kein Caching verwendet wurde, was Aarons Test widerspiegelt). Es war klar, dass es eine zusätzliche Variation gab, die getestet werden musste.
Ich habe den aktuellen unterstützenden Index entfernt, einen neuen mit erstellt TransactionId
und den Plan-Cache geleert (nur um sicherzugehen):
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;
Ich habe Test Nummer 1 erneut durchgeführt und die Ergebnisse waren erwartungsgemäß dieselben. Ich habe dann Test Nummer 3 erneut ausgeführt und die Ergebnisse haben sich tatsächlich geändert:
Die obigen Ergebnisse gelten für den Standardtest ohne Zwischenspeicherung. Dieses Mal hat nicht nur CROSS APPLY
der CTE die Nase vorn (wie Aarons Test gezeigt hat), sondern auch der SQLCLR-Proc übernahm die Führung mit 30 Reads (woo hoo).
Die obigen Ergebnisse gelten für den Test mit aktiviertem Caching. Diesmal ist die Leistung des CTE nicht beeinträchtigt, obwohl das CROSS APPLY
immer noch übertrifft. Jetzt übernimmt der SQLCLR-Prozess die Führung mit 23 Reads (wieder woo hoo).
Take Aways
Es gibt verschiedene Möglichkeiten. Am besten probieren Sie mehrere aus, da sie alle ihre Stärken haben. Die hier durchgeführten Tests zeigen eine relativ geringe Varianz in Bezug auf Lesevorgänge und Dauer zwischen den besten und schlechtesten Performern über alle Tests hinweg (mit einem unterstützenden Index). Die Abweichung bei den Lesevorgängen beträgt ungefähr 350 und die Dauer beträgt 55 ms. Während der SQLCLR-Prozess in allen Tests außer 1 (in Bezug auf Lesevorgänge) gewonnen hat, sind das Speichern einiger Lesevorgänge in der Regel nicht die Wartungskosten wert, die für den SQLCLR-Test anfallen. In AdventureWorks2012 hat die Product
Tabelle jedoch nur 504 Zeilen und TransactionHistory
nur 113.443 Zeilen. Der Leistungsunterschied zwischen diesen Methoden wird wahrscheinlich größer, wenn die Zeilenanzahl zunimmt.
Diese Frage war zwar spezifisch für das Abrufen eines bestimmten Satzes von Zeilen, es sollte jedoch nicht übersehen werden, dass der einzige Hauptfaktor für die Leistung die Indizierung und nicht das bestimmte SQL war. Ein guter Index muss vorhanden sein, bevor die wirklich beste Methode ermittelt werden kann.
Die wichtigste Lektion, die hier zu finden ist, handelt nicht von CROSS APPLY vs CTE vs SQLCLR, sondern von TESTING. Geh nicht davon aus Holen Sie sich Ideen von mehreren Personen und testen Sie so viele Szenarien wie möglich.
APPLY TOP
oderROW_NUMBER()
? Was kann man noch dazu sagen?Eine kurze Zusammenfassung der Unterschiede und um es wirklich kurz zu halten, werde ich nur die Pläne für Option 2 zeigen und ich habe den Index für hinzugefügt
Production.TransactionHistory
.Die
row_number()
Abfrage:Die
apply top
Version:Der Hauptunterschied zwischen diesen besteht darin, dass
apply top
Filter im oberen Ausdruck unterhalb des Joins mit verschachtelten Schleifen verwendet werden, bei denen dierow_number
Version nach dem Join gefiltert wird. Das bedeutet, dass mehr Lesevorgänge vorhanden sind,Production.TransactionHistory
als wirklich erforderlich sind.Wenn es nur eine Möglichkeit gäbe, die Operatoren, die für die Aufzählung der Zeilen verantwortlich sind, vor dem Join in den unteren Zweig zu verschieben, wäre die
row_number
Version möglicherweise besser.Also
apply row_number()
Version eingeben .Wie Sie sehen,
apply row_number()
ist es so ziemlich das Gleiche wieapply top
nur etwas komplizierter. Die Ausführungszeit ist ungefähr gleich oder etwas langsamer.Warum habe ich mir dann die Mühe gemacht, eine Antwort zu finden, die nicht besser ist als die, die wir bereits haben? Nun, Sie müssen noch eine Sache in der realen Welt ausprobieren, und es gibt tatsächlich einen Unterschied bei den Lesarten. Eine, für die ich keine Erklärung habe.
Während ich dabei bin, könnte ich auch eine zweite
row_number()
Version einwerfen , die in bestimmten Fällen der richtige Weg sein könnte. In bestimmten Fällen müssen Sie davon ausgehen, dass Sie die meisten Zeilen benötigen,Production.TransactionHistory
da hier eine Zusammenführungsverknüpfung zwischenProduction.Product
und der Aufzählung erfolgtProduction.TransactionHistory
.Um die obige Form ohne einen Sortieroperator zu erhalten, müssen Sie auch den Hilfsindex in
TransactionDate
absteigender Reihenfolge ändern .* Bearbeiten: Die zusätzlichen logischen Lesevorgänge sind auf das Prefetching von verschachtelten Schleifen zurückzuführen, das mit dem Apply-Top verwendet wird. Sie können dies mit undoc'd TF 8744 (und / oder 9115 in späteren Versionen) deaktivieren, um die gleiche Anzahl logischer Lesevorgänge zu erhalten. Das Vorabrufen könnte unter den richtigen Umständen ein Vorteil der Option "Top anwenden" sein. - Paul White
quelle
Normalerweise verwende ich eine Kombination aus CTEs und Fensterfunktionen. Sie können diese Antwort folgendermaßen erreichen:
Für den zusätzlichen Kreditanteil, bei dem verschiedene Gruppen möglicherweise eine unterschiedliche Anzahl von Zeilen zurückgeben möchten, können Sie eine separate Tabelle verwenden. Sagen wir mit geografischen Kriterien wie Zustand:
Um dies zu erreichen, wenn die Werte unterschiedlich sein können, müssen Sie Ihren CTE mit der Statustabelle wie folgt verknüpfen:
quelle