如何在存储过程完成之前获得响应?

Bog*_*nov 9 stored-procedures t-sql sql-server-2012 multi-thread web-service

我需要在完成之前从存储过程返回部分结果(作为简单的选择)。

有可能这样做吗?

如果是,如何做到这一点?

如果没有,有什么解决方法吗?

编辑:我有几个部分的程序。在第一部分我计算了几个字符串。我稍后会在过程中使用它们来进行附加操作。问题是调用者尽快需要字符串。因此,我需要计算该字符串并将其传回(不知何故,例如从选择中),然后继续工作。调用者更快地获得其有价值的字符串。

调用者是一个 Web 服务。

Eri*_*rik 11

您可能正在寻找RAISERROR带有NOWAIT选项的命令。

根据备注

RAISERROR 可用作 PRINT 的替代方法,以将消息返回给调用应用程序。

这不会从SELECT语句返回结果,但它会让您将消息/字符串传递回客户端。如果您想返回您选择的数据的一个快速子集,那么您可能需要考虑FAST查询提示。

指定为快速检索第一个 number_rows 优化查询。这是一个非负整数。返回第一个 number_rows 后,查询将继续执行并生成其完整结果集。

新增由香遣散的评论:

来自Erland Sommarskog在 SQL Server 中错误和事务处理

但是请注意,某些 API 和工具可能会在其一侧进行缓冲,从而使WITH NOWAIT.

有关完整上下文,请参阅源文章。


Sol*_*zky 5

