【问题标题】:How can I speed up this SQL Server CURSOR used for change tracking? [closed]如何加快用于更改跟踪的 SQL Server CURSOR? [关闭]
【发布时间】:2022-01-09 18:13:16
【问题描述】:

谁能推荐我如何加速这段代码,主要是光标?该代码是一个 SQL Server 数据库查询,它在 INSERT、UPDATE 或 DELETE 上创建触发器。它将记录写入 changlog 表,标识更改的类型(I、U 或 D),然后将每行的每个受影响列的旧值和新值保存在详细信息表中。

我希望它是通用的,这样我就可以轻松地将它用于我扔给它的任何表格,这些表格有一个可以过滤的唯一列。不幸的是,将整行更改写入克隆的结构审计表不是一种选择。

非常感谢任何帮助,我不是最擅长查询优化的,欢迎任何反馈或重写。谢谢!

ALTER TRIGGER [dbo].[tbl_Address_ChangeTracking] ON [dbo].[tbl_Address]
AFTER INSERT, DELETE, UPDATE
AS
BEGIN
       -- SET NOCOUNT ON added to prevent extra result sets from
       -- interfering with SELECT statements.
       SET NOCOUNT ON
       --SET XACT_ABORT ON
       -- Get the table name of the current process
       DECLARE @TableName VARCHAR(25)
       SET @TableName = COALESCE(
       (
              SELECT SCHEMA_NAME(schema_id) + '.' + OBJECT_NAME(parent_object_id)
              FROM sys.objects
              WHERE sys.objects.name = OBJECT_NAME(@@PROCID) AND
                       SCHEMA_NAME(sys.objects.schema_id) = OBJECT_SCHEMA_NAME(@@PROCID)
       ), 'Unknown')

       --Declare our cursor to navigate the records in inserted and deleted
       DECLARE @cursorSQL AS NVARCHAR(MAX) = ''
       DECLARE @PrimaryID AS VARCHAR(MAX) = ''
       DROP TABLE IF EXISTS #inserted1TableTemp
       DROP TABLE IF EXISTS #inserted2TableTemp
       DROP TABLE IF EXISTS #deletedTableTemp
       DECLARE @ourLogCursor CURSOR

       --If we have a record in inserted and deleted this is an update record and we should pull from the inserted table and assume
       --this is one update or many update statements
       IF EXISTS
       (
              SELECT 1
              FROM inserted
       ) AND
          EXISTS
       (
              SELECT 1
              FROM deleted
       )
       BEGIN
              SELECT *
              INTO #inserted1TableTemp
              FROM inserted

              SET @cursorSQL = 'SET @ourLogCursor = CURSOR FOR SELECT AddressID FROM #inserted1TableTemp; OPEN @ourLogCursor;'
       END

       --If we have an inserted record and no deleted record this is an insert and we pull from the inserted table
       IF EXISTS
       (
              SELECT 1
              FROM inserted
       ) AND
          NOT EXISTS
       (
              SELECT 1
              FROM deleted
       )
       BEGIN
              DROP TABLE IF EXISTS #inserted2TableTemp
              DROP TABLE IF EXISTS #inserted1TableTemp
              DROP TABLE IF EXISTS #deletedTableTemp

              SELECT *
              INTO #inserted2TableTemp
              FROM inserted

              SET @cursorSQL = 'SET @ourLogCursor = CURSOR FOR SELECT AddressID FROM #inserted2TableTemp; OPEN @ourLogCursor;'
       END

       --If we have a deleted record and no insert record this is a deletion and we pull from the deleted table
       IF NOT EXISTS
       (
              SELECT 1
              FROM inserted
       ) AND
          EXISTS
       (
              SELECT 1
              FROM deleted
       )
       BEGIN
              DROP TABLE IF EXISTS #inserted1TableTemp
              DROP TABLE IF EXISTS #inserted2TableTemp
              DROP TABLE IF EXISTS #deletedTableTemp

              SELECT *
              INTO #deletedTableTemp
              FROM deleted

              SET @cursorSQL = 'SET @ourLogCursor = CURSOR FOR SELECT AddressID FROM #deletedTableTemp; OPEN @ourLogCursor;'
       END

       --If we have a deleted record and no insert record this is a deletion and we pull from the deleted table
       IF NOT EXISTS
       (
              SELECT 1
              FROM inserted
       ) AND
          NOT EXISTS
       (
              SELECT 1
              FROM deleted
       )
       BEGIN
              RETURN;
       END

       --Execute our dynamic SQL that sets the correct FOR SELECT statment for the cursor. Pass @ourCursorLog as an input param, and then grab the output
       --so the results are available outside the scope of the executesql call
       EXEC sys.sp_executesql @cursorSQL, N'@ourLogCursor CURSOR OUTPUT', @ourLogCursor OUTPUT;

       FETCH NEXT FROM @ourLogCursor INTO @PrimaryID
       DECLARE @xmlOld XML
       DECLARE @xmlNew XML
       DECLARE @SummaryID INT
       SET @TableName = COALESCE(
       (
              SELECT SCHEMA_NAME(schema_id) + '.' + OBJECT_NAME(parent_object_id)
              FROM sys.objects
              WHERE sys.objects.name = OBJECT_NAME(@@PROCID) AND
                       SCHEMA_NAME(sys.objects.schema_id) = OBJECT_SCHEMA_NAME(@@PROCID)
       ), 'Unknown')
       --Navigate all our rows
       WHILE @@FETCH_STATUS = 0
       BEGIN
              DROP TABLE IF EXISTS #tmp_AddressesChangelogTrigger
              DROP TABLE IF EXISTS #tmp_AddressesChangelogTriggerXML1
              DROP TABLE IF EXISTS #tmp_AddressesChangelogTriggerXML2
              DROP TABLE IF EXISTS #tmp_AddressesChangelogTriggerXMLsWithDifferences

              --Get the deleted and inserted records as xml for comparison against each other
              SET @xmlNew =
              (
                     SELECT *
                     FROM deleted AS [TABLE]
                     WHERE AddressID = @PrimaryID
                     ORDER BY AddressID FOR XML AUTO, ELEMENTS
              )
              SET @xmlOld =
              (
                     SELECT *
                     FROM inserted AS [TABLE]
                     WHERE AddressID = @PrimaryID
                     ORDER BY AddressID FOR XML AUTO, ELEMENTS
              )
              
              CREATE TABLE #tmp_AddressesChangelogTriggerXML1
              (
                                  NodeName VARCHAR(MAX), Value VARCHAR(MAX)
              )
              CREATE TABLE #tmp_AddressesChangelogTriggerXML2
              (
                                  NodeName VARCHAR(MAX), Value VARCHAR(MAX)
              )

              --Extract the values and column names
              INSERT INTO #tmp_AddressesChangelogTriggerXML2( NodeName, Value )
                        --Throw the XML into temp tables with the column name and value
                        SELECT N.value( 'local-name(.)', 'nvarchar(MAX)' ) AS NodeName, N.value( 'text()[1]', 'nvarchar(MAX)' ) AS VALUE
                        FROM @xmlNew.nodes( '/TABLE/*' ) AS T(N)

              INSERT INTO #tmp_AddressesChangelogTriggerXML1( NodeName, Value )
                        SELECT N.value( 'local-name(.)', 'nvarchar(MAX)' ) AS NodeName, N.value( 'text()[1]', 'nvarchar(MAX)' ) AS VALUE
                        FROM @xmlOld.nodes( '/TABLE/*' ) AS T(N)

              --Get the differences into a temp table
              SELECT *
              INTO #tmp_AddressesChangelogTriggerXMLsWithDifferences
              FROM
              (
                     SELECT COALESCE(A.NodeName, B.NodeName) AS NodeName, B.Value AS OldValue, A.Value AS NewValue
                     FROM #tmp_AddressesChangelogTriggerXML1 AS A
                           FULL OUTER JOIN #tmp_AddressesChangelogTriggerXML2 AS B ON A.NodeName = B.NodeName
                     WHERE A.Value <> B.Value
              ) AS tmp

              --If anything changed thhen start our write statments
              IF
              (
                     SELECT COUNT(*)
                     FROM #tmp_AddressesChangelogTriggerXMLsWithDifferences
              ) > 0
              BEGIN
                     BEGIN TRY
                           -- Now create the Summary record
                           --BEGIN TRANSACTION WRITECHANGELOGRECORDS
                           INSERT INTO TableChangeLogSummary( ID, ModifiedDate, ChangeType, TableName )
                                     --Get either insert, or if no insert value, get the delete value
                                     --Set the update type, I, D, U
                                     --Compare values with a full outer join
                                     --Filter on the ID we are on in the CURSOR
                                    SELECT COALESCE(I.AddressID, D.AddressID), GETDATE(),
                                       CASE
                                       WHEN D.AddressID IS NULL THEN 'I'
                                       WHEN I.AddressID IS NULL THEN 'D'
                                       ELSE 'U'
                                       END, @TableName
                                     FROM inserted AS I
                                                FULL OUTER JOIN deleted AS D ON I.AddressID = D.AddressID
                                     WHERE( I.AddressID = @PrimaryID OR
                                                  I.AcesAddressID IS NULL
                                                ) AND
                                                ( D.AddressID = @PrimaryID OR
                                                  D.AcesAddressID IS NULL
                                                )

                           --Get the last summary id that was inserted so we can use it in the detail record
                           SET @SummaryID = (SELECT IDENT_CURRENT('TableChangeLogSummary'))

                           --Insert our 
                           INSERT INTO TableChangeLogDetail( SummaryID, ColumnName, OldValue, NewValue )
                                     SELECT @SummaryID, T.NodeName, T.OldValue, T.NewValue
                                     FROM #tmp_AddressesChangelogTriggerXMLsWithDifferences AS T
                           --COMMIT TRANSACTION WRITECHANGELOGRECORDS
                           --PRINT 'RECORD WRITTEN'
                     END TRY
                     BEGIN CATCH
                           DECLARE @errorXML XML
                           SET @errorXML = (SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_STATE() AS ErrorState, ERROR_SEVERITY() AS ErrorSeverity, ERROR_PROCEDURE() AS ErrorProcedure, ERROR_LINE() AS ErrorLine, ERROR_MESSAGE() AS ErrorMessage FOR XML RAW)
                           DECLARE @errorXMLText NVARCHAR(MAX) = ''
                           SET @errorXMLText = (SELECT CAST(@errorXML AS NVARCHAR(MAX)))
                           RAISERROR(@errorXMLText, 16, 1) WITH NOWAIT
                     END CATCH
              END
              --Go to the next record and process
              FETCH NEXT FROM @ourLogCursor INTO @PrimaryID
       END
       CLOSE @ourLogCursor
       DEALLOCATE @ourLogCursor
