J'ai adopté une approche légèrement différente, principalement pour voir comment cette technique se comparerait aux autres, car avoir des options, c'est bien, non?
Le test
Pourquoi ne commençons-nous pas par regarder simplement comment les différentes méthodes se superposent. J'ai fait trois séries de tests:
- Le premier jeu a fonctionné sans modification de base de données
- Le deuxième ensemble s'est exécuté après la création d'un index pour prendre en charge les
TransactionDate
requêtes basées sur Production.TransactionHistory
.
- La troisième série faisait une hypothèse légèrement différente. Étant donné que les trois tests ont été exécutés sur la même liste de produits, si nous avions mis cette liste en cache? Ma méthode utilise un cache en mémoire alors que les autres méthodes utilisaient une table temporaire équivalente. L'index de prise en charge créé pour le deuxième ensemble de tests existe toujours pour cet ensemble de tests.
Détails de test supplémentaires:
- Les tests ont été exécutés
AdventureWorks2012
sur SQL Server 2012, SP2 (Developer Edition).
- Pour chaque test, j'ai indiqué la réponse de la requête et la requête particulière.
- J'ai utilisé l'option "Supprimer les résultats après l'exécution" dans Options de requête | Résultats.
- Veuillez noter que pour les deux premiers ensembles de tests, le résultat
RowCounts
semble être "désactivé" pour ma méthode. Ceci est dû au fait que ma méthode est une implémentation manuelle de ce qui CROSS APPLY
est en train de se faire: elle exécute la requête initiale Production.Product
et récupère 161 lignes, qu’elle utilise ensuite pour les requêtes Production.TransactionHistory
. Par conséquent, les RowCount
valeurs de mes entrées sont toujours 161 supérieures à celles des autres entrées. Dans le troisième ensemble de tests (avec la mise en cache), le nombre de lignes est le même pour toutes les méthodes.
- J'ai utilisé SQL Server Profiler pour capturer les statistiques au lieu de compter sur les plans d'exécution. Aaron et Mikael ont déjà fait un excellent travail en montrant les plans de leurs requêtes et il n’est pas nécessaire de reproduire cette information. Et le but de ma méthode est de réduire les requêtes à une forme si simple que cela n'aurait pas vraiment d'importance. Il existe une raison supplémentaire d'utiliser Profiler, mais nous le mentionnerons plus tard.
- Plutôt que d'utiliser la
Name >= N'M' AND Name < N'S'
construction, j'ai choisi d'utiliser Name LIKE N'[M-R]%'
, et SQL Server les traite de la même manière.
Les resultats
Aucun indice de soutien
Ceci est essentiellement AdventureWorks2012 prêt à l'emploi. Dans tous les cas, ma méthode est clairement meilleure que certaines des autres, mais jamais aussi bonne que les 1 ou 2 meilleures méthodes.
Test 1
Aaron's CTE est clairement le gagnant ici.
Test 2
Le CTE d'Aaron (encore) et la seconde apply row_number()
méthode de Mikael sont proches.
Test 3
Aaron (CTE) (encore) est le gagnant.
Conclusion
Quand il n'y a pas d'indice de support TransactionDate
, ma méthode est meilleure que de faire un standard CROSS APPLY
, mais quand même, utiliser la méthode CTE est clairement la voie à suivre.
Avec index de prise en charge (pas de cache)
Pour cet ensemble de tests, j'ai ajouté l'index évident, TransactionHistory.TransactionDate
car toutes les requêtes sont triées sur ce champ. Je dis "évident" car la plupart des autres réponses sont également d'accord sur ce point. Et comme les requêtes veulent toutes les dates les plus récentes, le TransactionDate
champ doit être commandé DESC
. J'ai donc saisi la CREATE INDEX
déclaration au bas de la réponse de Mikael et ajouté un mot explicite FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Une fois que cet index est en place, les résultats changent un peu.
Test 1
Cette fois, c’est ma méthode qui me précède, du moins en termes de lectures logiques. La CROSS APPLY
méthode, auparavant la moins performante du test 1, gagne sur la durée et bat même la méthode CTE sur les lectures logiques.
Test 2
Cette fois, c'est la première apply row_number()
méthode de Mikael qui a remporté le meilleur prix lors de la lecture de Reads, alors qu'elle était auparavant l'une des moins performantes. Et maintenant, ma méthode arrive à la deuxième place très proche quand on regarde Reads. En fait, en dehors de la méthode CTE, les autres sont assez proches en termes de lectures.
Test 3
Ici, le CTE est toujours le gagnant, mais maintenant la différence entre les autres méthodes est à peine perceptible comparée à la différence radicale qui existait avant la création de l'indice.
Conclusion
L'applicabilité de ma méthode est plus apparente maintenant, bien qu'elle résiste moins bien à la mise en place d'index appropriés.
Avec index de prise en charge ET mise en cache
Pour cet ensemble de tests, j'ai utilisé la mise en cache parce que, pourquoi pas? Ma méthode permet d'utiliser la mise en cache en mémoire à laquelle les autres méthodes ne peuvent pas accéder. Donc, pour être juste, j'ai créé la table temporaire suivante qui a été utilisée à la place de Product.Product
toutes les références de ces autres méthodes au cours des trois tests. Le DaysToManufacture
champ n'est utilisé que dans le test n ° 2, mais il était plus facile d'être cohérent dans les scripts SQL pour utiliser la même table et cela ne faisait pas de mal de l'avoir là.
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
Toutes les méthodes semblent bénéficier également de la mise en cache, et ma méthode est toujours en avance.
Test 2
Ici, nous voyons maintenant une différence dans la gamme puisque ma méthode est à peine avancée, seulement 2 lectures sont meilleures que la première apply row_number()
méthode de Mikael , alors que sans la mise en cache, ma méthode était en retard de 4 lectures.
Test 3
S'il vous plaît voir mise à jour vers le bas (en dessous de la ligne) . Ici encore, nous voyons une différence. La version "paramétrée" de ma méthode est maintenant à peine en avance par 2 lectures par rapport à la méthode CROSS APPLY d'Aaron (sans mise en cache, elles étaient égales). Mais ce qui est vraiment étrange, c’est que, pour la première fois, nous voyons une méthode affectée négativement par la mise en cache: la méthode CTE d’Aaron (qui était auparavant la meilleure pour le test n ° 3). Mais, je ne vais pas prendre le crédit là où ce n’est pas dû, et puisque sans la méthode CTE d’Aaron est toujours plus rapide que ma méthode est ici, la meilleure approche pour cette situation semble être la méthode CTE d’Aaron.
Conclusion S'il vous plaît voir la mise à jour vers le bas (au-dessous de la ligne) Les
situations qui utilisent de manière répétée les résultats d'une requête secondaire peuvent souvent (mais pas toujours) bénéficier de la mise en cache de ces résultats. Mais lorsque la mise en cache est un avantage, l’utilisation de la mémoire pour cette mise en cache présente certains avantages par rapport à l’utilisation de tables temporaires.
La méthode
Généralement
J'ai séparé la requête "en-tête" (c'est-à-dire obtenir le ProductID
s, et dans un cas également le DaysToManufacture
, en Name
partant du début avec certaines lettres) des requêtes "en détail" (c'est-à-dire obtenir les TransactionID
s et TransactionDate
s). Le concept consistait à effectuer des requêtes très simples et à ne pas laisser confondre l'optimiseur lors de leur JOINDRE. Clairement, cela n’est pas toujours avantageux car cela empêche également l’optimiseur d’optimiser. Mais comme nous l'avons vu dans les résultats, cette méthode a ses avantages, selon le type de requête.
La différence entre les différentes saveurs de cette méthode sont:
Constantes: Soumettez toutes les valeurs remplaçables en tant que constantes en ligne au lieu d'être des paramètres. Cela ferait référence ProductID
dans les trois tests ainsi que le nombre de lignes à renvoyer dans le test 2 car il est fonction de "cinq fois l' DaysToManufacture
attribut du produit". Cette sous-méthode signifie que chacun ProductID
aura son propre plan d'exécution, ce qui peut être bénéfique s'il existe une grande variation dans la distribution des données ProductID
. Mais si la distribution des données varie peu, le coût de la génération des plans supplémentaires ne vaudra probablement pas la peine.
Paramétrisé: Soumettez au moins ProductID
le @ProductID
, permettant la mise en cache et la réutilisation du plan d'exécution. Il existe une option de test supplémentaire pour traiter également le nombre variable de lignes à renvoyer pour le test 2 en tant que paramètre.
Optimiser inconnu: lors de la référence en ProductID
tant que @ProductID
, s'il existe une grande variation de la distribution des données, il est possible de mettre en cache un plan qui a un effet négatif sur les autres ProductID
valeurs. Il serait donc bon de savoir si l'utilisation de cette astuce de requête en aide.
Cache Products: Plutôt que d’interroger la Production.Product
table à chaque fois, mais seulement pour obtenir la même liste, lancez la requête une fois (et pendant que nous y sommes, filtrez tous les ProductID
s qui ne sont même pas dans la TransactionHistory
table pour ne pas gaspiller ressources là-bas) et cache cette liste. La liste devrait inclure le DaysToManufacture
champ. Avec cette option, le nombre de tentatives de lecture logique lors de la première exécution est légèrement plus important au départ, mais après cela, seule la TransactionHistory
table est interrogée.
Plus précisément
Ok, mais alors, euh, comment est-il possible d'émettre toutes les sous-requêtes en tant que requêtes séparées sans utiliser un CURSEUR et de vider chaque jeu de résultats dans une table ou une variable de table temporaire? Manifestement, la méthode CURSOR / Temp Table aurait des conséquences manifestes dans les lectures et les écritures. Eh bien, en utilisant SQLCLR :). En créant une procédure stockée SQLCLR, j'ai pu ouvrir un jeu de résultats et essentiellement lui transférer les résultats de chaque sous-requête, sous la forme d'un jeu de résultats continu (et non de plusieurs jeux de résultats). En dehors des informations sur le produit (c'est ProductID
-à- dire Name
, etDaysToManufacture
), aucun des résultats de la sous-requête n'a dû être stocké nulle part (mémoire ou disque) et vient d'être transmis en tant que jeu de résultats principal de la procédure stockée SQLCLR. Cela m'a permis de faire une requête simple pour obtenir les informations sur le produit, puis de les parcourir, en émettant des requêtes très simples TransactionHistory
.
Et c'est pourquoi j'ai dû utiliser SQL Server Profiler pour capturer les statistiques. La procédure stockée SQLCLR n'a pas renvoyé de plan d'exécution, en définissant l'option de requête "Inclure le plan d'exécution réel" ou en émettant SET STATISTICS XML ON;
.
Pour la mise en cache des informations sur le produit, j'ai utilisé une readonly static
liste générique (c'est- _GlobalProducts
à- dire dans le code ci-dessous). Il semble que l'ajout de collections ne viole pas l' readonly
option. Par conséquent, ce code fonctionne lorsque l'assembly a un PERMISSON_SET
de SAFE
:), même si cela est contre-intuitif.
Les requêtes générées
Les requêtes générées par cette procédure stockée SQLCLR sont les suivantes:
Information sur le produit
Test numéros 1 et 3 (pas de mise en cache)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Test numéro 2 (pas de cache)
;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;
Test numéros 1, 2 et 3 (mise en cache)
;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;
Information de transaction
Test N ° 1 et 2 (Constantes)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Numéros de test 1 et 2 (paramétrés)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Numéros de test 1 et 2 (paramétrés + OPTIMISÉ INCONNU)
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 numéro 2 (paramétré les deux)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Test numéro 2 (paramétré les deux + OPTIMIZE UNKNOWN)
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 numéro 3 (constantes)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Test numéro 3 (paramétré)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Test numéro 3 (paramétré + OPTIMISÉ INCONNU)
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));
Le 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);
}
}
}
}
Les requêtes de test
Il n'y a pas assez de place pour poster les tests ici, donc je vais trouver un autre endroit.
La conclusion
Pour certains scénarios, SQLCLR peut être utilisé pour manipuler certains aspects des requêtes qui ne peuvent pas être effectuées dans T-SQL. Et il est possible d'utiliser la mémoire pour la mise en cache au lieu de tables temporaires, bien que cela doive être fait avec parcimonie et prudence, car la mémoire n'est pas automatiquement restituée au système. Cette méthode ne facilite pas non plus les requêtes ad hoc, mais il est possible de la rendre plus flexible que ce que j'ai montré ici simplement en ajoutant des paramètres permettant de personnaliser davantage d'aspects des requêtes en cours d'exécution.
MISE À JOUR
Test supplémentaire
Mes tests d'origine comprenant un index de prise en charge TransactionHistory
utilisaient la définition suivante:
ProductID ASC, TransactionDate DESC
J'avais décidé à l'époque de renoncer, y compris TransactionId DESC
à la fin, en pensant que cela pourrait aider le test n ° 3 (qui spécifie le départage des TransactionId
égalités sur le plus récent - bien, "le plus récent" est supposé puisque non explicitement indiqué, mais tout le monde semble d'accord sur cette hypothèse), il n'y aurait probablement pas assez de liens pour faire une différence.
Mais, ensuite, Aaron a réessayé avec un index de support qui incluait TransactionId DESC
et a trouvé que la CROSS APPLY
méthode était la gagnante des trois tests. Ceci était différent de mes tests qui indiquaient que la méthode CTE était la meilleure pour le test n ° 3 (lorsqu'aucune mise en cache n'est utilisée, ce qui reflète le test d'Aaron). Il était clair qu'il y avait une autre variation à tester.
J'ai supprimé l'index actuel de prise en charge, créé un nouvel index avec TransactionId
et effacé le cache du plan (juste pour être sûr):
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;
J'ai refait le test n ° 1 et les résultats étaient les mêmes, comme prévu. J'ai ensuite refait le test numéro 3 et les résultats ont bien changé:
Les résultats ci-dessus concernent le test standard sans mise en cache. Cette fois, non seulement le temps est CROSS APPLY
battu par le CTE (exactement comme le test d'Aaron l'a indiqué), mais le processus SQLCLR a pris l'avantage par 30 lectures (woo hoo).
Les résultats ci-dessus concernent le test avec la mise en cache activée. Cette fois, la performance du CTE n'est pas dégradée, même si CROSS APPLY
elle bat encore. Cependant, maintenant, le processus SQLCLR prend la tête de 23 lectures (woo hoo, encore).
À emporter
Il existe différentes options à utiliser. Il vaut mieux en essayer plusieurs car ils ont chacun leurs points forts. Les tests effectués ici montrent une assez petite variance dans les lectures et la durée entre les meilleurs et les moins performants parmi tous les tests (avec un indice complémentaire); La variation en lecture est d'environ 350 et la durée est de 55 ms. Bien que le processus SQLCLR ait remporté tous les tests sauf un (en termes de lectures), enregistrer seulement quelques lectures ne vaut généralement pas le coût de maintenance associé à la route SQLCLR. Mais dans AdventureWorks2012, la Product
table ne comporte que 504 lignes et TransactionHistory
ne contient que 113 443 lignes. La différence de performance entre ces méthodes devient probablement plus prononcée à mesure que le nombre de lignes augmente.
Bien que cette question concerne spécifiquement l’obtention d’un ensemble particulier de lignes, il ne faut pas oublier que le facteur le plus important en termes de performances est l’indexation et non le code SQL particulier. Un bon index doit être mis en place avant de déterminer quelle méthode est vraiment la meilleure.
La leçon la plus importante trouvée ici ne concerne pas CROSS APPLY vs CTE vs SQLCLR: il s'agit de TESTING. Ne présume pas. Obtenez des idées de plusieurs personnes et testez autant de scénarios que possible.