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

Law*_*gle 9 sql t-sql sql-server sqlclr sql-server-2008

我正在努力更新当前选择最多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    
Run Code Online (Sandbox Code Playgroud)

我想把它减少到一个select语句.基于这个问题,COUNT(*) OVER()允许这个,但是它被放在每一行而不是输出参数中.也许像FOUND_ROWS()MYSQL中的东西,比如@@ TOTALROWCOUNT等.

作为旁注,由于实际选择具有order by,数据库将需要已经遍历整个集合(以确保它获得正确的前n个有序记录),因此数据库应该已经在某处具有此计数.

Sol*_*zky 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方法是:

  • 很多更多的工作来设置(在10倍- 20倍以上):你需要一个真正的表来表示每个不同的结果集,你需要一个触发,触发需要既可以动态构建,或者得到的行数返回从一些配置表,或者我想它可以从CONTEXT_INFO()临时表中获取它.尽管如此,整个过程还是相当多的步骤而且令人费解.

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

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

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

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


唯一的实际方法:

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

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

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

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

  1. 呼叫 ExecuteDataReader()
  2. 在你读任何行之前,做一个 GetSchemaTable()
  3. 从SchemaTable中获取结果集字段名称和数据类型
  4. 从结构集结构中构造一个 SqlDataRecord
  5. SqlDataRecord你打电话SqlContext.Pipe.SendResultsStart(_DataRecord)
  6. 现在你开始打电话了 Reader.Read()
  7. 对于您调用的每一行:
    1. Reader.GetValues()
    2. DataRecord.SetValues()
    3. SqlContext.Pipe.SendResultRow(_DataRecord)
    4. RowCounter++
  8. 而不是做典型的" while (Reader.Read())",而是包含@RowsToReturn参数:while(Reader.Read() && RowCounter < RowsToReturn.Value)
  9. 在循环之后,调用SqlContext.Pipe.SendResultsEnd()以关闭结果集(您要发送的结果集,而不是您正在阅读的结果集)
  10. 然后执行第二个while循环,循环遍历结果的其余部分,但永远不会得到任何字段:while(Reader.Read()){RowCounter ++; }
  11. 然后设置 TotalRows = RowCounter;哪个将传回完整结果集的行数,即使你只返回它的前n行:)

不知道这对于临时表方法,双重调用方法,甚至是@ M.Ali的方法(我也尝试了类似,但问题是特定于将值作为列发送)的表现如何,但它应该没问题,并按要求完成任务.

编辑:
更好!另一个选项(上述C#建议的变体)是使用@@ROWCOUNT来自T-SQL存储过程,作为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;
Run Code Online (Sandbox Code Playgroud)

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

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

我试过这个,它确实有效.但到目前为止,我没有时间来测试其他方法的性能.

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

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

那时,就像上面第一个"编辑"中提到的那样,只需关闭SqlDataReader并获取.ValueOUTPUT参数:).