如何从存储过程完成之前获取响应?


8

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

有可能这样做吗?

如果是,该怎么做?

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

编辑:我有该过程的几个部分。在第一部分中,我计算了几个字符串。我将在后面的过程中使用它们进行附加操作。问题是调用者需要尽快使用字符串。因此,我需要计算该字符串并将其传递回(以某种方式,例如通过选择),然后继续工作。调用方可以更快地获取其有价值的字符串。

呼叫者是一个Web服务。


假设尚未发生完整的表锁定或未声明显式事务,则应该能够在单独的会话中运行SELECT,而不会出现问题。
Steve Mangiameli

总的来说,这只是我现在看到的方式,但我认为它不会更快(还有其他问题),@SteveMangiameli
Bogdan Bogdanov

将其拆分为两个SP?将输出从第一个传递到第二个。
狗仔队

不是很快的解决方案,这就是我们为什么要放弃的原因,@ Paparazzi
Bogdan Bogdanov

Answers:


11

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

根据备注

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

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

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

Shannon Severance在评论中添加:

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

不过请注意,某些API和工具可能会在其一侧进行缓冲,从而使的作用无效WITH NOWAIT

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


FAST在我需要同步执行存储过程和C#代码以加剧和重现竞争条件的问题中,为我解决了该问题。以编程方式使用结果集要比使用像这样容易RAISERROR()。当我开始阅读您的答案时,似乎您是在说无法解决问题SELECT,所以也许可以澄清一下吗?
binki

5

UPDATE: 见strutzky的答案(以上)和至少一个例子,我希望在这里描述这种不行为的意见。如果时间允许,我将不得不尝试/进一步阅读以更新我的理解...

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

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table

1:根据评论和srutzky答案中的后续更新,如果所监视的进程未包装在事务中,则不需要设置隔离级别,尽管我倾向于在不引起这种情况的情况下将其设置为习惯在这些情况下不需要时会造成伤害

当然,如果您可以以这种方式运行多个进程(如果您的Web服务器接受并发用户,则很可能会发生这种情况,这种情况很少发生),则需要以某种方式标识此进程的进度信息。也许将新创建的UUID作为密钥传递给过程,将其添加到进度表中,并读取:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM progress_table WHERE process = <current_process_uuid>

我已经使用这种方法来监视SSMS中长时间运行的手动过程。我无法决定它是否“太臭”了,我无法考虑在生产中使用它...


1
这是一种选择,但我目前不喜欢。我希望其他选项会弹出。
Bogdan Bogdanov

5

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

  1. 如果您的数据足够小,可以容纳128个字节,那么您很可能会使用SET CONTEXT_INFO通过使其可见的值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;
      

    使用String.Format(),您可以替换{0}为Guid变量中的值。测试是否为Reader.HasRows,如果为true,则读取结果,否则调用Thread.Sleep()或进行其他任何查询,然后再次轮询。

    优点:

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


    我已经对此进行了测试,并且可以正常工作。您可以使用以下示例代码自行尝试。

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

    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;
    */
    

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

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

    继续打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。在这种情况下,您需要测试传递到消息事件处理程序中的其他事件属性。这对于整个连接都是正确的,这使事情有些棘手,但并非难以控制。

    • 所有消息都显示在同一级别,没有单独的字段或属性来区分彼此。接收顺序应与发送方式相同,但不确定是否足够可靠。您可能需要包括一个标记或随后可以解析的内容。这样,您至少可以确定哪个是哪个。


2
我试试看 计算完字符串后,我将其作为简单选择返回并继续该过程。问题在于它同时返回所有集合(我想在RETURN声明之后)。因此它不起作用。
Bogdan Bogdanov

2
@BogdanBogdanov您是否正在使用.NET和SqlConnection?您想传回多少数据?什么数据类型?你有没有试过要么PRINT还是RAISERROR WITH NOWAIT
所罗门·鲁兹基

我现在尝试。我们使用.NET Web服务。
Bogdan Bogdanov

“因为它是一个全局临时表,所以您不必担心事务隔离级别”-真的正确吗?IIRC临时表,即使是全局临时表,也应与其他任何表一样受相同的ACID限制。您能否详细说明如何测试行为?
David Spillett

@DavidSpillett现在,我考虑了一下,隔离级别确实不是问题,关于您的建议也是如此。只要未在事务内创建表。我刚刚用示例代码更新了答案。
所罗门·鲁兹基

0

如果您的存储过程需要在后台运行(即异步运行),则应使用Service Broker。设置有点麻烦,但是一旦完成,您就可以启动存储过程(非阻塞),并根据需要收听(或少听)进度消息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.