{"id":10799,"date":"2025-08-17T21:25:22","date_gmt":"2025-08-17T21:25:22","guid":{"rendered":"https:\/\/putridparrot.com\/blog\/?p=10799"},"modified":"2025-08-17T21:25:22","modified_gmt":"2025-08-17T21:25:22","slug":"creating-audit-history-for-your-tables-in-sql-server","status":"publish","type":"post","link":"https:\/\/putridparrot.com\/blog\/creating-audit-history-for-your-tables-in-sql-server\/","title":{"rendered":"Creating audit history for your table(s) in SQL Server"},"content":{"rendered":"<p>We&#8217;re wanting to capture audit history on our table(s) in our database. A good way to do this is via triggers.<\/p>\n<p>Let&#8217;s create ourselves an Audit table first, something like this<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nCREATE TABLE &#x5B;dbo].&#x5B;Audit](\r\n  &#x5B;Id] &#x5B;int] IDENTITY(1,1) NOT NULL,\r\n  &#x5B;Operation] &#x5B;nvarchar](3) NOT NULL,\r\n  &#x5B;DateTime] &#x5B;datetime] NOT NULL,\r\n  &#x5B;Before] &#x5B;nvarchar](256) NULL,\r\n  &#x5B;After] &#x5B;nvarchar](256) NULL,\r\n) ON &#x5B;PRIMARY]\r\n<\/pre>\n<p>The <em>Operation<\/em> will be used to store a value from &#8220;UPD&#8221;, &#8220;DEL&#8221; and &#8220;INS&#8221;. In other words the audit record is an Update, Delete or Insert. The <em>DateTime<\/em> is the date and time the operation took place, <em>Before<\/em> is the data before it&#8217;s changed, <em>After<\/em> the data it changed to and a <em>Description<\/em> field for a bit more information. <\/p>\n<p>I&#8217;ve not included this field, but most likely we&#8217;ll also want to have a <em>Who<\/em> field for who made the change.<\/p>\n<p>In my little example I have a table named <em>Scale<\/em> which is used to store musical scales. Just for completeness let&#8217;s take a look at the Scale table<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nCREATE TABLE &#x5B;dbo].&#x5B;Scale](\r\n  &#x5B;Id] &#x5B;int] IDENTITY(1,1) NOT NULL,\r\n  &#x5B;Name] &#x5B;nvarchar](256) NOT NULL,\r\n  &#x5B;Intervals] &#x5B;nvarchar](256) NOT NULL,\r\n  &#x5B;Description] &#x5B;nvarchar](256) NULL\r\n) ON &#x5B;PRIMARY]\r\n<\/pre>\n<p>Now I&#8217;m just going to add triggers to this one table, however you might have such triggers on multiple tables. We will want to have a trigger for deletion, insertion and updates.<\/p>\n<p>For the delete trigger we might use something like this<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nALTER TRIGGER &#x5B;dbo].&#x5B;ScaleDelete]\r\nON &#x5B;dbo].&#x5B;Scale]\r\nFOR DELETE\r\nAS\r\n  INSERT INTO dbo.Audit(Operation, DateTime, Before)\r\n  SELECT &#039;DEL&#039;, GETDATE(), (select d.Name, d.Description, d.Intervals from deleted d FOR JSON PATH)\r\n  FROM deleted\r\n<\/pre>\n<p>For a deletion we insert the operation, date\/time and a description into the audit table using the <em>deleted<\/em> data.<\/p>\n<p>For insertions we might have something like this<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nALTER TRIGGER &#x5B;dbo].&#x5B;ScaleInsert] \r\nON &#x5B;dbo].&#x5B;Scale]\r\nFOR INSERT\r\nAS\r\n  INSERT INTO dbo.Audit(Operation, DateTime, After)\r\n  SELECT &#039;INS&#039;, GETDATE(), (select i.Name, i.Description, i.Intervals from inserted i FOR JSON PATH)\r\n  FROM inserted;\r\n<\/pre>\n<p>In this case we take the data from the <em>insert<\/em> data and passing parts of this data to the audit table (similar to the deletion).<\/p>\n<p>For the update trigger we might write something similar to the above, but any time the UPDATE SQL command is called the audit log will be added to, even if there were not changes. It would, therefore, be nice if we only updated the audit table when real changes are detected.<\/p>\n<p>In the case of an update we get both <em>inserted<\/em> and <em>deleted<\/em> data. So we can check whether the <em>inserted<\/em> differs from <em>deleted<\/em> using the EXCEPT SQL command. Then, only if a difference has been found, will we update the audit table. For fun, we&#8217;re also going to turn the deleted and inserted data into JSON and store the whole row to in the <em>Before<\/em> and <em>After<\/em> fields of the audit table.<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nCREATE TRIGGER &#x5B;dbo].&#x5B;ScaleUpdate]\r\nON &#x5B;dbo].&#x5B;Scale]\r\nAFTER UPDATE\r\nAS\r\nIF EXISTS (\r\n  SELECT Name, Description, Intervals FROM inserted\r\n  EXCEPT\r\n  SELECT Name, Description, Intervals FROM deleted\r\n)\r\nBEGIN\r\n  INSERT INTO dbo.Audit(Operation, DateTime, Before, After)\r\n  SELECT &#039;UPD&#039;, GETDATE(), \r\n  (select d.Name, d.Description, d.Intervals from deleted d FOR JSON PATH), \r\n  (select i.Name, i.Description, i.Intervals from inserted i FOR JSON PATH)\r\n  FROM inserted i\r\n  join deleted d on (i.Id = d.Id)\r\nEND\r\n<\/pre>\n<p>If you&#8217;d prefer to create XML output you can replace <em>JSON PATH<\/em> to <em>XML RAW<\/em>.<\/p>\n<p>Now to test these we can use<\/p>\n<pre class=\"brush: sql; title: ; notranslate\" title=\"\">\r\nDELETE from Scale where Name = &#039;Chromatic&#039;\r\n\r\nINSERT INTO Scale (Name, Intervals)\r\nVALUES (&#039;Chromatic&#039;, &#039;1,1,1,1,1,1,1,1,1,1,1,1&#039;)\r\n\r\nUPDATE Scale\r\nSET Intervals = &#039;1,1,1,1,1,1,1,1,1,1,1,2&#039;\r\nWHERE Name = &#039;Chromatic&#039;\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>We&#8217;re wanting to capture audit history on our table(s) in our database. A good way to do this is via triggers. Let&#8217;s create ourselves an Audit table first, something like this CREATE TABLE &#x5B;dbo].&#x5B;Audit]( &#x5B;Id] &#x5B;int] IDENTITY(1,1) NOT NULL, &#x5B;Operation] &#x5B;nvarchar](3) NOT NULL, &#x5B;DateTime] &#x5B;datetime] NOT NULL, &#x5B;Before] &#x5B;nvarchar](256) NULL, &#x5B;After] &#x5B;nvarchar](256) NULL, ) ON [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[724],"tags":[],"class_list":["post-10799","post","type-post","status-publish","format-standard","hentry","category-sql-server-2"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/10799","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/comments?post=10799"}],"version-history":[{"count":5,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/10799\/revisions"}],"predecessor-version":[{"id":10806,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/10799\/revisions\/10806"}],"wp:attachment":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/media?parent=10799"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/categories?post=10799"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/tags?post=10799"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}