xp_cmdshell 的替代方法,用于将报告作为 CSV 文件通过电子邮件发送

Ark*_*ane 5 sql-server t-sql database-mail table-variable xp-cmdshell

我有一个问题,如果可能的话,我可以用一些想法来解决如何在不使用(或启用)的情况下实现所需的问题xp_cmdshell

我知道它xp_cmdshell本身会带来风险,即使使用代理帐户,但是 - 在我们的环境中它被禁用并且说服 IT 经理启用它充其量是困难的。

我遇到的问题是我有一个存储过程,它从各种表中查找数据并将其全部放入表变量中。

我想做以下事情:

  • 将表变量的内容导出到 CSV 文件。
  • 将创建的 CSV 文件附加到电子邮件并将其发送到特定地址。

这需要在没有任何类型的用户输入的情况下自动发生,它的电子邮件端将由同一存储过程中的变量提供。

我已经想到了两种可能做到这一点的方法,都使用 bcp 和xp_cmdshell.

第一种方法是创建一个临时表,从表变量中选择我想要的记录到临时表中,然后使用 bcp 来触发 SSIS 包或 SQL 代理作业,它将查询临时表,导出到 CSV 和 e-给我发邮件。

第二种方法是创建一个临时表,从表变量中选择我想要的记录放入临时表中,然后使用bcp将临时表选择为CSV文件,然后使用sp_send_dbmail发送。

第三种选择可能是触发另一个 SP/函数,并让它执行上述任一方法,从而将新功能分开 - 但这只是为了让事情变得简单。

所以,我要问的是关于如何安全地实现这一目标的任何想法,最好不使用xp_cmdshell. 我不能使用 PowerShell 或类似的技术,因为这里没有人知道 PowerShell,虽然我熟悉 VB.NET,但是我不确定这有什么帮助(如果有的话)。

如果您需要更多信息以提供帮助,请告诉我。

Sol*_*zky 6

由于在这里使用sp_send_dbmail是一个选项,我不明白为什么需要导出任何sp_send_dbmail可以运行查询并包含结果的内容,无论是在正文中还是作为附件。我会先尝试利用的@query@attach_query_result_as_file@query_attachment_filename@query_result_header@query_result_width@query_result_separator@query_no_truncate,和@query_result_no_padding参数。请记住该 MSDN 页面(即上面的链接)中有关要运行的查询的注释:

请注意,查询是在单独的会话中执行的,因此调用 sp_send_dbmail 的脚本中的局部变量对查询不可用。

因此,并假设您不会在任何特定时刻多次运行此进程,您应该将表变量中所需的行转储到全局临时表中(即以##代替开始#)。

我相信sp_send_dbmail使用 Service Broker 并且是异步的。这似乎允许在@query最终执行查询时全局临时表不存在的可能性,如果执行的会话sp_send_dbmail在查询@query开始之前结束。在这种情况下,您可以执行以下任一操作:

  • 添加一个WAITFOR DELAY '00:00:10.000';以在调用会话结束之前添加 10 秒的延迟。
  • 不是使用全局临时表,而是NEWID()用于构造一个唯一的表名,以便将该数据转储到tempdb一个真正的表中,该表将在您需要时一直存在。然后,您可以将它放在为报告提交的查询的末尾。更大的问题是,现在表名需要动态 SQL,但这不适用于表变量,因为它无法传递到动态 SQL 中。我们需要一个已知且一致的对象名称来将表变量中的行插入到真正的表中。值得庆幸的是,SYNONYM可以在当前上下文中提供一致的名称,该名称指向tempdb因为它们可以在动态 SQL 中创建。这给了我们以下内容(您可以运行以下命令以查看它是否有效,尽管我没有在 中实现DROP真实表的最终结果tempdb):

    SET NOCOUNT ON;
    
    DECLARE @TableName NVARCHAR(70) = N'tempdb.dbo.[Report_'
                                      + CONVERT(NVARCHAR(36), NEWID()) + N']';
    DECLARE @SQL NVARCHAR(MAX);
    
    SET @SQL = N'CREATE TABLE ' + @TableName + N' ([Col1a] INT);';
    EXEC (@SQL);
    
    SET @SQL = N'CREATE SYNONYM dbo.TempDump FOR ' + @TableName + N';';
    EXEC (@SQL);
    
    DECLARE @SomeTableVar TABLE ([Col1b] INT);
    INSERT INTO @SomeTableVar ([Col1b]) VALUES (1), (20), (34), (4444);
    
    INSERT INTO dbo.TempDump ([Col1a])
      SELECT Col1b
      FROM   @SomeTableVar;
    
    SELECT * FROM dbo.TempDump; -- only needed for debug
    
    SET @SQL = N'DROP SYNONYM dbo.TempDump;';
    EXEC (@SQL);
    
    
    -- create the report query, ending with the DROP statement
    DECLARE @ReportSQL NVARCHAR(MAX) = N'';
    SET @ReportSQL += N'my report, sent as @query, using ' + @TableName + N';';
    SET @ReportSQL += NCHAR(0x0D) + NCHAR(0x0A) + N'DROP TABLE ' + @TableName + N';';
    print @ReportSQL;
    
    -- EXEC  sp_send_dbmail ..., @query = @ReportSQL, @attach_query_result_as_file = 1,
    --                      @query_result_separator = N',', @query_no_truncate = 1, ...;
    
    Run Code Online (Sandbox Code Playgroud)

    如果该过程在创建表之后但在DROP调用语句之前失败,则没有问题:每次 SQL Server 服务重新启动时都会tempdb从空model数据库创建(这就是为什么我建议在tempdb!)中创建表。


小智 1

您可以使用 SSIS 相对轻松地完成此操作,只需绕过表变量或临时表并使用平面文件目标(在您的情况下为 CSV)。然后,您可以使用电子邮件任务并指定 CSV 作为附件,直接从 SSIS 工作流程中发送电子邮件。

这将带来额外的好处,即能够使用 SSIS 的日志记录和其他管道,例如在需要时归档/删除发送后的 CSV 等。

或者,如果您确实必须使用 xp_cmdshell,您始终可以有一个过程来打开它,执行您想要的处理,然后再次将其关闭。