OP 已经尝试发送多个结果集(不是 MARS)并且已经看到它确实在返回任何结果集之前等待存储过程完成。考虑到这种情况,这里有一些选择:

  1. 如果您的数据小到足以容纳 128 个字节,您很可能会使用SET CONTEXT_INFOwhich 使该值通过SELECT [context_info] FROM [sys].[dm_exec_requests] WHERE [session_id] = @SessionID;. 您只需要在运行存储过程之前执行一个快速查询SELECT @@SPID;并通过SqlCommand.ExecuteScalar.

    我刚刚测试了这个,它确实有效。

  2. 类似于@David 将数据放入“进度”表的建议,但无需处理清理或并发/进程分离问题:

    1. Guid在应用程序代码中创建一个新的并将其作为参数传递给存储过程。将此 Guid 存储在变量中,因为它将被多次使用。
    2. 在存储过程中,使用该 Guid 作为表名的一部分创建一个全局临时表,类似于CREATE TABLE ##MyProcess_{GuidFromApp};. 该表可以包含您需要的任何数据类型的任何列。
    3. 只要您有数据,就将其插入到该全局临时表中。

    4. 在应用程序代码中,开始尝试读取数据,但将其包装SELECT在一个中,IF EXISTS以便在尚未创建表的情况下不会失败:

      IF (OBJECT_ID('tempdb..[##MyProcess_{0}]')
          IS NOT NULL)
      BEGIN
        SELECT * FROM [##MyProcess_{0}];
      END;
      
      Run Code Online (Sandbox Code Playgroud)

    使用String.Format(),您可以替换{0}为 Guid 变量中的值。测试 if Reader.HasRows,如果 true 则读取结果,else 调用Thread.Sleep()或其他方法再次轮询。

    好处:

    • 此表与其他进程隔离,因为只有应用程序代码知道特定的 Guid 值,因此无需担心其他进程。另一个进程将拥有自己的私有全局临时表。
    • 因为它是一个表,所以一切都是强类型的。
    • 因为它是一个临时表,当执行存储过程的会话结束时,该表将被自动清理。
    • 因为是全局临时表:
      • 它可以被其他会话访问,就像一个永久的表
      • 它将在创建它的子进程结束后继续存在(即EXEC/sp_executesql调用)


    我已经对此进行了测试,它按预期工作。您可以使用以下示例代码自行尝试。

    在一个查询选项卡中,运行以下命令,然后突出显示块注释中的 3 行并运行:

    CREATE
    --ALTER
    PROCEDURE #GetSomeInfoBackQuickly
    (
      @MessageTableName NVARCHAR(50) -- might not always be a GUID
    )
    AS
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX) = N'CREATE TABLE [##MyProcess_' + @MessageTableName
                 + N'] (Message1 NVARCHAR(50), Message2 NVARCHAR(50), SomeNumber INT);';
    
    -- Do some calculations
    
    EXEC (@SQL);
    
    SET @SQL = N'INSERT INTO [##MyProcess_' + @MessageTableName
    + N'] (Message1, Message2, SomeNumber) VALUES (@Msg1, @Msg2, @SomeNum);';
    
    DECLARE @SomeNumber INT = CRYPT_GEN_RANDOM(2);
    
    EXEC sp_executesql
        @SQL,
        N'@Msg1 NVARCHAR(50), @Msg2 NVARCHAR(50), @SomeNum INT',
        @Msg1 = N'wow',
        @Msg2 = N'yadda yadda yadda',
        @SomeNum = @SomeNumber;
    
    WAITFOR DELAY '00:00:10.000';
    
    SET @SomeNumber = CRYPT_GEN_RANDOM(3);
    EXEC sp_executesql
        @SQL,
        N'@Msg1 NVARCHAR(50), @Msg2 NVARCHAR(50), @SomeNum INT',
        @Msg1 = N'wow',
        @Msg2 = N'yadda yadda yadda',
        @SomeNum = @SomeNumber;
    
    WAITFOR DELAY '00:00:10.000';
    GO
    /*
    DECLARE @TempTableID NVARCHAR(50) = NEWID();
    RAISERROR('%s', 10, 1, @TempTableID) WITH NOWAIT;
    
    EXEC #GetSomeInfoBackQuickly @TempTableID;
    */
    
    Run Code Online (Sandbox Code Playgroud)

    转到“消息”选项卡并复制打印的 GUID。然后,打开另一个查询选项卡并运行以下命令,将从另一个会话的消息选项卡复制的 GUID 放入第 1 行的变量初始化中:

    DECLARE @TempTableID NVARCHAR(50) = N'GUID-from-other-session';
    
    EXEC (N'SELECT * FROM [##MyProcess_' + @TempTableID + N']');
    
    Run Code Online (Sandbox Code Playgroud)

    继续打F5。您应该在前 10 秒看到 1 个条目,然后在接下来的 10 秒看到 2 个条目。

  3. 您可以使用 SQLCLR 通过 Web 服务或其他方式调用回您的应用程序。

  4. 也许可以使用PRINT/RAISERROR(..., 1, 10) WITH NOWAIT立即将字符串传回,但由于以下问题,这会有点棘手:

    • “消息”输出仅限于VARCHAR(8000)NVARCHAR(4000)
    • 消息的发送方式与结果的发送方式不同。为了捕获它们,您需要设置一个事件处理程序。在这种情况下,您可以创建一个变量作为静态集合,以获取可用于代码的所有部分的消息。或者也许是其他方式。我在这里的其他答案中有一个或两个示例,展示了如何捕获消息,稍后我会在找到它们时链接到它们。
    • 默认情况下,消息也不会在进程完成之前发送。这种行为,但是,可以通过设定改变SqlConnection.FireInfoMessageEventOnUserErrors属性true。该文件指出:

      当您将 FireInfoMessageEventOnUserErrors 设置为true 时,以前被视为异常的错误现在作为 InfoMessage 事件处理。所有事件立即触发并由事件处理程序处理。如果 FireInfoMessageEventOnUserErrors 设置为false,则在过程结束时处理 InfoMessage 事件。

      这里的缺点是大多数 SQL 错误将不再引发SqlException. 在这种情况下,您需要测试传递到消息事件处理程序的其他事件属性。这适用于整个连接,这使事情变得有点棘手,但并非无法管理。

    • 所有消息都出现在同一级别,没有单独的字段或属性来区分彼此。接收它们的顺序应该与发送它们的顺序相同,但不确定这是否足够可靠。您可能需要包含一个标签或一些您可以解析的内容。这样你至少可以确定哪个是哪个。

  • 我试试那个。计算字符串后,我将其作为简单的选择返回并继续该过程。问题是它同时返回所有集合(我想在`RETURN` 语句之后)。所以它不起作用。 (2认同)
  • @BogdanBogdanov 你在使用 .NET 和 `SqlConnection` 吗?您想传回多少数据?什么数据类型?您是否尝试过“PRINT”或“RAISERROR WITH NOWAIT”? (2认同)

Dav*_*ett 5

更新: 请参阅 strutzky 的回答(上面)以及至少一个示例的评论,其中的行为与我的预期和描述不符。在时间允许的情况下,我将不得不进一步试验/阅读以更新我的理解......

如果您的调用方与数据库异步交互或者是线程/多进程的,因此您可以在第一个会话仍在运行时打开第二个会话,您可以创建一个表来保存部分数据并随着过程的进行更新。然后,它可以被设置为事务隔离级别1的第二个会话读取,以使其能够读取未提交的更改:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table
Run Code Online (Sandbox Code Playgroud)

1:根据 srutzky 回答中的评论和后续更新,如果被监视的进程没有包含在事务中,则不需要设置隔离级别,尽管在这种情况下我倾向于将其设置为不习惯,因为它不会导致在这些情况下不需要时伤害

当然,如果您可以有多个进程以这种方式运行(如果您的 Web 服务器接受并发用户,这很可能,但这种情况很少见),您将需要以某种方式识别此进程的进度信息. 也许将新创建的 UUID 作为键传递给程序,将其添加到进度表中,然后阅读:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table WHERE process = <current_process_uuid>
Run Code Online (Sandbox Code Playgroud)

我已经使用这种方法来监控 SSMS 中长时间运行的手动进程。我无法决定它是否“闻起来”太多让我考虑在生产中使用它虽然......