Я выбрал немного другой подход, главным образом, чтобы увидеть, как этот метод будет сравниваться с другими, потому что наличие вариантов - это хорошо, правда?
Тестирование
Почему бы нам не начать с простого рассмотрения того, как различные методы складываются друг против друга. Я сделал три набора тестов:
- Первый набор прошел без изменений БД
- Второй набор выполнялся после создания индекса для поддержки
TransactionDate
запросов на основе Production.TransactionHistory
.
- Третий сет сделал немного другое предположение. Поскольку все три теста выполнялись для одного и того же списка продуктов, что, если мы кэшируем этот список? Мой метод использует кэш в памяти, в то время как другие методы использовали эквивалентную временную таблицу. Поддерживающий индекс, созданный для второго набора тестов, все еще существует для этого набора тестов.
Дополнительные детали теста:
- Тесты были выполнены
AdventureWorks2012
на SQL Server 2012, SP2 (Developer Edition).
- Для каждого теста я помечал, чей ответ я взял запрос и какой конкретный запрос это был.
- Я использовал опцию «Отменить результаты после выполнения» в параметрах запроса | Результаты.
- Обратите внимание, что для первых двух наборов тестов,
RowCounts
похоже, «выключен» для моего метода. Это связано с тем, что мой метод представляет собой ручную реализацию того, что CROSS APPLY
происходит: он выполняет исходный запрос Production.Product
и возвращает 161 строку назад, которую затем использует для запросов Production.TransactionHistory
. Следовательно, RowCount
значения для моих записей всегда на 161 больше, чем для других записей. В третьем наборе тестов (с кэшированием) число строк одинаково для всех методов.
- Я использовал SQL Server Profiler для сбора статистики вместо того, чтобы полагаться на планы выполнения. Аарон и Микаэль уже проделали большую работу, показав планы своих запросов, и нет необходимости воспроизводить эту информацию. И цель моего метода - свести запросы к такой простой форме, что это на самом деле не имеет значения. Существует дополнительная причина для использования Profiler, но это будет упомянуто позже.
- Вместо того, чтобы использовать
Name >= N'M' AND Name < N'S'
конструкцию, я решил использовать Name LIKE N'[M-R]%'
, и SQL Server обрабатывает их так же.
Результаты, достижения
Нет поддержки индекса
Это по сути из AdventureWorks2012 из коробки. Во всех случаях мой метод явно лучше, чем некоторые другие, но никогда не так хорош, как лучшие 1 или 2 метода.
Тест 1
CTE Аарона, безусловно, победитель.
Тест 2
CTE Аарона (снова) и второй apply row_number()
метод Микаэля - вторая секунда.
Тест 3
CTE Аарона (снова) является победителем.
Заключение
Когда нет поддерживающего индекса TransactionDate
, мой метод лучше, чем стандартный CROSS APPLY
, но все же, использование метода CTE - лучший путь.
С индексом поддержки (без кэширования)
Для этого набора тестов я добавил очевидный индекс, TransactionHistory.TransactionDate
поскольку все запросы сортируются по этому полю. Я говорю «очевидно», так как большинство других ответов также согласны с этим. И поскольку все запросы требуют самых последних дат, TransactionDate
поле должно быть упорядочено DESC
, поэтому я просто взял CREATE INDEX
утверждение в нижней части ответа Микаэля и добавил явное FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Как только этот индекс будет создан, результаты меняются совсем немного.
Тест 1 На
этот раз мой метод выходит вперед, по крайней мере, с точки зрения логического чтения. CROSS APPLY
Метод, ранее худший для теста 1, выигрывает по продолжительности и даже превосходит метод КТРА на логических чтениях.
Тест 2 На
этот раз это первый apply row_number()
метод Микаэля, который является победителем при чтении Рединса, тогда как раньше он был одним из худших исполнителей. И теперь мой метод находится на очень близком втором месте, если смотреть на Reads. Фактически, вне метода CTE, все остальные довольно близки с точки зрения чтения.
Тест 3
Здесь CTE остается победителем, но теперь разница между другими методами едва заметна по сравнению с радикальной разницей, существовавшей до создания индекса.
Заключение
Применимость моего метода теперь более очевидна, хотя он менее устойчив к отсутствию надлежащих индексов.
С поддержкой индекса и кэширования
Для этого набора тестов я использовал кэширование, потому что, ну почему бы и нет? Мой метод позволяет использовать кэширование в памяти, к которому другие методы не могут получить доступ. Чтобы быть справедливым, я создал следующую временную таблицу, которая использовалась вместо Product.Product
всех ссылок в этих других методах во всех трех тестах. Это DaysToManufacture
поле используется только в тесте № 2, но было проще быть согласованным по всем сценариям SQL, чтобы использовать одну и ту же таблицу, и это не мешало иметь ее там.
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);
Тест 1
Кажется, что все методы одинаково выигрывают от кэширования, и мой метод все еще впереди.
Тест 2
Здесь мы видим различие в линейке, поскольку мой метод выходит далеко вперед, только на 2 чтения лучше, чем первый apply row_number()
метод Микаэля , тогда как без кеширования мой метод отставал на 4 чтения.
Тест 3
Пожалуйста, смотрите обновление в нижней части (ниже линии) . Здесь мы снова видим некоторую разницу. «Параметризованный» вариант моего метода теперь едва ли лидирует на 2 операции чтения по сравнению с методом Аарона CROSS APPLY (без кеширования они были равны). Но действительно странная вещь заключается в том, что впервые мы видим метод, на который кеширование оказывает негативное влияние: метод CTE Аарона (который ранее был лучшим для теста № 3). Но я не собираюсь брать кредит там, где это не нужно, и, поскольку без кэширования метод CTE Аарона все еще быстрее, чем мой метод с кэшированием, лучшим подходом для этой конкретной ситуации является метод CTE Аарона.
Заключение Пожалуйста, смотрите обновление в нижней части (ниже строки).
Ситуации, в которых многократно используются результаты вторичного запроса, могут часто (но не всегда) извлекать выгоду из кэширования этих результатов. Но когда кэширование является преимуществом, использование памяти для указанного кэширования имеет некоторое преимущество по сравнению с использованием временных таблиц.
Метод
В общем
Я отделил запрос «заголовок» (т.е. получение ProductID
s и, в одном случае, также DaysToManufacture
, основываясь на Name
начале с определенными буквами) от запросов «подробно» (т.е. получение TransactionID
s и TransactionDate
s). Идея заключалась в том, чтобы выполнять очень простые запросы и не позволить оптимизатору запутаться при их присоединении. Понятно, что это не всегда выгодно, так как не позволяет оптимизатору оптимизировать. Но, как мы видели в результатах, в зависимости от типа запроса этот метод имеет свои достоинства.
Разница между различными вкусами этого метода:
Константы: передайте любые заменяемые значения в виде встроенных констант, а не параметров. Это относится ко ProductID
всем трем тестам, а также к числу строк, которые нужно вернуть в тесте 2, поскольку это функция «пятикратного DaysToManufacture
атрибута Product». Этот под-метод означает, что каждый ProductID
получит свой собственный план выполнения, который может быть полезен, если существует широкий разброс в распределении данных для ProductID
. Но если распределение данных будет незначительным, стоимость создания дополнительных планов, скорее всего, не будет того стоить.
Параметризованный: Отправить хотя бы ProductID
как @ProductID
, что позволяет кэширование и повторное использование плана выполнения. Существует дополнительная опция теста, которая также обрабатывает переменное число строк, возвращаемых для теста 2, в качестве параметра.
Оптимизировать неизвестно: при ссылке ProductID
на @ProductID
, если существует широкий разброс в распределении данных, можно кэшировать план, который отрицательно влияет на другие ProductID
значения, поэтому было бы полезно узнать, помогает ли это использование подсказки запроса.
Продукты кэширования: вместо того, чтобы Production.Product
каждый раз запрашивать таблицу, только чтобы получить один и тот же список, выполните запрос один раз (и пока мы на нем, отфильтровываем любые ProductID
s, которых нет даже в TransactionHistory
таблице, поэтому мы не теряем ничего ресурсы там) и кэшировать этот список. Список должен включать DaysToManufacture
поле. При использовании этого параметра начальное попадание при логическом чтении для первого выполнения несколько выше, но после этого TransactionHistory
запрашивается только таблица.
конкретно
Хорошо, но так, как можно выполнить все подзапросы как отдельные запросы без использования CURSOR и вывода каждого результирующего набора во временную таблицу или табличную переменную? Очевидно, что использование метода CURSOR / Temp Table вполне отражается в «Чтении и записи». Ну, используя SQLCLR :). Создав хранимую процедуру SQLCLR, я смог открыть набор результатов и, по существу, передать в него результаты каждого подзапроса в виде непрерывного набора результатов (а не нескольких наборов результатов). Вне информации о продукте (то есть ProductID
, Name
иDaysToManufacture
), ни один из результатов подзапроса не должен был храниться где-либо (память или диск) и просто передавался как основной набор результатов хранимой процедуры SQLCLR. Это позволило мне сделать простой запрос, чтобы получить информацию о продукте, а затем циклически просмотреть его, выпуская очень простые запросы TransactionHistory
.
И именно поэтому мне пришлось использовать SQL Server Profiler для сбора статистики. Хранимая процедура SQLCLR не возвращала план выполнения, либо путем установки параметра запроса «Включить фактический план выполнения», либо путем выдачи SET STATISTICS XML ON;
.
Для кеширования информации о продукте я использовал readonly static
общий список (т.е. _GlobalProducts
в приведенном ниже коде). Кажется , что добавление к коллекциям не нарушает readonly
вариант, следовательно , этот код работает , когда сборка имеет PERMISSON_SET
в SAFE
:), даже если это нелогичное.
Сгенерированные запросы
Запросы, произведенные этой хранимой процедурой SQLCLR, следующие:
Информация о продукте
Тест № 1 и 3 (без кэширования)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Тест № 2 (без кеширования)
;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;
Тестовые номера 1, 2 и 3 (кеширование)
;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;
Информация о транзакции
Тест № 1 и 2 (Константы)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Тест № 1 и 2 (параметризованный)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Тестовые номера 1 и 2 (Параметризованный + ОПТИМИЗИРОВАТЬ НЕИЗВЕСТНЫЙ)
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));
Тест № 2 (параметризованный оба)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Тест № 2 (Параметризованный Оба + ОПТИМИЗИРОВАТЬ НЕИЗВЕСТНЫЙ)
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));
Тест № 3 (Константы)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Тест № 3 (Параметризованный)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;
Тест № 3 (Параметризованный + ОПТИМИЗИРОВАТЬ НЕИЗВЕСТНЫЙ)
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));
Код
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);
}
}
}
}
Тестовые Запросы
Здесь недостаточно места для размещения тестов, поэтому я найду другое место.
Вывод
Для определенных сценариев SQLCLR может использоваться для манипулирования определенными аспектами запросов, которые не могут быть выполнены в T-SQL. И есть возможность использовать память для кэширования вместо временных таблиц, хотя это следует делать осторожно и осторожно, так как память не возвращается автоматически в систему. Этот метод также не является чем-то, что поможет специальным запросам, хотя можно сделать его более гибким, чем я показал здесь, просто добавив параметры, чтобы адаптировать больше аспектов выполняемых запросов.
ОБНОВИТЬ
Дополнительный тест
Мои оригинальные тесты, которые включали вспомогательный индекс, TransactionHistory
использовали следующее определение:
ProductID ASC, TransactionDate DESC
В то время я решил отказаться, в том числе и TransactionId DESC
в конце, полагая, что, хотя это может помочь в тесте № 3 (который определяет разрыв TransactionId
связей по самым последним - хорошо, «самое последнее» предполагается, поскольку не указано явно, но всем кажется, согласиться с этим предположением), вероятно, будет недостаточно связей, чтобы изменить ситуацию.
Но затем Аарон еще раз проверил со вспомогательным индексом, который включал TransactionId DESC
и обнаружил, что CROSS APPLY
метод был победителем во всех трех тестах. Это отличалось от моего тестирования, которое показало, что метод CTE был лучшим для теста № 3 (когда не использовалось кэширование, что отражает тест Аарона). Было ясно, что существует дополнительный вариант, который необходимо протестировать.
Я удалил текущий поддерживающий индекс, создал новый с TransactionId
и очистил кэш плана (просто чтобы убедиться):
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;
Я перезапустил Тест № 1, и результаты оказались такими же, как и ожидалось. Затем я перезапустил Тест № 3, и результаты действительно изменились:
Приведенные выше результаты относятся к стандартному тесту без кэширования. На этот раз не только CROSS APPLY
побил CTE (как показал тест Аарона), но и процесс SQLCLR взял на себя инициативу на 30 операций чтения (ух-ху).
Приведенные выше результаты относятся к тесту с включенным кэшированием. На этот раз производительность CTE не ухудшилась, хотя CROSS APPLY
все же превосходит его. Тем не менее, теперь процесс SQLCLR лидирует на 23 операции чтения (опять же, ух ты).
Убери прочь
Существуют различные варианты использования. Лучше всего попробовать несколько, так как у каждого из них есть свои сильные стороны. Тесты, проведенные здесь, показывают довольно небольшую разницу в чтениях и продолжительности между лучшими и худшими показателями во всех тестах (с поддерживающим индексом); изменение в чтениях составляет около 350, а продолжительность составляет 55 мс. Хотя процесс SQLCLR победил во всех тестах, кроме 1 (с точки зрения операций чтения), сохранение только нескольких операций чтения обычно не стоит затрат на обслуживание при прохождении маршрута SQLCLR. Но в AdventureWorks2012 Product
таблица имеет только 504 строки и TransactionHistory
только 113 443 строки. Разница в производительности между этими методами, вероятно, становится более заметной по мере увеличения числа строк.
Хотя этот вопрос был специфичен для получения определенного набора строк, не следует упускать из виду, что самым большим фактором производительности является индексирование, а не конкретный SQL. Хороший индекс должен быть на месте, прежде чем определить, какой метод действительно лучше.
Самый важный урок, найденный здесь, не о CROSS APPLY против CTE против SQLCLR: это о ТЕСТИРОВАНИИ. Не думай Получите идеи от нескольких людей и протестируйте как можно больше сценариев.