Я застосував трохи інший підхід, головним чином, щоб побачити, як ця методика буде порівнюватись з іншими, адже наявність варіантів добре, чи не так?
Тестування
Чому б нам не почати, просто роздивившись, як різні методи складаються один проти одного. Я зробив три набори тестів:
- Перший набір працював без модифікацій БД
- Другий набір пройшов після створення індексу для підтримки
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. У всіх випадках мій метод явно кращий, ніж деякі інші, але ніколи не такий хороший, як топ-2 або 2 способи.
Тест 1
CTE Аарона тут явно переможець.
Тест 2
АТС Аарона (знову ж таки) і другий 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()
метод Мікаїла переможець, коли дивиться на «Читання», тоді як раніше це був один із найгірших виконавців. І тепер мій метод посідає дуже близьке друге місце, коли дивлюся на «Читання». Насправді, за межами методу 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 Aaron (без кешування вони були рівними). Але насправді дивним є те, що ми вперше бачимо метод, на який негативно впливає кешування: метод CTE Аарона (який раніше був найкращим для тесту № 3). Але я не збираюся брати кредит там, де цього не потрібно, і оскільки без кешування метод CTE Аарона все ще швидший, ніж у мене метод кешування, найкращим підходом для цієї конкретної ситуації є метод АТР Аарона.
Висновок Перегляньте оновлення донизу (під рядком)
Ситуації, які повторно використовують результати вторинного запиту, можуть часто (але не завжди) отримувати користь від кешування цих результатів. Але якщо кешування - це користь, використання пам'яті для згаданого кешування має певну перевагу перед використанням тимчасових таблиць.
Метод
Взагалі
Я відокремив запит "заголовка" (тобто отримання ProductID
s, і в одному випадку також DaysToManufacture
, на основі Name
початку з певних літер) від "детальних" запитів (тобто отримання TransactionID
s і TransactionDate
s). Концепція полягала в тому, щоб виконувати дуже прості запити і не дозволяти оптимізатору плутатись при приєднанні до них. Зрозуміло, що це не завжди вигідно, оскільки це також відключає оптимізатор від, ну, оптимізації. Але, як ми бачили в результатах, залежно від типу запиту, у цього методу є свої достоїнства.
Різниця між різними ароматами цього способу полягає в:
Константи: подайте будь-які змінні значення як вбудовані константи, а не параметри. Це стосується ProductID
всіх трьох тестів, а також кількість рядків для повернення в тесті 2, оскільки це функція "п’ять разів більше DaysToManufacture
атрибута Product". Цей під-метод означає, що кожен ProductID
отримає свій власний план виконання, що може бути корисним, якщо є широка різниця в розподілі даних для ProductID
. Але якщо в розповсюдженні даних є невеликі розбіжності, вартість створення додаткових планів, швидше за все, не варто.
Параметризовано: Надішліть принаймні ProductID
так @ProductID
, що дозволяє кешувати план виконання та використовувати повторно. Існує додатковий варіант тестування, щоб також розглядати змінну кількість рядків для повернення для тесту 2 як параметр.
Оптимізація невідомого: Якщо посилатися на ProductID
як @ProductID
, якщо існує велика різниця розподілу даних, то можна кешувати план, який негативно впливає на інші ProductID
значення, тому було б добре знати, чи допомагає використання цього підказки запитів.
Продукти кешування: Замість того, щоб запитувати Production.Product
таблицю кожен раз, лише щоб отримати абсолютно той самий список, запустіть запит один раз (і поки ми в ньому, відфільтруйте всі ProductID
, яких немає навіть у TransactionHistory
таблиці, щоб ми не витрачали жодного ресурси там) і кешувати цей список. Список повинен містити DaysToManufacture
поле. Використовуючи цю опцію, для першого виконання є трохи вищий початковий хіт на Logical Reads, але після цього TransactionHistory
запитується лише таблиця.
Конкретно
Гаразд, але так, гм, як можна видавати всі підзапити як окремі запити без використання CURSOR і скидання кожного результату, встановленого до тимчасової таблиці або змінної таблиці? Очевидно, що використання методу CURSOR / Temp Table відображається цілком очевидно у "Read and Writes". Ну, використовуючи 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 proc взяв на себе лідерство на 30 читачів (ву-ху).
Наведені вище результати для тесту з увімкненим кешуванням. Цього разу ефективність роботи CTE не погіршується, хоча вона CROSS APPLY
все ще перемагає її. Однак зараз SQLCLR Pro переймає лідируючі позиції 23-х читачів (знову ж таки).
Забирай
Існують різні варіанти використання. Найкраще спробувати декілька, оскільки кожен має свої сильні сторони. Тести, зроблені тут, демонструють досить малу різницю як у режимах читання, так і в тривалості між найкращими та найгіршими виконавцями у всіх тестах (із допоміжним показником); варіація читання становить приблизно 350, а тривалість - 55 мс. Хоча Pro SQLCLR виграв у всіх, окрім 1 тесту (з точки зору "Читання"), заощадження лише декількох читань зазвичай не вартує витрат на обслуговування проходження маршруту SQLCLR. Але в AdventureWorks2012 Product
таблиця налічує лише 504 рядки і TransactionHistory
містить лише 113 443 рядки. Різниця в ефективності цих методів, ймовірно, стає більш вираженою в міру збільшення кількості рядків.
Хоча це питання було специфічним для отримання певного набору рядків, не слід забувати, що найбільшим фактором продуктивності був індексація, а не конкретний SQL. Перш ніж визначити, який метод є справді найкращим, повинен бути хороший показник.
Тут найважливіший урок - це не про CROSS APPLY vs CTE проти SQLCLR: це про тестування. Не припускайте. Отримайте ідеї від кількох людей і протестуйте якомога більше сценаріїв.