Tomé un enfoque ligeramente diferente, principalmente para ver cómo esta técnica se compararía con las otras, porque tener opciones es bueno, ¿verdad?
La prueba
¿Por qué no comenzamos simplemente observando cómo los distintos métodos se comparan entre sí? Hice tres series de pruebas:
- El primer conjunto se ejecutó sin modificaciones de la base de datos.
- El segundo conjunto se ejecutó después de que se creó un índice para admitir
TransactionDate
consultas basadas en Production.TransactionHistory
.
- El tercer set hizo una suposición ligeramente diferente. Dado que las tres pruebas se ejecutaron en la misma lista de Productos, ¿qué pasa si almacenamos en caché esa lista? Mi método usa un caché en memoria mientras que los otros métodos usan una tabla temporal equivalente. El índice de soporte creado para el segundo conjunto de pruebas todavía existe para este conjunto de pruebas.
Detalles de prueba adicionales:
- Las pruebas se ejecutaron
AdventureWorks2012
en SQL Server 2012, SP2 (Developer Edition).
- Para cada prueba, marqué de qué respuesta tomé la consulta y de qué consulta en particular era.
- Usé la opción "Descartar resultados después de la ejecución" de Opciones de consulta | Resultados
- Tenga en cuenta que para los primeros dos conjuntos de pruebas,
RowCounts
parece estar "apagado" para mi método. Esto se debe a que mi método es una implementación manual de lo que CROSS APPLY
está haciendo: ejecuta la consulta inicial Production.Product
y recupera 161 filas, que luego utiliza para las consultas Production.TransactionHistory
. Por lo tanto, los RowCount
valores para mis entradas son siempre 161 más que las otras entradas. En el tercer conjunto de pruebas (con almacenamiento en caché) los recuentos de filas son los mismos para todos los métodos.
- Usé SQL Server Profiler para capturar las estadísticas en lugar de confiar en los planes de ejecución. Aaron y Mikael ya hicieron un gran trabajo al mostrar los planes para sus consultas y no hay necesidad de reproducir esa información. Y la intención de mi método es reducir las consultas a una forma tan simple que realmente no importaría. Hay una razón adicional para usar Profiler, pero eso se mencionará más adelante.
- En lugar de usar la
Name >= N'M' AND Name < N'S'
construcción, elegí usar Name LIKE N'[M-R]%'
, y SQL Server los trata de la misma manera.
Los resultados
Sin índice de respaldo
Esto es esencialmente AdventureWorks2012 listo para usar. En todos los casos, mi método es claramente mejor que algunos de los otros, pero nunca es tan bueno como los métodos 1 o 2 principales.
Prueba 1
El CTE de Aaron es claramente el ganador aquí.
Prueba 2
CTE de Aaron (nuevamente) y el segundo apply row_number()
método de Mikael es un segundo cercano.
Prueba 3
Aaron's CTE (nuevamente) es el ganador.
Conclusión
Cuando no hay un índice de respaldo TransactionDate
, mi método es mejor que hacer un estándar CROSS APPLY
, pero aun así, usar el método CTE es claramente el camino a seguir.
Con índice de soporte (sin almacenamiento en caché)
Para este conjunto de pruebas, agregué el índice obvio TransactionHistory.TransactionDate
ya que todas las consultas se ordenan en ese campo. Digo "obvio" ya que la mayoría de las otras respuestas también están de acuerdo en este punto. Y dado que todas las consultas desean las fechas más recientes, el TransactionDate
campo debe ordenarse DESC
, por lo que simplemente tomé la CREATE INDEX
declaración al final de la respuesta de Mikael y agregué un explícito FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Una vez que este índice está en su lugar, los resultados cambian bastante.
Prueba 1
Esta vez es mi método el que sale adelante, al menos en términos de lecturas lógicas. El CROSS APPLY
método, que anteriormente era el de peor desempeño para la Prueba 1, gana en Duración e incluso supera el método CTE en Lecturas lógicas.
Prueba 2
Esta vez, el primer apply row_number()
método de Mikael es el ganador al mirar las Lecturas, mientras que anteriormente era uno de los de peor desempeño. Y ahora mi método viene en un segundo lugar muy cercano cuando se mira Lecturas. De hecho, fuera del método CTE, el resto están bastante cerca en términos de lecturas.
Prueba 3
Aquí el CTE sigue siendo el ganador, pero ahora la diferencia entre los otros métodos es apenas notable en comparación con la diferencia drástica que existía antes de crear el índice.
Conclusión
La aplicabilidad de mi método es más evidente ahora, aunque es menos resistente a no tener índices adecuados en su lugar.
Con índice de soporte y almacenamiento en caché
Para este conjunto de pruebas utilicé el almacenamiento en caché porque, bueno, ¿por qué no? Mi método permite usar el almacenamiento en caché en memoria al que los otros métodos no pueden acceder. Para ser justos, creé la siguiente tabla temporal que se utilizó en lugar de Product.Product
todas las referencias en esos otros métodos en las tres pruebas. El DaysToManufacture
campo solo se usa en la Prueba número 2, pero fue más fácil ser coherente entre los scripts de SQL para usar la misma tabla y no hizo daño tenerlo allí.
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);
Prueba 1
Todos los métodos parecen beneficiarse igualmente del almacenamiento en caché, y mi método aún sale adelante.
Prueba 2
Aquí ahora vemos una diferencia en la alineación ya que mi método sale apenas por delante, solo 2 lecturas mejor que el primer apply row_number()
método de Mikael , mientras que sin el almacenamiento en caché mi método estaba retrasado por 4 lecturas.
Prueba 3
Consulte la actualización hacia la parte inferior (debajo de la línea) . Aquí nuevamente vemos alguna diferencia. El sabor "parametrizado" de mi método ahora apenas está a la cabeza en 2 lecturas en comparación con el método CROSS APPLY de Aaron (sin almacenamiento en caché, eran iguales). Pero lo realmente extraño es que, por primera vez, vemos un método que se ve afectado negativamente por el almacenamiento en caché: el método CTE de Aaron (que anteriormente era el mejor para la Prueba número 3). Pero, no voy a tomar crédito donde no se debe, y dado que sin el almacenamiento en caché, el método CTE de Aaron es aún más rápido que mi método aquí con el almacenamiento en caché, el mejor enfoque para esta situación particular parece ser el método CTE de Aaron.
Conclusión Consulte la actualización en la parte inferior (debajo de la línea). Las
situaciones que hacen uso repetido de los resultados de una consulta secundaria a menudo (pero no siempre) se benefician al almacenar en caché esos resultados. Pero cuando el almacenamiento en caché es un beneficio, el uso de memoria para dicho almacenamiento en caché tiene alguna ventaja sobre el uso de tablas temporales.
El método
Generalmente
Separé la consulta de "encabezado" (es decir, obteniendo el ProductID
s, y en un caso también el DaysToManufacture
, basado en el Name
comienzo con ciertas letras) de las consultas de "detalle" (es decir, obteniendo los TransactionID
s y TransactionDate
s). El concepto era realizar consultas muy simples y no permitir que el optimizador se confunda al UNIRSE a ellas. Claramente, esto no siempre es ventajoso, ya que también impide que el optimizador optimice. Pero como vimos en los resultados, dependiendo del tipo de consulta, este método tiene sus ventajas.
La diferencia entre los diversos sabores de este método son:
Constantes: envíe los valores reemplazables como constantes en línea en lugar de ser parámetros. Esto se referiría a ProductID
las tres pruebas y también al número de filas que se devolverán en la Prueba 2, ya que es una función de "cinco veces el DaysToManufacture
atributo Producto". Este submétodo significa que cada uno ProductID
obtendrá su propio plan de ejecución, lo que puede ser beneficioso si existe una amplia variación en la distribución de datos ProductID
. Pero si hay poca variación en la distribución de datos, el costo de generar los planes adicionales probablemente no valdrá la pena.
Parametrizado: envíe al menos ProductID
como @ProductID
, permitiendo el almacenamiento en caché y la reutilización del plan de ejecución. Hay una opción de prueba adicional para tratar también el número variable de filas para devolver para la Prueba 2 como un parámetro.
Optimizar Desconocido: Cuando se hace referencia ProductID
como @ProductID
, si existe una amplia variación de la distribución de datos, entonces es posible almacenar en caché un plan que tiene un efecto negativo sobre otros ProductID
valores por lo que sería bueno saber si el uso de esta sugerencia de consulta ayuda a cualquier.
Productos de caché: en lugar de consultar la Production.Product
tabla cada vez, solo para obtener exactamente la misma lista, ejecute la consulta una vez (y mientras lo hacemos, filtre cualquier ProductID
correo que ni siquiera esté en la TransactionHistory
tabla para que no desperdiciemos ninguno recursos allí) y almacenar en caché esa lista. La lista debe incluir el DaysToManufacture
campo. Al usar esta opción, hay un impacto inicial ligeramente mayor en las lecturas lógicas para la primera ejecución, pero después de eso solo TransactionHistory
se consulta la tabla.
Específicamente
Ok, pero entonces, ¿cómo es posible emitir todas las subconsultas como consultas separadas sin usar un CURSOR y volcar cada conjunto de resultados en una tabla o variable de tabla temporal? Claramente, hacer el método CURSOR / Tabla temporal se reflejaría de manera bastante obvia en las lecturas y escrituras. Bueno, usando SQLCLR :). Al crear un procedimiento almacenado SQLCLR, pude abrir un conjunto de resultados y esencialmente transmitirle los resultados de cada subconsulta, como un conjunto de resultados continuo (y no múltiples conjuntos de resultados). Fuera de la información del producto (es decir ProductID
, Name
yDaysToManufacture
), ninguno de los resultados de la subconsulta tuvo que almacenarse en ningún lugar (memoria o disco) y simplemente pasó como el conjunto de resultados principal del procedimiento almacenado SQLCLR. Esto me permitió hacer una consulta simple para obtener la información del Producto y luego recorrerla, emitiendo consultas muy simples en contra TransactionHistory
.
Y es por eso que tuve que usar SQL Server Profiler para capturar las estadísticas. El procedimiento almacenado SQLCLR no devolvió un plan de ejecución, ya sea configurando la opción de consulta "Incluir plan de ejecución real" o emitiendo SET STATISTICS XML ON;
.
Para el almacenamiento en caché de información del producto, utilicé una readonly static
lista genérica (es decir, _GlobalProducts
en el código a continuación). Parece que la adición a las colecciones no viola la readonly
opción, por lo tanto, este código funciona cuando el conjunto tiene una PERMISSON_SET
de SAFE
:), incluso si esto es contrario a la intuición.
Las consultas generadas
Las consultas producidas por este procedimiento almacenado SQLCLR son las siguientes:
Información del producto
Números de prueba 1 y 3 (sin almacenamiento en caché)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Prueba número 2 (sin almacenamiento en caché)
;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;
Números de prueba 1, 2 y 3 (almacenamiento en caché)
;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;
Información de la transacción
Números de prueba 1 y 2 (constantes)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Números de prueba 1 y 2 (parametrizados)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Números de prueba 1 y 2 (parametrizado + OPTIMIZAR DESCONOCIDO)
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));
Prueba número 2 (ambos parametrizados)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Prueba número 2 (parametrizado ambos + OPTIMIZAR DESCONOCIDO)
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));
Prueba número 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;
Prueba número 3 (parametrizada)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Prueba número 3 (parametrizado + OPTIMIZAR DESCONOCIDO)
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));
El código
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);
}
}
}
}
Las consultas de prueba
No hay suficiente espacio para publicar las pruebas aquí, así que encontraré otra ubicación.
La conclusión
Para ciertos escenarios, SQLCLR se puede usar para manipular ciertos aspectos de las consultas que no se pueden hacer en T-SQL. Y existe la capacidad de utilizar la memoria para el almacenamiento en caché en lugar de las tablas temporales, aunque eso debe hacerse con moderación y cuidado, ya que la memoria no se libera automáticamente al sistema. Este método tampoco es algo que ayude a las consultas ad hoc, aunque es posible hacerlo más flexible de lo que he mostrado aquí simplemente agregando parámetros para adaptar más aspectos de las consultas que se ejecutan.
ACTUALIZAR
Prueba adicional
Mis pruebas originales que incluían un índice de respaldo TransactionHistory
utilizaron la siguiente definición:
ProductID ASC, TransactionDate DESC
Decidí renunciar en ese momento, incluso TransactionId DESC
al final, pensando que si bien podría ayudar la Prueba número 3 (que especifica el desempate en el más reciente TransactionId
, bueno, se supone "más reciente", ya que no se indica explícitamente, pero todos parecen estar de acuerdo con esta suposición), probablemente no habría suficientes lazos para marcar la diferencia.
Pero, Aaron volvió a probar con un índice de apoyo que sí incluyó TransactionId DESC
y descubrió que el CROSS APPLY
método fue el ganador en las tres pruebas. Esto fue diferente a mi prueba que indicaba que el método CTE era el mejor para la Prueba número 3 (cuando no se utilizó el almacenamiento en caché, que refleja la prueba de Aaron). Estaba claro que había una variación adicional que necesitaba ser probada.
Eliminé el índice de soporte actual, creé uno nuevo TransactionId
y borré el caché del plan (solo para estar seguro):
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;
Volví a ejecutar la Prueba número 1 y los resultados fueron los mismos, como se esperaba. Luego volví a ejecutar la Prueba número 3 y los resultados realmente cambiaron:
Los resultados anteriores son para la prueba estándar sin almacenamiento en caché. Esta vez, no solo CROSS APPLY
supera el CTE (tal como lo indicó la prueba de Aaron), sino que el proceso SQLCLR tomó la delantera en 30 lecturas (woo hoo).
Los resultados anteriores son para la prueba con el almacenamiento en caché habilitado. Esta vez, el rendimiento del CTE no se degrada, aunque CROSS APPLY
todavía lo supera. Sin embargo, ahora el proceso SQLCLR toma la delantera en 23 lecturas (woo hoo, de nuevo).
Llevar
Hay varias opciones para usar. Es mejor probar varios, ya que cada uno tiene sus puntos fuertes. Las pruebas realizadas aquí muestran una variación bastante pequeña tanto en Lecturas como en Duración entre los mejores y peores resultados en todas las pruebas (con un índice de apoyo); la variación en lecturas es de aproximadamente 350 y la duración es de 55 ms. Si bien el proceso SQLCLR ganó en todas las pruebas menos 1 (en términos de Lecturas), solo guardar algunas Lecturas generalmente no vale el costo de mantenimiento de seguir la ruta SQLCLR. Pero en AdventureWorks2012, la Product
tabla tiene solo 504 filas y TransactionHistory
solo 113,443 filas. La diferencia de rendimiento entre estos métodos probablemente se vuelve más pronunciada a medida que aumenta el recuento de filas.
Si bien esta pregunta era específica para obtener un conjunto particular de filas, no debe pasarse por alto que el factor más importante en el rendimiento fue la indexación y no el SQL particular. Es necesario establecer un buen índice antes de determinar qué método es realmente mejor.
La lección más importante que se encuentra aquí no se trata de CROSS APPLY vs CTE vs SQLCLR: se trata de TESTING. No asumas Obtenga ideas de varias personas y pruebe tantos escenarios como pueda.
APPLY TOP
oROW_NUMBER()
? ¿Qué podría haber más que decir al respecto?Una breve recapitulación de las diferencias y, para ser realmente breve, solo mostraré los planes para la opción 2 y he agregado el índice
Production.TransactionHistory
.La
row_number()
consulta :.La
apply top
versión:La principal diferencia entre estos es que los
apply top
filtros en la expresión superior debajo de los bucles anidados se unen donde larow_number
versión se filtra después de la unión. Eso significa que hay más lecturas de lasProduction.TransactionHistory
que realmente son necesarias.Si solo existiera una forma de empujar a los operadores responsables de enumerar las filas hasta la rama inferior antes de la unión, la
row_number
versión podría funcionar mejor.Entonces ingrese la
apply row_number()
versión.Como puede ver,
apply row_number()
es casi lo mismo queapply top
solo un poco más complicado. El tiempo de ejecución también es casi igual o un poco más lento.Entonces, ¿por qué me molesté en encontrar una respuesta que no sea mejor que la que ya tenemos? Bueno, tienes una cosa más para probar en el mundo real y en realidad hay una diferencia en las lecturas. Uno para el que no tengo una explicación *.
Mientras estoy en ello, podría lanzar una segunda
row_number()
versión que, en ciertos casos, podría ser el camino a seguir. Esos ciertos casos serían cuando esperas que realmente necesites la mayoría de las filasProduction.TransactionHistory
porque aquí obtienes una combinación de fusión entreProduction.Product
y los enumeradosProduction.TransactionHistory
.Para obtener la forma anterior sin un operador de clasificación, también debe cambiar el índice de soporte al orden
TransactionDate
descendiendo.* Editar: Las lecturas lógicas adicionales se deben a la captación previa de bucles anidados utilizada con el aplicador. Puede deshabilitar esto con TF 8744 no documentado (y / o 9115 en versiones posteriores) para obtener el mismo número de lecturas lógicas. La captación previa podría ser una ventaja de la alternativa de aplicación superior en las circunstancias correctas. - Paul White
fuente
Normalmente uso una combinación de CTE y funciones de ventanas. Puede lograr esta respuesta usando algo como lo siguiente:
Para la porción de crédito adicional, donde los diferentes grupos pueden querer devolver diferentes números de filas, puede usar una tabla separada. Digamos que usando criterios geográficos como el estado:
Para lograr esto donde los valores pueden ser diferentes, necesitaría unir su CTE a la tabla de estado similar a esto:
fuente