我采用了一种略有不同的方法,主要是看该技术与其他技术的比较,因为选择不错,对吗?
测试
我们为什么不首先看各种方法如何相互对立。我做了三组测试:
- 第一组运行时没有数据库修改
- 第二组是在创建索引以
TransactionDate
针对的基于支持的查询之后运行的Production.TransactionHistory
。
- 第三组假设略有不同。由于所有三个测试都针对相同的产品列表运行,因此如果我们缓存该列表怎么办?我的方法使用内存缓存,而其他方法使用等效的临时表。为第二组测试创建的支持索引对于该组测试仍然存在。
其他测试详细信息:
- 这些测试是
AdventureWorks2012
在SQL Server 2012 SP2(开发人员版)上运行的。
- 对于每个测试,我都标记了我从谁那里得到的答案以及该查询是哪个特定查询。
- 我使用了“查询选项” |“执行后丢弃结果”选项 结果。
- 请注意,对于前两组测试,
RowCounts
我的方法显示为“关闭”。这是由于我的方法是手动执行操作CROSS APPLY
:它针对进行初始查询Production.Product
并返回161行,然后将其用于进行查询Production.TransactionHistory
。因此,RowCount
我的条目的值始终比其他条目多161。在第三组测试(带缓存)中,所有方法的行数均相同。
- 我使用SQL Server Profiler来捕获统计信息,而不是依赖执行计划。亚伦(Aaron)和米凯尔(Mikael)在展示他们的查询计划方面已经做得非常出色,并且无需复制该信息。我的方法的目的是将查询简化为一个简单的形式,以至于实际上并不重要。使用Profiler的另一个原因是,稍后将提到。
Name >= N'M' AND Name < N'S'
我选择使用Name LIKE N'[M-R]%'
,而不是使用构造,而SQL Server对待它们的方式相同。
结果
没有支持指数
这本质上是开箱即用的AdventureWorks2012。在所有情况下,我的方法显然都比其他方法更好,但从来没有比前一种或前两种方法更好。
测试1
亚伦的CTE显然是赢家。
测试2
Aaron的CTE(再次)和Mikael的第二种apply row_number()
方法紧随其后。
测试3
亚伦的CTE(再次)是获胜者。
结论
在没有支持指标的情况下TransactionDate
,我的方法比制定标准更好CROSS APPLY
,但是仍然可以使用CTE方法。
有支持索引(无缓存)
对于这组测试,我添加了明显的索引,TransactionHistory.TransactionDate
因为所有查询都在该字段上排序。我说“显而易见”,因为大多数其他答案也同意这一点。而且由于查询都需要最新的日期,因此TransactionDate
应该对字段进行排序DESC
,因此我只是抓住了CREATE INDEX
Mikael答案底部的语句,并添加了一个明确的内容FILLFACTOR
:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
一旦建立了该索引,结果就会发生很大变化。
测试1
这次至少在逻辑读取方面是我的方法。该CROSS APPLY
方法以前在测试1中表现最差,在持续时间方面胜出,甚至在逻辑读取方面胜过CTE方法。
测试2
这次,这是Mikael的第apply row_number()
一种方法,它是看Reads的赢家,而以前它是表现最差的一种。现在,在查看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
在这里,我们现在看到了阵容上的差异,因为我的方法勉强领先,仅比Mikael的第apply row_number()
一种方法好2读,而没有缓存,我的方法落后4读。
测试3
请参阅底部的更新(在行下方)。在这里,我们再次看到了一些区别。与Aaron的CROSS APPLY方法相比,我的方法的“参数化”风格现在几乎没有2次读取领先(没有缓存,它们是相等的)。但真正奇怪的是,我们第一次看到一种受缓存负面影响的方法:Aaron的CTE方法(以前是测试3的最佳方法)。但是,我不会在不适当的地方使用它,并且由于没有缓存,Aaron的CTE方法仍然比我的缓存方法要快,因此针对这种特殊情况的最佳方法似乎是Aaron的CTE方法。
结论 请参见底部的更新(在此行下方)
,重复使用辅助查询结果的情况通常(但不总是)受益于缓存这些结果。但是当缓存是一个好处时,使用内存进行缓存比使用临时表具有一些优势。
方法
通常
我从“详细”查询(即获取s和s)中分离出“标头”查询(即,获取ProductID
s,在某些情况下还DaysToManufacture
基于Name
以某些字母开头的)。其概念是执行非常简单的查询,并且不允许优化器在加入查询时感到困惑。显然,这并不总是有利的,因为它也不允许优化器进行优化。但是正如我们在结果中看到的那样,根据查询的类型,此方法确实有其优点。TransactionID
TransactionDate
此方法的各种风味之间的区别是:
常量:提交任何可替换的值作为内联常量而不是参数。这将ProductID
在所有三个测试中以及在测试2中返回的行数中进行引用,因为这是“乘以DaysToManufacture
乘积属性的五倍”的函数。此子方法意味着每个人都有ProductID
自己的执行计划,如果的数据分布差异很大,这将是有益的ProductID
。但是,如果数据分布几乎没有变化,那么生成额外计划的成本可能就不值得了。
参数化:至少提交ProductID
为@ProductID
,以允许执行计划缓存和重用。还有一个额外的测试选项,也可以将要返回测试2的可变行数作为参数。
优化未知:当引用ProductID
为时@ProductID
,如果数据分布差异很大,则可以缓存对其他ProductID
值有负面影响的计划,因此最好知道使用此查询提示是否有帮助。
缓存产品:与其Production.Product
每次都查询表,不如要获取完全相同的列表,而是运行一次查询(在我们查询时,过滤掉ProductID
甚至不在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
of 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(不使用缓存时,反映了Aaron的测试)。显然,还有其他变化需要测试。
我删除了当前的支持索引,使用创建了一个新索引,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(正如Aaron的测试所示),而且SQLCLR proc领先30 Reads(woo hoo)。
以上结果用于启用缓存的测试。这次CTE的性能没有下降,尽管CROSS APPLY
仍然胜过它。但是,现在SQLCLR进程以23次读取领先(再次呼呼)。
拿走
有多种选择。最好尝试几种,因为它们各有所长。此处进行的测试显示,在所有测试中,表现最佳和表现最差的阅读器和持续时间之间的差异都很小(带有支持指数);读取的变化约为350,持续时间为55 ms。尽管SQLCLR proc确实在1次测试中均获胜(就读取而言),但仅保存少量读取通常不值得使用SQLCLR路由的维护成本。但是在AdventureWorks2012中,该Product
表只有504行,TransactionHistory
只有113,443行。随着行数的增加,这些方法之间的性能差异可能会变得更加明显。
尽管此问题特定于获取一组特定的行,但不应忽视的是,性能中的最大因素是索引而不是特定的SQL。在确定哪种方法真正最佳之前,需要有一个好的索引。
在这里找到的最重要的一课不是关于CROSS vs CTE和SQLCLR,而是关于TESTING。不要假设 从几个人那里获得想法,并尽可能多地测试场景。