【发布时间】: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 ... ) > 0看起来效率低下,您应该使用EXISTS(即使 SQL Server 通常会对此进行优化)。变更数据捕获肯定会更不具侵入性,您可以定期轮询所有变更并将它们批量处理。
标签: sql sql-server performance audit-logging