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

时间:2014-11-21 20:49:11

标签: sql sql-server sql-server-2008 tsql sqlclr

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

有点像:

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 questionCOUNT(*) OVER()允许这样做,但它放在每一行而不是输出参数中。也许类似于MYSQL中的FOUND_ROWS(),例如@@ TOTALROWCOUNT等。

作为旁注,由于实际的select有一个order by,数据库需要已经遍历整个set(以确保它获得正确的前n个有序记录),所以数据库应该已经有了这个算在某处。

4 个答案:

答案 0 :(得分:7)

正如@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倡导的方法实际上与我上面描述的临时表方法非常相似:它将完整的结果集转储到[tempdb](INSERTED表在[tempdb]中)以获得完整@@ROWCOUNT然后它获得一个子集。在缺点方面,INSTEAD OF TRIGGER方法是:

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

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

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

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

      

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


唯一可行的方法:

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

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

  • @QueryToExec NVARCHAR(MAX)
  • @RowsToReturn INT
  • @TotalRows INT OUTPUT

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

  1. 致电DELETE FROM dbo.RealTable WHERE ProcessID = @WhateverID;
  2. 在您阅读任何行之前,请执行ExecuteDataReader()
  3. 从SchemaTable中获取结果集字段名称和数据类型
  4. 从结构集结构中构建GetSchemaTable()
  5. 使用SqlDataRecord来调用SqlDataRecord
  6. 现在您开始致电SqlContext.Pipe.SendResultsStart(_DataRecord)
  7. 您呼叫的每一行:
    1. Reader.Read()
    2. Reader.GetValues()
    3. DataRecord.SetValues()
    4. SqlContext.Pipe.SendResultRow(_DataRecord)
  8. 而不是做典型的" RowCounter++",而是包含@RowsToReturn参数:while (Reader.Read())
  9. 在循环之后,调用while(Reader.Read() && RowCounter < RowsToReturn.Value)关闭结果集(您要发送的结果集,而不是您正在阅读的结果集)
  10. 然后执行第二个while循环,循环遍历结果的其余部分,但从不获取任何字段: 而(Reader.Read()) {    RowCounter ++; }
  11. 然后设置SqlContext.Pipe.SendResultsEnd(),它将传回完整结果集的行数,即使您只返回了它的前n行:)
  12. 不确定这是如何针对临时表方法,双重调用方法,甚至是@ M.Ali的方法(我也尝试了类似的方法),但问题是针对而不是将值作为列发送,但它应该没问题,并按要求完成任务。

    修改
    更好!另一个选项(上述C#建议的变体)是使用T-SQL存储过程中的TotalRows = RowCounter;,作为@@ROWCOUNT参数发送,而不是循环遍历OUTPUT中的其余行。 {1}}。因此存储过程类似于:

    SqlDataReader

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

    1. 而不是第二个while循环,只需调用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;
    2. 即可
    3. 不使用RowCounter变量,而是设置Reader.Close()
    4. 我试过这个并且确实有效。但到目前为止,我没有时间来测试其他方法的性能。

      编辑2:
      如果从应用层调用T-SQL存储过程(即不需要临时执行),那么这实际上是上述C#方法的更简单的变化。在这种情况下,您不必担心TotalRows = (int)RowCountOutputParam.Value;SqlDataRecord方法。假设您已设置SqlContext.Pipe设置以撤回结果,您只需:

      1. 确保T-SQL存储过程具有@RowCount INT OUTPUT = -1参数
      2. 确保在查询后立即SqlDataReader
      3. 将OUTPUT参数注册为具有Direction = Output
      4. SET @RowCount = @@ROWCOUNT;
      5. 使用类似于SqlParameter的循环,以便在撤回所需金额后停止检索结果。
      6. 请记住限制存储过程中的结果(即没有while(Reader.Read() && RowCounter < RowsToReturn)
      7. 就在那时,就像第一个&#34; EDIT&#34;在上面,只需关闭TOP (n)并获取OUTPUT参数的SqlDataReader :.

答案 1 :(得分:2)

这个怎么样....

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语句返回一个表和一个标量值。这是不可能的。

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

答案 2 :(得分:1)

仅仅因为你认为TSQL应该有一个行数,因为它的排序并不意味着它。如果确实如此,它目前还没有与外界分享。

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

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

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

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

enter image description here

答案 3 :(得分:0)

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

这个想法从这个Stack Overflow question开始。根据此消息来源,@@ROWCOUNT计算尝试插入的数量,即使它们确实没有发生。现在,我必须承认,仔细阅读可用文档似乎没有触及这个主题,所以这可能是也可能不是“正确”的行为。这种方法依赖于这个“问题”。

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

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

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