In an environment with many triggers being fired, it is often necessary to be able to identify and monitor the execution of triggers to analyze a certain behavior or understand how data is being changed.
This ends up getting even more complex when a trigger changes data from another table(s) and multiple triggers are fired in sequence, nested, from a single SQL command.
That's what you'll learn in this article.
One of the ways to be able to identify the DML command that triggered a trigger is to change the trigger itself to capture the SQL command.
See how to do this in the articles below:
- SQL Server - How to Create a Data Change History for Your Tables (Audit Logs)
- SQL Server - How to identify the query that triggered a DML trigger on a table
Test base creation
For the demonstration of this post, I will share the scripts used so that you can test in your environment as well.
Creating the tables
Click here to view the table code
Creation of triggers
Click here to view the trigger code
How to identify and monitor the execution of triggers
Now that we have the tables and triggers created, let's start our monitoring, change some data and test that everything is working as expected.
Let's create an Extended Event (XE) to capture the execution of triggers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
IF (EXISTS(SELECT TOP(1) NULL FROM sys.server_event_sessions WHERE [name] = 'trcMonitorTriggers')) DROP EVENT SESSION [trcMonitorTriggers] ON SERVER; GO CREATE EVENT SESSION [trcMonitorTriggers] ON SERVER ADD EVENT sqlserver.sp_statement_completed ( ACTION ( sqlserver.server_instance_name, sqlserver.session_id, sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.[database_name], sqlserver.username, sqlserver.nt_username, sqlserver.session_nt_username, sqlserver.session_server_principal_name, sqlserver.server_principal_name, sqlserver.sql_text ) WHERE ([object_type] = 'TRIGGER') ) ADD TARGET package0.event_file ( -- Lembre-se de criar a pasta antes de executar SET filename=N'C:\Traces\TriggerMonitor', max_file_size=(20), max_rollover_files=(10) ) WITH (STARTUP_STATE=ON) GO ALTER EVENT SESSION [trcMonitorTriggers] ON SERVER STATE = START GO |
Let's run some data changes to test our trigger monitor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
INSERT INTO dbo.Cliente VALUES('Dirceu', '1987-05-28', 0) GO INSERT INTO dbo.Cliente SELECT 'Resende' AS Nome, '1987-05-28' AS Dt_Nascimento, 9999 AS Vl_Salario GO INSERT INTO dbo.Cliente VALUES('Dirceu - Teste 2', '1987-05-28', 0) GO INSERT INTO dbo.Cliente SELECT 'Resende - Teste 2' AS Nome, '1987-05-28' AS Dt_Nascimento, 9999 AS Vl_Salario GO UPDATE dbo.Funcionario SET Nome = 'Dirceu Resende' WHERE Id_Funcionario = 1 GO |
And now let's read the data collected from the Extended Event to view the triggers execution history
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
DECLARE @TimeZone INT = DATEDIFF(HOUR, GETUTCDATE(), GETDATE()) IF (OBJECT_ID('tempdb..#Events') IS NOT NULL) DROP TABLE #Events ;WITH CTE AS ( SELECT CONVERT(XML, event_data) AS event_data FROM sys.fn_xe_file_target_read_file(N'C:\Traces\TriggerMonitor*.xel', NULL, NULL, NULL) ) SELECT DATEADD(HOUR, @TimeZone, CTE.event_data.value('(//event/@timestamp)[1]', 'datetime')) AS eventDate, CTE.event_data INTO #Events FROM CTE SELECT A.eventDate, xed.event_data.value('(data[@name="object_id"]/value)[1]', 'int') AS [object_id], OBJECT_NAME(xed.event_data.value('(data[@name="object_id"]/value)[1]', 'int')) AS [trigger_name], xed.event_data.value('(action[@name="session_id"]/value)[1]', 'int') AS session_id, xed.event_data.value('(action[@name="database_name"]/value)[1]', 'varchar(128)') AS [database_name], xed.event_data.value('(action[@name="nt_username"]/value)[1]', 'varchar(128)') AS nt_username, xed.event_data.value('(action[@name="session_server_principal_name"]/value)[1]', 'varchar(128)') AS session_server_principal_name, xed.event_data.value('(action[@name="client_hostname"]/value)[1]', 'varchar(128)') AS [client_hostname], xed.event_data.value('(action[@name="client_app_name"]/value)[1]', 'varchar(128)') AS [client_app_name], xed.event_data.value('(data[@name="nest_level"]/value)[1]', 'int') AS [nest_level], xed.event_data.value('(data[@name="statement"]/value)[1]', 'varchar(max)') AS [statement], TRY_CAST(xed.event_data.value('(//action[@name="sql_text"]/value)[1]', 'varchar(max)') AS XML) AS sql_text FROM #Events A CROSS APPLY A.event_data.nodes('//event') AS xed (event_data) |
With this script, you can identify each command executed inside the trigger (statement column) or the command that triggered the trigger (sql_text column).
If you want something simpler and more concise, to know just when the triggers were fired, you can use this script here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
DECLARE @TimeZone INT = DATEDIFF(HOUR, GETUTCDATE(), GETDATE()) IF (OBJECT_ID('tempdb..#Events') IS NOT NULL) DROP TABLE #Events ;WITH CTE AS ( SELECT CONVERT(XML, event_data) AS event_data FROM sys.fn_xe_file_target_read_file(N'C:\Traces\TriggerMonitor*.xel', NULL, NULL, NULL) ) SELECT DATEADD(HOUR, @TimeZone, CTE.event_data.value('(//event/@timestamp)[1]', 'datetime')) AS eventDate, CTE.event_data INTO #Events FROM CTE SELECT DISTINCT A.eventDate, xed.event_data.value('(data[@name="object_id"]/value)[1]', 'int') AS [object_id], OBJECT_NAME(xed.event_data.value('(data[@name="object_id"]/value)[1]', 'int')) AS [trigger_name], xed.event_data.value('(action[@name="session_id"]/value)[1]', 'int') AS session_id, xed.event_data.value('(action[@name="database_name"]/value)[1]', 'varchar(128)') AS [database_name], xed.event_data.value('(action[@name="nt_username"]/value)[1]', 'varchar(128)') AS nt_username, xed.event_data.value('(action[@name="session_server_principal_name"]/value)[1]', 'varchar(128)') AS session_server_principal_name, xed.event_data.value('(action[@name="client_hostname"]/value)[1]', 'varchar(128)') AS [client_hostname], xed.event_data.value('(action[@name="client_app_name"]/value)[1]', 'varchar(128)') AS [client_app_name], xed.event_data.value('(data[@name="nest_level"]/value)[1]', 'int') AS [nest_level], xed.event_data.value('(//action[@name="sql_text"]/value)[1]', 'varchar(max)') AS sql_text FROM #Events A CROSS APPLY A.event_data.nodes('//event') AS xed (event_data) ORDER BY A.eventDate, nest_level |
In this shorter version, it is very easy to identify that a single update activated 2 triggers by looking at the eventDate, nest_level and sql_text columns. After the UPDATE command, the trigger “trgFuncionario” was fired and as this trigger makes another UPDATE in the “Cliente” table, it activated the trigger “trgHistorico_Cliente”.
Now it's easy to identify triggers being fired in the environment, especially nested triggers like this example, where a single command can end up firing multiple triggers, making it difficult to track in a large environment.
Hope you enjoyed this tip and see you in the next article!
Hugs.