END

【问题讨论】:

  • 不使用CURSOR 是一个开始。 SQL 是一种基于集合的语言,因此它在基于集合的解决方案中表现出色; CURSOR/WHILE 与该方法完全相反。
  • 这里绝对不需要任何循环结构,我使用过更改跟踪和 CDC,从未想过我需要游标。您首先需要通过分而治之的方法确定哪些部分成本最高,然后发布Minimal, Reproducible Example - 强调最低限度。包括对象定义和实际执行计划(使用PasteThePlan)——事实上,我阅读了更改跟踪,并假设您正在使用它,您可能不会查看 SQL 提供的开箱即用的功能?
  • 如果您想要数据的历史记录,为什么不使用时态表?
  • 这是一个太大的过程,无法完全优化Stack Overflow 问题我可以在这里看到错误的事情:可能更容易制作 3 个单独的触发器而不是将它们全部混合一起。光标很糟糕,很糟糕,你几乎可以肯定地做这个基于集合的。最好为每个表及其所有列编写自定义触发器,而不是执行SELECT * ... FOR XML 和动态(可能会动态生成触发器)。这并不意味着您不能生成 XML,只需正确指定每一列。 .....
  • .....IDENT_CURRENT 不是获取最后插入的 ID 的最佳方式,它存在并发问题。为什么捕获错误只是为了引发一些奇怪的 XML 错误,只是让错误传播到客户端。 IF (SELECT COUNT ... ) &gt; 0 看起来效率低下,您应该使用EXISTS(即使 SQL Server 通常会对此进行优化)。变更数据捕获肯定会更不具侵入性,您可以定期轮询所有变更并将它们批量处理。

