将数据从一个 SQL Server 路由到另一个

Jus*_*tin 4 sql-server sql-server-2012

在 SQL Server 2012 中有没有办法将数据写入一个表,然后路由到另一台主机上的另一个 SQL Server?

澄清一下,我们使用 Hibernate Envars 来编写审计日志。Hibernate Envars 不允许将此数据发送到另一台服务器,而不是由 Hibernate 创建审计日志的服务器。

我希望将这些审核日志发送到另一台服务器。有没有办法将 SQL Server 配置为将特定表写入另一台服务器?

Sol*_*zky 5

是的,我实际上可以想到三种方法来完成此操作(好吧,如果包含更新部分,则为 4 种 ;-)。所有三种方法都使用表INSTEAD OF INSERT上的触发器AuditLog

对于所有三种方法,首先在远程实例上创建 AuditLog 表(这应该很明显,但为了完整性而声明)。


选项1

  • 将链接服务器设置为您希望数据转到的服务器。
  • 在远程服务器上创建一个存储过程:
    • 接受NVARCHAR(MAX)输入参数
    • 将输入参数转换为存储在局部变量中的 XML
    • 从解析 XML 变量的 SELECT 语句插入到 AuditLog 表中 FROM @XmlVariable.nodes()
  • INSTEAD OF INSERT在本地实例上创建触发器,在其中执行以下操作:

    DECLARE @RowsToSend NVARCHAR(MAX);
    SET @RowsToSend = (
      SELECT *
      FROM   INSERTED
      FOR XML RAW
    );
    EXEC [LinkedServerName].[DatabaseName].[SchemaName].[StoredProcedureName]
              @RowsToSend;
    
    Run Code Online (Sandbox Code Playgroud)
  • 使用存储过程而不是直接的 INSERT 语句的原因是链接服务器上的 DML 语句有明确的性能问题(我不记得确切的原因,但我记得他们重用缓存计划或与执行计划相关的东西)。

选项#2

  • 创建 SQLCLRINSTEAD OF INSERT触发器
  • 创建一个SqlConnection使用"Context Connection = true;"
  • 创建SqlCommand具有CommandTextSELECT * FROM Inserted;
  • 创建一个 SqlDataReader 通过 SqlCommand.ExecuteReader();
  • SqlBulkCopy (String, SqlBulkCopyOptions)与常规/外部连接字符串一起使用到远程实例
  • 调用SqlBulkCopy.WriteToServer(SqlDataReader)
  • 由于这会连接到远程实例,因此程序集需要 WITH PERMISSION_SET = EXTERNAL_ACCESS
    • 请不要将数据库设置为TRUSTWORTHY ON才能使用EXTERNAL_ACCESS。反而:
    • 签署程序集/DLL
    • [master]从 DLL在数据库中创建非对称密钥
    • 从该非对称密钥创建登录
    • 授予新登录EXTERNAL ACCESS ASSEMBLY权限

选项#3

  • 设置 Service Broker 将消息从本地实例发送到远程实例
  • 以与选项 1 中的方式类似的方式打包行:

    DECLARE @RowsToSend NVARCHAR(MAX);
    SET @RowsToSend = (
      SELECT *
      FROM   INSERTED
      FOR XML RAW
    );
    
    SEND ... @RowsToSend;
    
    Run Code Online (Sandbox Code Playgroud)
  • 有关详细信息,请参阅SQL Server 服务代理

  • 与其他两个选项相比,此选项需要更多的设置,但在使数据传输异步方面更好(因此,如果有大量日志记录数据插入和/或传输延迟,则这更好其他两个选项,因为这会减慢生成日志数据的操作)。

-- 更新 Uno --

选项#4

  • 我之前使用过的选项 #1 的一个变体(但不知何故忘记了直到阅读@datagod 的答案)是全天分批移动数据。
  • 在接收NVARCHAR(MAX)输入参数的远程实例上创建相同的存储过程,将其转换为 XML,然后通过INSERT INTO dbo.AuditLog SELECT c.value('@field1', 'type'), ... FROM @XmlVariable.nodes('/row') t(c);
  • 不是创建INSTEAD OF INSERT触发器,而是在本地实例上创建一个存储过程,它沿着以下行传输数据:

    SET XACT_ABORT ON;
    
    BEGIN TRY
    
      BEGIN TRAN;
    
      -- create temp queue table dynamically to adjust for schema changes
      SELECT * INTO #TempRows FROM @b WHERE 1 = 0;
    
      -- delete rows first to ensure they don't get transferred again
      DELETE tmp
      OUTPUT DELETED.*
      INTO   #TempRows
      FROM dbo.AuditLog tmp;
    
      DECLARE @RowsToSend NVARCHAR(MAX);
      SET @RowsToSend = (
        SELECT *
        FROM   #TempRows
        FOR XML RAW
      );
    
      EXEC [LinkedServerName].[DatabaseName].[SchemaName].[StoredProcedureName]
              @RowsToSend;
    
      COMMIT TRAN;
    END TRY
    BEGIN CATCH
      IF (XACT_STATE() <> 0)
      BEGIN
        ROLLBACK TRAN;
      END;
    
      THROW;
    END CATCH;
    
    Run Code Online (Sandbox Code Playgroud)
  • 创建一个 SQL 代理作业来安排上面的存储过程每隔几分钟运行一次

  • 这具有 Service Broker 方法的异步优势,但设置较少。
  • 关于性能:多年来我一直使用这种方法每天移动几百万行。如果您每天需要移动 2000 万行或更多行,那么也许可以用SqlBulkCopy选项 2的via SQLCLR 方法替换这里的 XML 打包,因为它允许您将本地数据作为电视节目。但除此之外,这种方法非常适用,特别是如果您坚持使用我在此处展示的基于属性的 XML,它比基于元素的 XML 解析速度更快。

