Ho adottato un approccio leggermente diverso, principalmente per vedere come questa tecnica sarebbe paragonabile alle altre, perché avere opzioni è buono, giusto?
Il test
Perché non iniziamo solo osservando come i vari metodi si sono sovrapposti. Ho fatto tre serie di test:
- Il primo set è stato eseguito senza modifiche al DB
- Il secondo set è stato eseguito dopo la creazione di un indice per supportare le
TransactionDate
query basate su Production.TransactionHistory
.
- Il terzo set ha assunto un'ipotesi leggermente diversa. Dal momento che tutti e tre i test sono stati eseguiti sullo stesso elenco di prodotti, cosa accadrebbe se memorizzassimo tale elenco nella cache? Il mio metodo utilizza una cache in memoria mentre gli altri metodi utilizzavano una tabella temporanea equivalente. L'indice di supporto creato per il secondo set di test esiste ancora per questo set di test.
Ulteriori dettagli del test:
- I test sono stati eseguiti
AdventureWorks2012
su SQL Server 2012, SP2 (Developer Edition).
- Per ogni test ho etichettato la cui risposta ho preso la query e quale particolare query era.
- Ho usato l'opzione "Elimina risultati dopo l'esecuzione" di Opzioni query | Risultati.
- Si noti che per le prime due serie di test,
RowCounts
sembra che sia "off" per il mio metodo. Ciò è dovuto al fatto che il mio metodo è un'implementazione manuale di ciò che CROSS APPLY
sta facendo: esegue la query iniziale Production.Product
e ottiene 161 righe indietro, che quindi utilizza per le query a fronte Production.TransactionHistory
. Quindi, i RowCount
valori per le mie voci sono sempre 161 in più rispetto alle altre voci. Nella terza serie di test (con memorizzazione nella cache) i conteggi delle righe sono gli stessi per tutti i metodi.
- Ho usato SQL Server Profiler per acquisire le statistiche invece di fare affidamento sui piani di esecuzione. Aaron e Mikael hanno già fatto un ottimo lavoro mostrando i piani per le loro domande e non è necessario riprodurre tali informazioni. E l'intento del mio metodo è quello di ridurre le domande in una forma così semplice che non avrebbe davvero importanza. C'è un motivo in più per usare Profiler, ma che verrà menzionato più avanti.
- Piuttosto che usare il
Name >= N'M' AND Name < N'S'
costrutto, ho scelto di usare Name LIKE N'[M-R]%'
, e SQL Server li tratta allo stesso modo.
I risultati
Nessun indice di supporto
Questo è essenzialmente AdventureWorks2012 pronto all'uso. In tutti i casi il mio metodo è chiaramente migliore di alcuni degli altri, ma mai buono come i primi 1 o 2 metodi.
Test 1
Il CTE di Aaron è chiaramente il vincitore qui.
Prova 2
Il CTE di Aaron (di nuovo) e il secondo apply row_number()
metodo di Mikael è un secondo vicino.
Test 3
Il CTE di Aaron (di nuovo) è il vincitore.
Conclusione
Quando non c'è un indice di supporto attivo TransactionDate
, il mio metodo è meglio che fare uno standard CROSS APPLY
, ma comunque, usare il metodo CTE è chiaramente la strada da percorrere.
Con indice di supporto (nessuna memorizzazione nella cache)
Per questa serie di test ho aggiunto l'indice ovvio TransactionHistory.TransactionDate
dato che tutte le query sono ordinate su quel campo. Dico "ovvio" poiché la maggior parte delle altre risposte concordano anche su questo punto. E poiché le query richiedono tutte le date più recenti, il TransactionDate
campo dovrebbe essere ordinato DESC
, quindi ho appena preso l' CREATE INDEX
affermazione in fondo alla risposta di Mikael e ho aggiunto un esplicito FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Una volta che questo indice è a posto, i risultati cambiano un po '.
Test 1
Questa volta è il mio metodo che viene fuori, almeno in termini di Letture logiche. Il CROSS APPLY
metodo, in precedenza il peggiore per il Test 1, vince su Durata e batte persino il metodo CTE su Letture logiche.
Test 2
Questa volta è il primo apply row_number()
metodo di Mikael ad essere il vincitore quando guarda Reads, mentre in precedenza era uno dei peggiori. E ora il mio metodo arriva ad un secondo posto molto vicino quando si guarda a Reads. In effetti, al di fuori del metodo CTE, il resto è abbastanza vicino in termini di letture.
Test 3
Qui il CTE è ancora il vincitore, ma ora la differenza tra gli altri metodi è appena percettibile rispetto alla drastica differenza esistente prima della creazione dell'indice.
Conclusione
L'applicabilità del mio metodo è più evidente ora, anche se è meno resistente a non disporre di indici adeguati.
Con indice di supporto e memorizzazione nella cache
Per questa serie di test ho usato la cache perché, beh, perché no? Il mio metodo consente di utilizzare la memorizzazione nella cache a cui gli altri metodi non possono accedere. Quindi, per essere onesti, ho creato la seguente tabella temporanea che è stata usata al posto di Product.Product
tutti i riferimenti in quegli altri metodi in tutti e tre i test. Il DaysToManufacture
campo viene utilizzato solo nel Test numero 2, ma era più facile essere coerenti tra gli script SQL per utilizzare la stessa tabella e non ha fatto male averlo 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
Tutti i metodi sembrano beneficiare allo stesso modo della memorizzazione nella cache e il mio metodo è ancora in vantaggio.
Test 2
Qui ora vediamo una differenza nell'allineamento poiché il mio metodo esce appena avanti, solo 2 legge meglio del primo apply row_number()
metodo di Mikael , mentre senza la memorizzazione nella cache il mio metodo era indietro di 4 letture.
Test 3
Vedi aggiornamento verso il basso (sotto la linea) . Qui vediamo di nuovo qualche differenza. Il sapore "parametrizzato" del mio metodo ora è a malapena in testa a 2 letture rispetto al metodo CROSS APPLY di Aaron (senza memorizzazione nella cache erano uguali). Ma la cosa davvero strana è che per la prima volta vediamo un metodo che è influenzato negativamente dalla memorizzazione nella cache: il metodo CTE di Aaron (che era precedentemente il migliore per il Test numero 3). Ma non mi prenderò il merito dove non è dovuto, e poiché senza la memorizzazione nella cache il metodo CTE di Aaron è ancora più veloce di quanto il mio metodo sia qui con la memorizzazione nella cache, l'approccio migliore per questa particolare situazione sembra essere il metodo CTE di Aaron.
Conclusione Si prega di consultare l'aggiornamento verso il basso (sotto la riga) Le
situazioni che fanno un uso ripetuto dei risultati di una query secondaria possono spesso (ma non sempre) trarre vantaggio dalla memorizzazione nella cache di tali risultati. Ma quando la memorizzazione nella cache è un vantaggio, l'utilizzo della memoria per detta memorizzazione nella cache presenta alcuni vantaggi rispetto all'utilizzo di tabelle temporanee.
Il metodo
Generalmente
Ho separato la query "header" (cioè ottenendo la ProductID
s, e in un caso anche la DaysToManufacture
, basata Name
sull'avvio con determinate lettere) dalle query "dettagliate" (cioè ottenendo la TransactionID
s e la TransactionDate
s). L'idea era di eseguire query molto semplici e di non confondere l'ottimizzatore durante l'adesione. Chiaramente questo non è sempre vantaggioso in quanto impedisce anche all'ottimizzatore di, bene, l'ottimizzazione. Ma come abbiamo visto nei risultati, a seconda del tipo di query, questo metodo ha i suoi meriti.
Le differenze tra i vari gusti di questo metodo sono:
Costanti: inviare eventuali valori sostituibili come costanti incorporate anziché essere parametri. Ciò farebbe riferimento a ProductID
tutti e tre i test e anche al numero di righe da restituire nel Test 2 in quanto questa è una funzione di "cinque volte l' DaysToManufacture
attributo Prodotto". Questo sotto-metodo significa che ognuno ProductID
otterrà il proprio piano di esecuzione, il che può essere utile se c'è una grande variazione nella distribuzione dei dati per ProductID
. Ma se c'è una piccola variazione nella distribuzione dei dati, il costo di generazione dei piani aggiuntivi probabilmente non ne varrà la pena.
Parametrizzato: inviare almeno ProductID
come @ProductID
, consentendo la memorizzazione e il riutilizzo della cache del piano di esecuzione. Esiste un'opzione di test aggiuntiva per trattare anche il numero variabile di righe da restituire per Test 2 come parametro.
Ottimizza sconosciuto: quando si fa riferimento ProductID
a @ProductID
, se esiste una grande variazione nella distribuzione dei dati, è possibile memorizzare nella cache un piano che ha un effetto negativo su altri ProductID
valori, quindi sarebbe bene sapere se l'utilizzo di questo suggerimento per le query è utile.
Prodotti cache: Invece di Production.Product
eseguire una query sulla tabella ogni volta, solo per ottenere lo stesso elenco esatto, esegui la query una volta (e mentre ci siamo, filtra qualsiasi ProductID
s che non sia nemmeno nella TransactionHistory
tabella in modo da non sprecare alcun risorse lì) e memorizza nella cache quell'elenco. L'elenco dovrebbe includere il DaysToManufacture
campo. Usando questa opzione c'è un hit iniziale leggermente più alto nelle Letture logiche per la prima esecuzione, ma dopo è solo la TransactionHistory
tabella a cui viene interrogata.
In particolare
Ok, ma allora, come è possibile emettere tutte le sottoquery come query separate senza usare un CURSORE e scaricare ogni set di risultati in una tabella temporanea o variabile di tabella? Chiaramente fare il metodo CURSOR / Temp Table rifletterebbe abbastanza ovviamente nelle letture e scritture. Bene, usando SQLCLR :). Creando una procedura memorizzata SQLCLR, sono stato in grado di aprire un set di risultati e essenzialmente trasmettere i risultati di ogni sottointerrogazione su di esso, come set di risultati continuo (e non più set di risultati). Al di fuori delle informazioni del prodotto (vale a dire ProductID
, Name
eDaysToManufacture
), nessuno dei risultati della sottoquery doveva essere archiviato ovunque (memoria o disco) e appena passato come set di risultati principale della procedura memorizzata SQLCLR. Questo mi ha permesso di fare una semplice query per ottenere le informazioni sul prodotto e poi scorrere attraverso di essa, inviando domande molto semplici contro TransactionHistory
.
Ed è per questo che ho dovuto utilizzare SQL Server Profiler per acquisire le statistiche. La procedura memorizzata SQLCLR non ha restituito un piano di esecuzione, né impostando l'opzione di query "Includi piano di esecuzione effettivo" o emettendo SET STATISTICS XML ON;
.
Per la memorizzazione nella cache delle informazioni sul prodotto, ho utilizzato un readonly static
elenco generico (ovvero _GlobalProducts
nel codice seguente). Sembra che l'aggiunta alle raccolte non violi l' readonly
opzione, quindi questo codice funziona quando l'assembly ha un segno PERMISSON_SET
di SAFE
:), anche se è controintuitivo.
Le query generate
Le query prodotte da questa stored procedure SQLCLR sono le seguenti:
Informazioni sul prodotto
Test numeri 1 e 3 (nessuna memorizzazione nella cache)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Test numero 2 (nessuna memorizzazione nella 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 numeri 1, 2 e 3 (memorizzazione nella 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;
Informazioni sulla transazione
Test numeri 1 e 2 (costanti)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Test numeri 1 e 2 (parametrizzati)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Numeri di test 1 e 2 (parametrizzati + OTTIMIZZA SCONOSCIUTO)
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 numero 2 (entrambi parametrizzati)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Test numero 2 (parametrizzato entrambi + OTTIMIZZA SCONOSCIUTO)
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 numero 3 (Costanti)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Test numero 3 (parametrizzato)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Test numero 3 (parametrizzato + OTTIMIZZA SCONOSCIUTO)
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));
Il codice
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);
}
}
}
}
Le query di prova
Non c'è abbastanza spazio per pubblicare i test qui, quindi troverò un'altra posizione.
La conclusione
Per alcuni scenari, SQLCLR può essere utilizzato per manipolare determinati aspetti delle query che non possono essere eseguiti in T-SQL. E c'è la possibilità di usare la memoria per la memorizzazione nella cache anziché le tabelle temporanee, sebbene ciò dovrebbe essere fatto con parsimonia e attenzione poiché la memoria non viene automaticamente rilasciata di nuovo nel sistema. Questo metodo non è anche qualcosa che aiuterà le query ad hoc, anche se è possibile renderlo più flessibile di quello che ho mostrato qui semplicemente aggiungendo parametri per personalizzare più aspetti delle query in esecuzione.
AGGIORNARE
Test aggiuntivo I
miei test originali che includevano un indice di supporto hanno TransactionHistory
utilizzato la seguente definizione:
ProductID ASC, TransactionDate DESC
Avevo deciso in quel momento di rinunciare anche TransactionId DESC
alla fine, immaginando che mentre potrebbe aiutare il Test Numero 3 (che specifica la rottura del più recente - TransactionId
beh, si presume che il "più recente" non sia esplicitamente dichiarato, ma tutti sembrano per concordare su questo presupposto), probabilmente non ci sarebbero legami sufficienti per fare la differenza.
Ma poi Aaron riprovò con un indice di supporto che includeva TransactionId DESC
e scoprì che il CROSS APPLY
metodo era il vincitore in tutti e tre i test. Questo era diverso dal mio test che indicava che il metodo CTE era il migliore per il Test Numero 3 (quando non veniva usata la cache, il che rispecchia il test di Aaron). Era chiaro che c'era una variazione aggiuntiva che doveva essere testata.
Ho rimosso l'attuale indice di supporto, ne ho creato uno nuovo TransactionId
e ho cancellato la cache del piano (per essere sicuro):
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;
Ho rieseguito il test numero 1 e i risultati sono stati gli stessi, come previsto. Ho quindi rieseguito il test numero 3 e i risultati sono effettivamente cambiati:
I risultati sopra riportati sono per il test standard senza memorizzazione nella cache. Questa volta, non solo CROSS APPLY
batte il CTE (come indicato dal test di Aaron), ma il proc SQLCLR ha preso il comando di 30 Read (woo hoo).
I risultati sopra riportati sono per il test con memorizzazione nella cache abilitata. Questa volta le prestazioni del CTE non sono degradate, anche se lo CROSS APPLY
batte ancora. Tuttavia, ora il proc SQLCLR prende il comando di 23 letture (woo hoo, di nuovo).
Take Aways
Ci sono varie opzioni da usare. È meglio provarne diversi in quanto ognuno ha i propri punti di forza. I test effettuati qui mostrano una varianza piuttosto piccola sia in Letture che in Durata tra i migliori e i peggiori in tutti i test (con un indice di supporto); la variazione in Letture è di circa 350 e la durata è di 55 ms. Mentre il proc SQLCLR ha vinto in tutti tranne 1 test (in termini di letture), il salvataggio di poche letture di solito non vale il costo di manutenzione per andare sulla rotta SQLCLR. Ma in AdventureWorks2012, la Product
tabella ha solo 504 righe e TransactionHistory
solo 113.443 righe. La differenza di prestazioni tra questi metodi probabilmente diventa più pronunciata all'aumentare del numero di righe.
Mentre questa domanda era specifica per ottenere un particolare set di righe, non si deve trascurare il fatto che il singolo fattore più importante nelle prestazioni era l'indicizzazione e non il particolare SQL. Un buon indice deve essere in atto prima di determinare quale metodo è veramente migliore.
La lezione più importante trovata qui non riguarda CROSS APPLY vs CTE vs SQLCLR: si tratta di TEST. Non dare per scontato Ottieni idee da più persone e testa quanti più scenari puoi.