标签: sql sql-server performance audit-logging


【解决方案1】:

承认使用变更数据跟踪的建议并注意不要将过多的逻辑放入触发器中,以下是变更捕获逻辑的重构(以及一些彻底的重写)。

更新后的逻辑会一次性处理数据,同时处理所有受影响的记录。鉴于要求,我认为它非常接近最优,但可能仍有改进的空间。与 XML 之间的转换可能会增加大量开销。另一种方法是为每个表动态生成和应用自定义触发器,这些触发器分别显式引用所有数据列以获取详细信息并将它们联合在一起。

我还改进了值比较,以更好地处理空值、区分大小写和潜在的尾随空格更改。

下面的代码不是触发器的形式,而是适合单机测试的形式。我认为您(以及任何其他可能感兴趣的人)会想要测试一旦检查出来,您应该能够将其改装回您的触发器。

请注意,这不是 100% 通用的解决方案。某些列类型可能不受支持。该逻辑当前假定整数类型的单列主键。需要进行更改以处理与这些(可能还有一些当前未识别的)约束的偏差。

-- Simulated change log tables
DECLARE @TableChangeLogSummary TABLE (ID INT IDENTITY(1,1), KeyValue INT NOT NULL, ModifiedDate DATETIME NOT NULL, ChangeType CHAR(1) NOT NULL, TableName NVARCHAR(1000) NOT NULL )
DECLARE @TableChangeLogDetails TABLE (ID INT IDENTITY(1,1), SummaryID int NOT NULl, ColumnName NVARCHAR(1000) NOT NULL, OldValue NVARCHAR(MAX), NewValue NVARCHAR(MAX))