-- 更新 Dos --

关于FOR XML RAW处理所有可能的数据类型和数据的能力,我做了以下测试:

首先,创建一个包含列和数据的测试临时表,这些列和数据在转换为基于文本的格式时可能会成为一个问题,尤其是那些保留了某些字符的表,因此如果数据中存在,则需要对其进行转义,以免导致错误。

-- DROP TABLE #BadValues;
CREATE TABLE #BadValues (Col1 NVARCHAR(15), Col2 GEOMETRY, Col3 XML,
                         Col4 SQL_VARIANT, Col5 VARBINARY(MAX));
INSERT INTO #BadValues (Col1, Col2, Col3, Col4, Col5)
  VALUES (N'" =  & < ', 'LINESTRING(11 10, 20 20)', '<test/>', 23, 0x00125DFFFF);
Run Code Online (Sandbox Code Playgroud)

如果有人想知道我从哪里得到这个符号,它是一个补充字符,它是 UTF-16 的一部分,但不在基本 UCS-2 代码点/字符中。我通过执行以下操作创建:

SELECT NCHAR(150150) -- returns "" in a DB with a collation ending in "_SC", else NULL
Run Code Online (Sandbox Code Playgroud)

现在,看看它通常返回什么:

SELECT *
FROM   #BadValues;

-- " =  & < 
-- 0x0000000001140000000000002640000000000000244000000000000034400000000000003440
-- <test />
-- 23
-- 0x00125DFFFF
Run Code Online (Sandbox Code Playgroud)

伟大的。现在让我们加入FOR XML RAW子句,看看会发生什么:

SELECT *
FROM   #BadValues
FOR XML RAW;

/*
Msg 6865, Level 16, State 1, Line 1
FOR XML does not support CLR types - cast CLR types explicitly into one of the
supported types in FOR XML queries.
*/
Run Code Online (Sandbox Code Playgroud)

好的。所以我们需要将 GEOMETRY 字段转换为可用的东西。所有 CLR 类型(无论是自定义 UDT 还是由 Microsoft 提供)都应该有一个.ToString()函数,所以让我们尝试一下:

SELECT Col1, Col2.ToString() AS [Col2stringified], Col3, Col4, Col5
FROM   #BadValues
FOR XML RAW;

/*
Msg 6829, Level 16, State 1, Line 1
FOR XML EXPLICIT and RAW modes currently do not support addressing binary data as
URLs in column 'Col5'. Remove the column, or use the BINARY BASE64 mode, or create
the URL directly using the 'dbobject/TABLE[@PK1="V1"]/@COLUMN' syntax.
*/
Run Code Online (Sandbox Code Playgroud)

好吧,我们通过了 CLR 类型的错误,只是在 VARBINARY 字段上得到了错误。但它建议我们使用“BINARY BASE64”模式,所以让我们尝试一下:

SELECT Col1, Col2.ToString() AS [Col2stringified], Col3, Col4, Col5
FROM   #BadValues
FOR XML RAW, BINARY BASE64;

/*
<row Col1="&quot; =  &amp; &lt; " Col2stringified="LINESTRING (11 10, 20 20)" Col4="23"
     Col5="ABJd//8=">
  <Col3>
    <test />
  </Col3>
</row>
*/
Run Code Online (Sandbox Code Playgroud)

没那么糟糕。这只是意味着,如果您有一个 CLR 类型——GEOMETRY、GEOGRAPHY、HIERARCHYID 或自定义 UDT——那么SELECT *在打包Inserted伪表中的记录时不能使用。在这些情况下,您需要明确列出列,以便您可以应用于.ToString()CLR 类型的任何字段。

请注意,这仅适用于使用 XML 传输数据的方法。如果您使用SqlBulkCopy该类在 SQLCLR 中执行此操作(如上面在几个地方所述),则可能会直接传输(尽管我还没有尝试过)。


-- 更新 Dos punto Uno --

继续上面之前更新 Dos 的测试,以下(使用相同的临时表和测试行)显示了将初始表转换为 XML,然后转换为 NVARCHAR(MAX) 的过程,然后将其传递给将转换的存储过程它返回到 XML,然后使用该.nodes()函数从中选择。

DECLARE @RowsToSend NVARCHAR(MAX);

SET @RowsToSend =
(
  SELECT Col1, Col2.ToString() AS [Col2stringified], Col3, Col4, Col5
  FROM   #BadValues
  FOR XML RAW, BINARY BASE64
);

SELECT @RowsToSend;
--=====--   here would be the call to the remote procedure, which would do the following:
DECLARE @RowsToInsert XML;

SET @RowsToInsert = CONVERT(XML, @RowsToSend);

SELECT @RowsToInsert;

SELECT c.value(N'@Col1', N'NVARCHAR(15)') AS [Col1],
       CONVERT(GEOMETRY, c.value(N'@Col2stringified', N'VARCHAR(MAX)')) AS [Col2],
       c.query(N'(./Col3/*)') AS [Col3], -- use .query() to get sub-element
       c.value(N'@Col4', N'INT') AS [Col4],
       c.value(N'@Col5', N'VARBINARY(MAX)') AS [Col5]
FROM   @RowsToInsert.nodes(N'/row') t(c);
Run Code Online (Sandbox Code Playgroud)