Eu tomei uma abordagem um pouco diferente, principalmente para ver como essa técnica se compara às outras, porque ter opções é bom, certo?
O teste
Por que não começamos apenas olhando como os vários métodos se comparam. Eu fiz três conjuntos de testes:
- O primeiro conjunto foi executado sem modificações no banco de dados
- O segundo conjunto foi executado depois que um índice foi criado para suportar
TransactionDate
consultas baseadas em Production.TransactionHistory
.
- O terceiro set fez uma suposição um pouco diferente. Como todos os três testes foram executados na mesma lista de produtos, e se colocarmos em cache essa lista? Meu método usa um cache na memória enquanto os outros métodos usavam uma tabela temporária equivalente. O índice de suporte criado para o segundo conjunto de testes ainda existe para esse conjunto de testes.
Detalhes adicionais do teste:
- Os testes foram executados
AdventureWorks2012
no SQL Server 2012, SP2 (Developer Edition).
- Para cada teste, identifiquei de quem recebi a resposta e de qual consulta específica.
- Usei a opção "Descartar resultados após a execução" de Opções de consulta | Resultados.
- Observe que, para os dois primeiros conjuntos de testes, o
RowCounts
parece estar "desativado" para o meu método. Isso se deve ao fato de meu método ser uma implementação manual do que CROSS APPLY
está sendo feito: ele executa a consulta inicial Production.Product
e recupera 161 linhas, que são usadas para as consultas Production.TransactionHistory
. Portanto, os RowCount
valores para minhas entradas são sempre 161 a mais que as outras entradas. No terceiro conjunto de testes (com armazenamento em cache), a contagem de linhas é a mesma para todos os métodos.
- Eu usei o SQL Server Profiler para capturar as estatísticas em vez de confiar nos planos de execução. Aaron e Mikael já fizeram um ótimo trabalho mostrando os planos para suas consultas e não há necessidade de reproduzir essas informações. E a intenção do meu método é reduzir as consultas para uma forma tão simples que realmente não importa. Há um motivo adicional para usar o Profiler, mas isso será mencionado posteriormente.
- Em vez de usar a
Name >= N'M' AND Name < N'S'
construção, eu escolhi usar Name LIKE N'[M-R]%'
, e o SQL Server os trata da mesma maneira.
Os resultados
Nenhum índice de suporte
Este é essencialmente o AdventureWorks2012 pronto para uso. Em todos os casos, meu método é claramente melhor que alguns outros, mas nunca tão bom quanto os 1 ou 2 métodos mais importantes.
Teste 1
O CTE de Aaron é claramente o vencedor aqui.
Teste 2
O CTE de Aaron (novamente) e o segundo apply row_number()
método de Mikael é um segundo próximo.
Teste 3
O CTE de Aaron (novamente) é o vencedor.
Conclusão
Quando não há índice de suporte ativado TransactionDate
, meu método é melhor do que fazer um padrão CROSS APPLY
, mas ainda assim, usar o método CTE é claramente o caminho a percorrer.
Com índice de suporte (sem armazenamento em cache)
Para esse conjunto de testes, adicionei o índice óbvio, TransactionHistory.TransactionDate
pois todas as consultas são classificadas nesse campo. Eu digo "óbvio", já que a maioria das outras respostas também concorda com esse ponto. E como as consultas estão todas querendo as datas mais recentes, o TransactionDate
campo deve ser ordenado DESC
, então peguei a CREATE INDEX
declaração na parte inferior da resposta de Mikael e adicionei um explícito FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Quando esse índice está em vigor, os resultados mudam bastante.
Teste 1
Desta vez, é o meu método que sai à frente, pelo menos em termos de leituras lógicas. O CROSS APPLY
método, anteriormente o pior desempenho do Teste 1, vence no Duration e até supera o método CTE nas Logical Reads.
Teste 2
Desta vez, é o primeiro apply row_number()
método de Mikael que vence quando se lê o Reads, enquanto anteriormente era um dos piores desempenhos. E agora meu método chega em um segundo lugar muito próximo ao olhar para o Reads. De fato, fora do método CTE, o restante é bastante próximo em termos de leitura.
Teste 3
Aqui o CTE ainda é o vencedor, mas agora a diferença entre os outros métodos é quase imperceptível em comparação com a diferença drástica que existia antes da criação do índice.
Conclusão
A aplicabilidade do meu método é mais aparente agora, embora seja menos resistente a não ter índices adequados.
Com índice e cache de suporte
Para esse conjunto de testes, usei o cache porque, bem, por que não? Meu método permite o uso de cache na memória que os outros métodos não podem acessar. Portanto, para ser justo, criei a seguinte tabela temporária que foi usada no lugar de Product.Product
todas as referências nesses outros métodos nos três testes. O DaysToManufacture
campo é usado apenas no Teste Número 2, mas era mais fácil ser consistente nos scripts SQL para usar a mesma tabela e não fazia mal tê-la 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);
Teste 1
Todos os métodos parecem se beneficiar igualmente do cache, e meu método ainda está à frente.
Teste 2
Aqui agora vemos uma diferença na programação, já que meu método sai à frente, apenas 2 leituras melhor que o primeiro apply row_number()
método de Mikael , enquanto que sem o armazenamento em cache, meu método ficou atrasado em 4 leituras.
Teste 3
Consulte a atualização na parte inferior (abaixo da linha) . Aqui novamente vemos alguma diferença. O sabor "parametrizado" do meu método agora está quase na liderança em 2 leituras em comparação com o método CROSS APPLY de Aaron (sem cache, eles eram iguais). Mas o mais estranho é que, pela primeira vez, vemos um método que é afetado negativamente pelo cache: o método CTE de Aaron (que anteriormente era o melhor para o teste número 3). Mas não vou levar o crédito onde não é devido e, como o método CTE do Aaron em cache ainda é mais rápido do que o meu método aqui com o cache, a melhor abordagem para essa situação específica parece ser o método CTE do Aaron.
Conclusão Consulte a atualização na parte inferior (abaixo da linha). As
situações que fazem uso repetido dos resultados de uma consulta secundária geralmente podem (mas nem sempre) se beneficiar do armazenamento em cache desses resultados. Mas quando o armazenamento em cache é um benefício, o uso de memória para esse armazenamento em cache tem alguma vantagem sobre o uso de tabelas temporárias.
O método
Geralmente
I separada da consulta "cabeçalho" (isto é, ficando a ProductID
s, e, num caso, também o DaysToManufacture
, com base no Name
começando com determinadas letras) a partir das consultas "detail" (isto é, ficando os TransactionID
s e TransactionDate
s). O conceito era realizar consultas muito simples e não permitir que o otimizador se confundisse ao se juntar a elas. Claramente, isso nem sempre é vantajoso, pois também impede o otimizador de, assim, otimizar. Mas, como vimos nos resultados, dependendo do tipo de consulta, esse método tem seus méritos.
A diferença entre os vários sabores desse método são:
Constantes: envie quaisquer valores substituíveis como constantes em linha em vez de serem parâmetros. Isso se refere aos ProductID
três testes e também ao número de linhas a serem retornadas no Teste 2, pois isso é uma função de "cinco vezes o DaysToManufacture
atributo Produto". Esse sub-método significa que cada um ProductID
terá seu próprio plano de execução, o que pode ser benéfico se houver uma grande variação na distribuição de dados ProductID
. Mas se houver pouca variação na distribuição de dados, o custo de gerar os planos adicionais provavelmente não valerá a pena.
Parametrizado: envie pelo menos ProductID
como @ProductID
, permitindo o armazenamento em cache e a reutilização do plano de execução. Há uma opção de teste adicional para também tratar o número variável de linhas a serem retornadas para o Teste 2 como um parâmetro.
Otimizar desconhecido: ao referenciar ProductID
como @ProductID
, se houver uma grande variação na distribuição de dados, é possível armazenar em cache um plano que tenha um efeito negativo em outros ProductID
valores, portanto, seria bom saber se o uso dessa dica de consulta ajuda alguma.
Produtos de cache: em vez de consultar a Production.Product
tabela a cada vez, apenas para obter a mesma lista exata, execute a consulta uma vez (e enquanto estivermos nela, filtre quaisquer ProductID
s que não estejam na TransactionHistory
tabela para não desperdiçar nada) recursos lá) e armazene em cache essa lista. A lista deve incluir o DaysToManufacture
campo Usando esta opção, há um acerto inicial ligeiramente mais alto nas leituras lógicas para a primeira execução, mas depois disso é apenas a TransactionHistory
tabela que é consultada.
Especificamente
Ok, mas como é possível emitir todas as subconsultas como consultas separadas sem usar um CURSOR e despejar cada conjunto de resultados em uma tabela ou variável de tabela temporária? Claramente, o método CURSOR / Temp Table refletiria obviamente nas leituras e gravações. Bem, usando SQLCLR :). Ao criar um procedimento armazenado SQLCLR, consegui abrir um conjunto de resultados e essencialmente transmitir os resultados de cada subconsulta a ele, como um conjunto de resultados contínuos (e não vários conjuntos de resultados). Fora das informações do produto (ie ProductID
, Name
eDaysToManufacture
), nenhum dos resultados da subconsulta precisou ser armazenado em qualquer lugar (memória ou disco) e passou como o principal conjunto de resultados do procedimento armazenado SQLCLR. Isso me permitiu fazer uma consulta simples para obter as informações do produto e depois percorrer as mesmas, emitindo consultas muito simples TransactionHistory
.
E é por isso que tive que usar o SQL Server Profiler para capturar as estatísticas. O procedimento armazenado SQLCLR não retornou um plano de execução, definindo a opção de consulta "Incluir plano de execução real" ou emitindo SET STATISTICS XML ON;
.
Para o cache de informações do produto, usei uma readonly static
lista genérica (ou seja, _GlobalProducts
no código abaixo). Parece que adicionar coleções não viola a readonly
opção; portanto, esse código funciona quando o assembly possui um PERMISSON_SET
de SAFE
:), mesmo que isso seja contra-intuitivo.
As consultas geradas
As consultas produzidas por este procedimento armazenado SQLCLR são as seguintes:
Informação do produto
Números de teste 1 e 3 (sem armazenamento em cache)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Teste número 2 (sem armazenamento em 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;
Números de teste 1, 2 e 3 (armazenamento em 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;
Informações da transação
Números de teste 1 e 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 teste 1 e 2 (com parâmetros)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Números de teste 1 e 2 (Parametrizado + OTIMIZAR DESCONHECIDO)
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));
Teste 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
;
Teste Número 2 (Parametrizado Ambos + OTIMIZAR DESCONHECIDO)
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));
Teste 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;
Teste número 3 (parametrizado)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Teste Número 3 (Parametrizado + OTIMIZAR DESCONHECIDO)
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));
O 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);
}
}
}
}
As consultas de teste
Não há espaço suficiente para postar os testes aqui, então vou encontrar outro local.
A conclusão
Para certos cenários, o SQLCLR pode ser usado para manipular certos aspectos de consultas que não podem ser feitas no T-SQL. E existe a capacidade de usar memória para armazenamento em cache em vez de tabelas temporárias, embora isso deva ser feito com moderação e cuidado, pois a memória não é liberada automaticamente de volta ao sistema. Esse método também não é algo que ajudará consultas ad hoc, embora seja possível torná-lo mais flexível do que mostrei aqui, simplesmente adicionando parâmetros para personalizar mais aspectos das consultas que estão sendo executadas.
ATUALIZAR
Teste Adicional
Meus testes originais que incluíam um índice de suporte TransactionHistory
usavam a seguinte definição:
ProductID ASC, TransactionDate DESC
Eu tinha decidido na época renunciar à inclusão TransactionId DESC
no final, imaginando que, embora isso possa ajudar o Teste Número 3 (que especifica o desempate nos mais recentesTransactionId
, "o mais recente" é assumido, uma vez que não foi declarado explicitamente, mas todos parecem concordar com essa suposição), provavelmente não haveria laços suficientes para fazer a diferença.
Porém, Aaron testou novamente com um índice de suporte que incluiu TransactionId DESC
e descobriu que o CROSS APPLY
método foi o vencedor nos três testes. Isso foi diferente do meu teste, que indicou que o método CTE era melhor para o Teste Número 3 (quando nenhum cache foi usado, o que reflete o teste de Aaron). Ficou claro que havia uma variação adicional que precisava ser testada.
Removai o índice de suporte atual, criei um novo TransactionId
e limpei o cache do plano (apenas para ter certeza):
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;
Voltei a executar o Teste Número 1 e os resultados foram os mesmos, conforme o esperado. Em seguida, refiz o Teste Número 3 e os resultados realmente mudaram:
Os resultados acima são para o teste padrão sem cache. Desta vez, não apenas CROSS APPLY
vence o CTE (como o teste de Aaron indicou), mas o processo SQLCLR assumiu a liderança em 30 leituras (woo hoo).
Os resultados acima são para o teste com o cache ativado. Desta vez, o desempenho do CTE não é degradado, embora o CROSS APPLY
ainda o supere. No entanto, agora o processo SQLCLR assume a liderança em 23 leituras (woo hoo, novamente).
Aprendizado
Existem várias opções para usar. É melhor tentar vários, pois cada um tem seus pontos fortes. Os testes realizados aqui mostram uma variação bastante pequena nas leituras e na duração entre os melhores e os piores desempenhos em todos os testes (com um índice de suporte); a variação nas leituras é de cerca de 350 e a duração é de 55 ms. Embora o processo SQLCLR tenha vencido em todos os testes, exceto um (em termos de leituras), salvar apenas algumas leituras geralmente não vale o custo de manutenção de seguir a rota SQLCLR. Mas no AdventureWorks2012, a Product
tabela possui apenas 504 linhas e TransactionHistory
apenas 113.443 linhas. A diferença de desempenho entre esses métodos provavelmente se torna mais acentuada à medida que a contagem de linhas aumenta.
Embora essa pergunta tenha sido específica para obter um conjunto específico de linhas, não se deve esquecer que o maior fator de desempenho foi a indexação e não o SQL em particular. Um bom índice precisa estar em vigor antes de determinar qual método é realmente melhor.
A lição mais importante encontrada aqui não é sobre CROSS APPLY vs CTE vs SQLCLR: trata-se de TESTAR. Não assuma. Obtenha idéias de várias pessoas e teste o maior número possível de cenários.