-- Simulated system defined inserted/deleted tables
DECLARE @inserted TABLE (ID INTEGER, Value1 NVARCHAR(100), Value2 BIT, Value3 FLOAT)
DECLARE @deleted TABLE (ID INTEGER, Value1 NVARCHAR(100), Value2 BIT, Value3 FLOAT)

-- Test data
INSERT @inserted
VALUES
    (1, 'AAA', 0, 3.14159), -- Insert
    (2, 'BBB', 1, null),    -- Mixed updates including null to non-null and non-null to null
    (3, 'CCC', 0, 0),       -- Trailing space change
    (4, 'DDD', null, 1.68), -- No changes
    (5, '', 0, null),       -- No changes with blanks and nulls
    (6, null, null, null),  -- No changes all nulls
    (7, null, null, null)   -- Insert all nulls (summary with key, but no details will be logged)

INSERT @deleted
VALUES
    (2, 'bbb', null, 2.73),
    (3, 'CCC ', 0, 0),
    (4, 'DDD', null, 1.68),
    (5, '', 0, null),
    (6, null, null, null),
    (8, null, null, null), -- Delete all null values (summary with key, but no details will be logged)
    (9, 'ZZZ', 999, 999.9) -- Delete non-nulls

--- Now the real work begins...

-- Set table and information. Assumes table has exactly one PK column. Later logic assumes an INT.
DECLARE @TableName NVARCHAR(1000) = 'MyTable' -- To be extracted from the parent object of the trigger
DECLARE @KeyColumnName SYSNAME = 'ID' -- This can be fixed if known or derived on the fly from the primary key definition

-- Extract inserted and/or deleted data
DECLARE @InsertedXml XML = (
        SELECT *
        FROM @inserted
        FOR XML PATH('inserted'), TYPE
)
DECLARE @DeletedXml XML = (
        SELECT *
        FROM @deleted
        FOR XML PATH('deleted'), TYPE
)

-- Parse and reassange the captured key and data values
DECLARE @TempDetails TABLE(
    KeyValue INT NOT NULL,
    ChangeType CHAR(1) NOT NULL,
    ColumnName VARCHAR(1000) NOT NULL,
    IsKeyColumn BIT NOT NULL,
    NewValue NVARCHAR(MAX),
    OldValue NVARCHAR(MAX))
INSERT @TempDetails
SELECT
    KeyValue = COALESCE(I.KeyValue, D.KeyValue),
    ChangeType = CASE WHEN D.KeyValue IS NULL THEN 'I' WHEN I.KeyValue IS NULL THEN 'D' ELSE 'U' END,
    ColumnName = COALESCE(I.ColumnName, D.ColumnName),
    IsKeyColumn = K.IsKeyColumn,
    NewValue = I.Value,
    OldValue = D.Value
