TSQL:有没有办法限制返回的行并计算在没有限制的情况下返回的总数(不将其添加到每一行)?

Posted

技术标签:

【中文标题】TSQL:有没有办法限制返回的行并计算在没有限制的情况下返回的总数(不将其添加到每一行)?【英文标题】:TSQL: Is there a way to limit the rows returned and count the total that would have been returned without the limit (without adding it to every row)? 【发布时间】:2015-01-20 02:45:38 【问题描述】:

我正在更新当前最多选择 n 行的存储过程,如果返回的行 = n,则执行无限制的选择计数,然后返回原始选择和受影响的总行数。

有点像:

SELECT TOP (@rowsToReturn)
    A.data1,
    A.data2
FROM
    mytable A

SET @maxRows = @@ROWCOUNT
IF @rowsToReturn = @@ROWCOUNT
BEGIN
  SET @maxRows = (SELECT COUNT(1) FROM mytableA)
END    

我希望将其简化为单个 select 语句。基于this question,COUNT(*) OVER() 允许这样做,但它被放置在每一行而不是输出参数中。也许像 mysql 中的FOUND_ROWS(),比如@@TOTALROWCOUNT 之类的。

附带说明,由于实际的 select 有一个 order by,数据库将需要已经遍历整个集合(以确保它获得正确的前 n 个有序记录),所以数据库应该已经有了这个数数。

【问题讨论】:

在每一行的末尾包含数据可能更有效,因为另一种方法是执行两次查询逻辑。 没有一种直接的方式不需要运行两次查询或具体化到临时表中然后选择所需的行。如果计划有阻塞排序,理论上你可以收集实际的执行计划并解析进入排序的实际行数,但我不认真推荐。 为什么不直接从跟踪表中总行数的sys表中选择呢? @Zane 为了简单起见,我省略了 where 子句和所有连接。如果这只是计算表的行数,那将不是问题。 @@ROWCOUNT 在每条语句之后都会被重置,所以除非@RowsToReturn 为 1,否则第二次检查永远不会像你想象的那样工作...... 【参考方案1】:

这个怎么样....

DECLARE @N INT = 10

;WITH CTE AS
 (
  SELECT 
    A.data1,
    A.data2
  FROM  mytable A 
 )
SELECT TOP (@N) * , (SELECT COUNT(*) FROM CTE) Total_Rows
FROM CTE 

最后一列将填充没有 TOP 子句将返回的总行数。

您的要求的问题是,您期望 SINGLE select 语句返回一个表和一个标量值。这是不可能的。

单个选择语句将返回一个表或一个标量值。或者,您可以有两个单独的选择,一个返回标量值,另一个返回标量。选择是你的:)

【讨论】:

您是否错过了“不将其添加到每一行”? CTE 只是语法。它将被评估两次。更糟糕的情况是每一行。 对不起,我确实错过了“没有将其添加到每一行”。【参考方案2】:

仅仅因为您认为 TSQL 应该因为排序而具有行数并不意味着它确实如此。如果它这样做了,它当前不会与外界共享它。

你缺少的是这是非常有效的

select count(*) 
from ...
where ...
select top x 
from ...
where ... 
order by ...

使用 count(*) 除非查询很丑,否则这些索引应该在内存中。

它必须执行计数才能根据什么进行排序? 您是否真的评估过任何查询计划? 如果 TSQL 必须执行排序,请解释以下内容。 当第二个无论如何都要进行计数时,为什么计数(*)是成本的 100%? 在第二个查询计划中的哪个位置有免费计算的机会? 如果它们都需要计数,为什么这些查询计划会如此不同?

【讨论】:

