V G*_*V G 10 sql-server-2008 sql-server window-functions
我有一个COUNT(*) OVER ()
作为查询的一部分来获取大量数据,其中包含大约 15 个表的连接(有些大,有些小)。这里计数的最佳解决方案是什么?
Joe*_*ish 10
COUNT(*) OVER ()
是那些听起来对查询优化器来说应该很便宜的操作之一。毕竟,SQL Server 已经知道查询返回了多少行。您只是要求它将该值投影到查询的结果集中。不幸的是,这COUNT(*) OVER ()
可能是一项昂贵的操作,具体取决于您将其添加到的查询。
对于一个简单的测试,我会将适量的测试放入表格中。真的任何表格都可以,但这是我在家中以下人员的示例数据:
SELECT
CAST(REPLICATE('A', 100) AS VARCHAR(100)) ID1
, CAST(REPLICATE('Z', 100) AS VARCHAR(100)) ID2
INTO #JUNK_DATA
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
Run Code Online (Sandbox Code Playgroud)
丢弃结果集时,此查询需要 2.5 秒:
SELECT ID1, ID2
FROM #JUNK_DATA;
Run Code Online (Sandbox Code Playgroud)
此查询在串行运行时需要 2.7 秒,但可以并行化:
SELECT COUNT(*)
FROM #JUNK_DATA
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)
COUNT
在我的机器上完成带有聚合查询的查询需要 47.2 秒。
SELECT ID1, ID2, COUNT(*) OVER () CNT
FROM #JUNK_DATA;
Run Code Online (Sandbox Code Playgroud)
也许我的机器有问题,但它肯定在做更多的工作。这是查询计划:
SQL Server 将所有列值加载到表假脱机中,然后读取该数据两次并将其连接在一起。当它已经知道结果集中有多少行时,为什么还要做所有这些工作?
SQL Server 查询计划以流式方式一次(或多或少)处理一行数据。因此,如果我们计算行数,它可能如下所示:
????????????????????????????
? ID1 ? ID2 ? COUNT_SO_FAR ?
????????????????????????????
? A ? B ? 1 ?
? C ? D ? 2 ?
? E ? F ? 3 ?
? ... ? ... ? ... ?
? ZZZ ? ZZZ ? 6431296 ?
????????????????????????????
Run Code Online (Sandbox Code Playgroud)
该操作不需要单独的数据副本,但如何将 6431296 的最终值应用于之前的所有行?有时这个操作被实现为我们在查询计划中看到的双假脱机。可以想象我们希望 SQL Server 使用更高效的内部算法,但我们显然无法直接控制它。
正如我所看到的,这里是您解决问题的选项:
修复应用程序。SQL Server 已经知道查询将返回多少行。使用@@ROWCOUNT
或其他一些方法是迄今为止最可取的。
运行单独的查询以获取计数。如果您将大型结果集返回给客户端并且保证您的数据不会在查询之间发生更改,那么这可能是一个不错的选择。分离COUNT
可能受益于并行性和相关表已经加载到缓冲区缓存中。根据表上的索引,SQL Server 可能能够执行更少的 IO 来获得计数,而不是完整的结果集。
将COUNT
聚合添加到查询中。如果您的典型结果集很小,这可能是一个不错的选择。这样您就不会将尽可能多的数据加载到假脱机中。
此答案的其余部分包含有关可以使用窗口函数完成的一些技巧的信息。并非所有这些都与您的 SQL Server 2008 版本相关。
简单的ROW_NUMBER()
加法不需要将结果集放入假脱机表中。如果你考虑一下前面的例子,那是有道理的。SQL Server 可以随时计算该值,而无需将值应用于前一行。这个查询在我的机器上运行了 3.1 秒:
SELECT
ID1
, ID2
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM #JUNK_DATA;
Run Code Online (Sandbox Code Playgroud)
我们可以在查询计划中看到这一点:
如果您将应用程序更改为简单地将 的最大值RN
作为行数,那么这对您来说可能是一个不错的选择。
您可能想ROW_NUMBER()
与 a 一起使用UNION ALL
来获取您在最后一个虚拟行中寻找的值,类似于您在问题中提出的内容。例如:
SELECT
t.ID1
, t.ID2
, CASE WHEN ROW_COUNT_ROW = 'Y' THEN -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) ELSE NULL END ROW_COUNT
FROM
(
SELECT
ID1
, ID2
, 'N' ROW_COUNT_ROW
FROM #JUNK_DATA
UNION ALL
SELECT
NULL ID1
, NULL ID2
, 'Y' ROW_COUNT_ROW
) t;
Run Code Online (Sandbox Code Playgroud)
上面的查询在 3.4 秒内完成并为我返回了正确的结果。但是,因为它依赖于未指定的连接顺序,所以它也可能返回错误的结果。看计划:
蓝色的查询部分必须是串联的顶部。红色的查询部分必须是下半部分。查询优化可以自由地重新排列查询计划或UNION
使用不同的物理运算符来实现,因此您可能不会总是得到正确的结果。
从 SQL Server 2012 开始,可以使用LEAD
窗口函数来完成类似于上述示例的操作:
SELECT
t.ID1
, t.ID2
, CASE WHEN LD IS NULL THEN RN ELSE NULL END ROW_COUNT
FROM
(
SELECT ID1, ID2
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
, LEAD(ID1) OVER (ORDER BY (SELECT NULL)) LD
FROM #JUNK_DATA
) t;
Run Code Online (Sandbox Code Playgroud)
这个查询避免了显式的表假脱机,感觉比另一个更安全。毕竟,为什么 SQL Server 会以不同的顺序处理窗口函数?但是,不能保证正确的结果,它在我的机器上在 12.5 秒内完成。
可以向ORDER
查询添加显式以始终获得正确的结果:
SELECT
t.ID1
, t.ID2
, CASE WHEN ORDER_BY_COL = 1 THEN -1 + ROW_NUMBER() OVER (ORDER BY ORDER_BY_COL) ELSE NULL END ROW_COUNT
FROM
(
SELECT
ID1
, ID2
, 0 ORDER_BY_COL
FROM #JUNK_DATA
UNION ALL
SELECT
NULL ID1
, NULL ID2
, 1 ORDER_BY_COL
) t
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)
但这需要在查询计划中使用昂贵的排序运算符:
查询在 21.5 秒内串行运行,如果我让它并行运行,它会完成得更快。
最后,从 SQL Server 2016 开始,可以为实现窗口函数的运算符添加批处理模式。我将创建一个空的 CCI 表并将其添加到查询中:
CREATE TABLE #ADD_BATCH_MODE (
ID INT NOT NULL,
INDEX CCI_BATCH CLUSTERED COLUMNSTORE
);
SELECT ID1, ID2, COUNT(*) OVER () ROW_COUNT
FROM #JUNK_DATA
LEFT OUTER JOIN #ADD_BATCH_MODE ON 1 = 0;
Run Code Online (Sandbox Code Playgroud)
这会诱使 SQL Server 对聚合使用批处理模式。这是计划:
查询当然看起来更简单。磁盘上的假脱机不再存在,它会在 5.4 秒内完成。向复杂查询添加批处理模式会产生其他影响,通常它们会是积极的,但如果不进行测试,您就无法知道。
Gri*_*ldi -1
这些分析函数的发明是为了一次性完成一些工作。因此,根据您的桌子的大小,这可能是正确的做法。
您必须检查查询优化器正在做什么。我见过一些案例,其中count(*)
是从最小的索引中检索的。青年MMV
归档时间: |
|
查看次数: |
13427 次 |
最近记录: |