FROM (
    SELECT K.KeyValue, C.ColumnName, C.Value
    FROM @InsertedXml.nodes( '/inserted' ) R(Row)
    CROSS APPLY (
        SELECT KeyValue = C.Col.value('text()[1]', 'int')
        FROM R.Row.nodes( './*' ) C(Col)
        WHERE C.Col.value( 'local-name(.)', 'nvarchar(MAX)' ) = @KeyColumnName
    ) K
    CROSS APPLY (
        SELECT ColumnName = C.Col.value('local-name(.)', 'nvarchar(MAX)'), Value = C.Col.value('text()[1]', 'nvarchar(MAX)')
        FROM R.Row.nodes( './*' ) C(Col)
    ) C
) I
FULL OUTER JOIN (
    SELECT K.KeyValue, C.ColumnName, C.Value
    FROM @DeletedXml.nodes( '/deleted' ) R(Row)
    CROSS APPLY (
        SELECT KeyValue = C.Col.value('text()[1]', 'int')
        FROM R.Row.nodes( './*' ) C(Col)
        WHERE C.Col.value( 'local-name(.)', 'nvarchar(MAX)' ) = @KeyColumnName
    ) K
    CROSS APPLY (
        SELECT ColumnName = C.Col.value('local-name(.)', 'nvarchar(MAX)'), Value = C.Col.value('text()[1]', 'nvarchar(MAX)')
        FROM R.Row.nodes( './*' ) C(Col)
    ) C
) D
    ON D.KeyValue = I.KeyValue
    AND D.ColumnName = I.ColumnName
CROSS APPLY (
    SELECT IsKeyColumn = CASE WHEN COALESCE(I.ColumnName, D.ColumnName) = @KeyColumnName THEN 1 ELSE 0 END
) K
WHERE ( -- We need to be careful about edge cases here
    (I.Value IS NULL AND D.Value IS NOT NULL)
    OR (I.Value IS NOT NULL AND D.Value IS NULL)
    OR I.Value <> D.Value COLLATE Latin1_General_Bin -- Precise compare (case and accent sensitive)
    OR DATALENGTH(I.Value) <> DATALENGTH(D.Value) -- Catch trailing space cases
    OR K.IsKeyColumn = 1
    )

-- Get rid of updates with no changes, but keep key-only inserts or deletes
DELETE T
FROM @TempDetails T
WHERE T.IsKeyColumn = 1
AND T.ChangeType = 'U'
AND NOT EXISTS (
    SELECT *
    FROM @TempDetails T2
    WHERE T2.KeyValue = T.KeyValue
    AND T2.IsKeyColumn = 0
)

-- Local table to capture and link SummaryID between the summary and details tables
DECLARE @CaptureSummaryID TABLE (SummaryID int, KeyValue INT NOT NULL)

-- Insert change summary and capture the assigned Summary ID via the OUTPUT clause
INSERT INTO @TableChangeLogSummary (KeyValue, ModifiedDate, ChangeType, TableName)
OUTPUT INSERTED.id, INSERTED.KeyValue INTO @CaptureSummaryID
SELECT T.KeyValue, ModifiedDate = GETDATE(), T.ChangeType, TableName = @TableName
FROM @TempDetails T
WHERE T.IsKeyColumn = 1
ORDER BY T.KeyValue  -- Optional, but adds consistancy

-- Insert change details
INSERT INTO @TableChangeLogDetails (SummaryID, ColumnName, OldValue, NewValue)
SELECT S.SummaryID, T.ColumnName, T.OldValue, T.NewValue
FROM @CaptureSummaryID S
JOIN @TempDetails T ON T.KeyValue = S.KeyValue
WHERE T.IsKeyColumn = 0
ORDER BY T.ColumnName  -- Optional, but adds consistancy

-- View test results
SELECT 'Change Log:', *
FROM @TableChangeLogSummary S
LEFT JOIN @TableChangeLogDetails D ON D.SummaryID = S.ID
ORDER BY S.ID, D.ID

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-01-20
    • 1970-01-01
    • 2017-09-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多