我请求丑陋到二级。附带说明一下,目前我们仅在返回的最大记录数等于或更多时才重新运行查询。每次运行两次不会更糟吗? 如果计数是 le 那么 max 能有多丑?如果它是如此丑陋以至于不在内存中,那么您的查询问题就没有计数问题。你真的知道事实查询不在内存中吗?你测试过吗?这对我来说确实像是过早的优化。我对一些命中超过 1 亿行的表的查询就是这样做的。 @Lawtonfogle 和 Blam:这里有一些不准确之处。 SQL Server 不缓存结果集,只缓存它使用的数据页和执行计划。这两个计划是不同的,因为查询本身是不同的。并且先做一个COUNT(*) 然后查询有时是有效的,但有时不是。这完全取决于查询和索引等。更改它,您会看到很大的不同。然而,Lawtonfogle 正确的。 SQL Server 必须在排序之前拥有整个结果集(这是第二遍操作)。在 XML 计划中应用 TOP 之前的总行数 @srutzky 我没有说它缓存了结果 - 我说索引仍在内存中。它显然不必像查询计划所示的那样计数。并通过相对于批次的成本来反映。计数比顶部更昂贵 - 如果顶部需要计数,它会更贵。批量成本不依赖于订单。买一个元音 - 如果我按颜色排序然后年龄并在到达第二种颜色之前到达顶部我停止并且 tsql 显然也足够聪明地停止 - 看到索引在计划中的早期搜索和 1% 的最高成本. @srutzky 真的“SQL Server 必须在排序之前拥有整个结果集(这是第二遍操作)”。哪里有证据证明这是二次通过操作?至于“在订购之前必须拥有整个结果集”,让我举一个简单的例子来证明这是错误的 - 以标识列作为 pk 的表,第二列作为 varchar 名称。按 Iden 从表中选择前 1 个名称。它执行索引查找,获取一行并停止。为什么它需要评估每一行?【参考方案3】:

我认为有一种神秘的方式可以做你想做的事。它涉及触发器和非临时表。而且,我应该提一下,虽然我已经实现了每个部分(用于不同的目的),但我从未为此目的将它们放在一起。

这个想法始于Stack Overflow question。根据这个来源,@@ROWCOUNT 会计算 尝试 插入的次数,即使它们并没有真正发生。现在,我必须承认,对可用文档的细读似乎并没有涉及到这个话题,所以这可能是也可能不是“正确”的行为。这个方法就是依赖这个“问题”。

所以,你可以做你想做的事:

为输出创建一个新表 - 但不是表变量或临时表。 创建一个“代替”触发器,以防止超过 @maxRows 进入表。 将查询结果选择到表格中。 在select 之后阅读@@ROWCOUNT

请注意,您可以使用动态 SQL 创建表和触发器。您也可以创建一次,然后让触发器从某种参数表中读取@maxRows 值。如前所述,这需要是一个支持触发器的真实表。

【讨论】:

对不起,本来想早点评论的,但是分心了。基本上,与仅向现有 proc 添加几行代码以将完整结果转储到临时表、获取 @@ROWCOUNT,然后从临时表中选择子集相比,此解决方案没有任何好处。相反,它非常复杂(比规定的要复杂)并且由于额外的 I/O 会导致性能更差。它纯粹是学术性的,不应该在实时系统上使用。由于人们并不总是阅读 cmets 和/或理解提案的含义,因此我表示这是一种双输的情况,不应尝试。 @srutzky 。 . .一点也不真实。此解决方案不会将附加数据写入表,仅计算附加行。这可能是性能上的显着差异。 戈登,我没有说您的解决方案会将数据写入“真实”表。但它确实在某个地方。考虑 SQL Server 正在做什么,而不是考虑净效应。你应该尝试实际测试这个想法。我有并且它按预期执行:比临时表差得多(逻辑读取的 6 倍 - 7 倍)。这甚至不考虑让您的解决方案为多个用户工作所需的清理步骤。也许您可以先解释一下如何“防止超过@maxRows 进入表格”。 @srutzky 。 . .我无法反驳您声称已经测试过它的说法(我什至不认为这个答案给人的印象是“这是一个好主意”,只是“这似乎是可能的”)。但是instead of 触发器不会写入日志,不会更新索引,也不会为未插入表中的数据分配新页面。这似乎是一种节省。 re:“我无法反驳您声称已经测试过它的说法”:this 是主要问题。你没有测试过。否则你会知道它非常低效且过于复杂。你不应该说它“似乎是一种储蓄”。您应该可以说它节省,因为您已经看到了。事实是,这是一个半生不熟的想法,你不知道,因为 a) 你不相信我,b) 你自己没有试过才知道。我和 SQL Server 都不关心您或其他任何人认为会发生什么;代码做它做的事情。让这个工作,然后我们可以讨论。【参考方案4】:

正如@MartinSmith 在对此问题的评论中提到的那样,没有直接(即纯 T-SQL)方法来获取将返回的总行数,同时限制它。过去我做过的方法是:

将查询转储到临时表以获取@@ROWCOUNT(总集) 在主查询的有序结果上使用ROW_NUBMER() AS [ResultID] SELECT TOP (n) FROM #Temp ORDER BY [ResultID] 或类似的东西

当然,这里的缺点是您有将这些记录放入临时表的磁盘 I/O 成本。将[tempdb] 放在SSD 上? :)

我也经历过“先用相同的查询运行 COUNT(*),然后运行常规的 SELECT”方法(正如@Blam 所提倡的),它不是“免费”重新运行查询:

在许多情况下,它会完全重新运行。问题在于,在执行COUNT(*)(因此不返回任何字段)时,优化器只需要担心 JOIN、WHERE、GROUP BY、ORDER BY 子句方面的索引。但是,当您想要返回一些实际数据时,可能会相当大地改变执行计划,特别是如果用于获取 COUNT(*) 的索引没有“覆盖”SELECT 列表中的字段. 另一个问题是,即使索引都相同,因此所有数据页仍在缓存中,这只是将您从物理读取中拯救出来。但是你仍然有逻辑读取。

我并不是说这种方法不起作用,但我认为问题中仅有条件地执行COUNT(*) 的方法对系统的压力要小得多。

@Gordon 提倡的方法实际上在功能上和我上面描述的 temp 表方法非常相似:它将完整的结果集转储到 [tempdb](INSERTED 表在 [tempdb] 中)以获得完整的@ 987654329@ 然后它得到一个子集。不利的一面是,INSTEAD OF TRIGGER 方法是:

很多需要设置更多工作(如 10 倍 - 20 倍更多):您需要一个真实的表格来表示每个不同的结果集,您需要一个触发器,触发器需要要么动态构建,要么从某个配置表中获取要返回的行数,或者我想它可以从CONTEXT_INFO() 或临时表中获取。尽管如此,整个过程还是相当多的步骤和复杂的。

非常效率低下:首先,它将完整的结果集转储到表中(即到 INSERTED 表中 - 它位于 [tempdb] 中),但是然后它会执行一个额外的步骤来选择所需的记录子集(这并不是真正的问题,因为它仍然应该在缓冲池中)以返回到真实表中。更糟糕的是,第二步实际上是双 I/O,因为该操作也在该真实表所在的数据库的事务日志中表示。但是等等,还有更多:下一次运行查询呢?你需要清除这张真实的桌子。无论是通过DELETE 还是TRUNCATE TABLE,它都是在事务日志中显示的另一个操作(基于使用这两个操作中的哪一个的表示量),加上额外操作所花费的额外时间。并且,我们不要忘记从INSERTED 中选择子集到真实表中的步骤:它没有机会使用索引,因为您无法索引INSERTEDDELETED 表。并不是说您总是想向临时表添加索引,但有时它会有所帮助(取决于具体情况),而且您至少可以选择。

过于复杂:当两个进程需要同时运行查询时会发生什么?如果它们共享同一个真实表以转储到然后从中选择最终输出,则需要添加另一列来区分 SPID。可能是@@SPID。或者它可能是在调用实际表中的初始 INSERT 之前创建的 GUID(以便它可以通过 CONTEXT_INFO() 或临时表传递给 INSTEAD OF 触发器)。无论值是什么,一旦选择了最终输出,它将用于执行DELETE 操作。如果不明显,这部分会影响上一个项目符号中提出的性能问题:TRUNCATE TABLE 不能使用,因为它会清除整个表,而将DELETE FROM dbo.RealTable WHERE ProcessID = @WhateverID; 作为唯一选项。

现在,公平地说,可能在触发器本身内执行最终的 SELECT。这将减少一些低效率,因为数据永远不会进入真实表,然后也永远不需要删除。它还减少了过度复杂化,因为不需要通过 SPID 分隔数据。然而,这是一个非常有时间限制的解决方案,因为在 SQL Server 的下一个版本中,从触发器中返回结果的能力将会再见,所以说 disallow results from triggers Server Configuration Option 的 MSDN 页面:

此功能将在 Microsoft SQL Server 的下一版本中删除。不要在新的开发工作中使用此功能,并尽快修改当前使用此功能的应用程序。我们建议您将此值设置为 1。

唯一实际的做法:

查询一次 获取行的子集 仍然得到完整结果集的总行数

是使用.Net。如果从应用程序代码调用 procs,请参阅底部的“EDIT 2”。如果您希望能够通过即席查询随机运行各种存储过程,那么它必须是一个 SQLCLR 存储过程,以便它可以是通用的并且适用于任何查询,因为存储过程可以返回动态结果集而函数不能。 proc 至少需要 3 个参数:

@QueryToExec NVARCHAR(MAX) @RowsToReturn INT @TotalRows INT 输出

这个想法是使用“Context Connection = true;”利用内部/进程内连接。然后,您执行以下基本步骤:

    致电ExecuteDataReader() 在阅读任何行之前,请执行GetSchemaTable() 您可以从 SchemaTable 获得结果集字段名称和数据类型 从您构造的结果集结构SqlDataRecord 用那个SqlDataRecord你打电话给SqlContext.Pipe.SendResultsStart(_DataRecord) 现在你开始打电话给Reader.Read() 对于您调用的每一行:
      Reader.GetValues() DataRecord.SetValues() SqlContext.Pipe.SendResultRow(_DataRecord) RowCounter++
    不是执行典型的“while (Reader.Read())”,而是包含@RowsToReturn 参数:while(Reader.Read() && RowCounter < RowsToReturn.Value) 在 while 循环之后,调用 SqlContext.Pipe.SendResultsEnd() 关闭结果集(您正在发送的结果集,而不是您正在阅读的结果集) 然后执行第二个 while 循环,循环遍历结果的其余部分,但永远不会获取任何字段: 而(Reader.Read()) 行计数器++; 然后只需设置TotalRows = RowCounter;,它将传回完整结果集的行数,即使您只返回了它的前 n 行:)

不确定这对临时表方法、双重调用方法,甚至 @M.Ali 的方法(我也尝试过并且有点喜欢,但问题是特定于 不是将值作为列发送),但它应该没问题,并且确实按要求完成了任务。

编辑: 甚至更好!另一种选择(上述 C# 建议的变体)是使用 T-SQL 存储过程中的@@ROWCOUNT,作为OUTPUT 参数发送,而不是循环遍历SqlDataReader 中的其余行。所以存储过程类似于:

CREATE PROCEDURE SchemaName.ProcName
(
   @Param1 INT,
   @Param2 VARCHAR(05),
   @RowCount INT OUTPUT = -1 -- default so it doesn't have to be passed in
)
AS
SET NOCOUNT ON;

any ol' query

SET @RowCount = @@ROWCOUNT;

然后,在应用程序代码中,为“@RowCount”创建一个新的 SqlParameter,Direction = Output。上面的编号步骤保持不变,除了最后两个(10 和 11)变为:

    而不是第二个while循环,只需调用Reader.Close() 不使用 RowCounter 变量,而是设置 TotalRows = (int)RowCountOutputParam.Value;

我已经尝试过了,它确实有效。但到目前为止,我还没有时间测试其他方法的性能。

编辑 2: 如果从应用层调用 T-SQL 存储过程(即不需要临时执行),那么这实际上是上述 C# 方法的更简单的变体。在这种情况下,您无需担心SqlDataRecordSqlContext.Pipe 方法。假设您已经设置了 SqlDataReader 来拉回结果,您只需要:

    确保 T-SQL 存储过程具有 @RowCount INT OUTPUT = -1 参数 确保在查询后立即SET @RowCount = @@ROWCOUNT; 将 OUTPUT 参数注册为具有 Direction = Output 的 SqlParameter 使用类似于:while(Reader.Read() && RowCounter < RowsToReturn) 的循环,这样您就可以在撤回所需数量后停止检索结果。 记住不要限制存储过程中的结果(即没有TOP (n)

此时,就像上面第一个“编辑”中提到的那样,只需关闭 SqlDataReader 并获取 OUTPUT 参数的 .Value :)。

【讨论】:

@srutsky 。 . .我不认为以“我认为有一种神秘的方式可以做你想做的事”开头的答案真的是倡导任何事情。你能把答案中的那个词改一下吗?

以上是关于TSQL:有没有办法限制返回的行并计算在没有限制的情况下返回的总数(不将其添加到每一行)?的主要内容,如果未能解决你的问题,请参考以下文章

SQL CE:限制查询中返回的行

如何在 JDBC 数据源级别限制从 Oracle 返回的行数?

Google Bigquery 命令行返回限制

如何将 extjs 网格限制为仅显示 100 行并提供所有行作为下载?

有没有办法可以限制多列ul显示中的行数?

使用 Fill in ADOMD 限制检索的行数