{"id":1965,"date":"2015-03-10T00:00:00","date_gmt":"2015-03-10T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/self-maintaining-contiguous-effective-dates-in-temporal-tables\/"},"modified":"2021-09-29T16:21:29","modified_gmt":"2021-09-29T16:21:29","slug":"self-maintaining-contiguous-effective-dates-in-temporal-tables","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/self-maintaining-contiguous-effective-dates-in-temporal-tables\/","title":{"rendered":"Self-maintaining, Contiguous Effective Dates in Temporal Tables"},"content":{"rendered":"<div id=\"pretty\">\n<h1>Self-maintaining, Contiguous Effective Dates in Temporal Tables<\/h1>\n<p>In a database that is dealing with price information where a new price immediately supersedes the previous one, you can find the correct price for any particular date when all you have is an &#8216;effective date&#8217; on each row. This is because the &#8216;effective end date&#8217; is implied by the effective date on the subsequent row, and the current price is always the one with the latest date on it. It is a simple system that requires few constraints to implement.<\/p>\n<p> Although this allows us to reproduce account information from the data and run historic financial reports, it is often a better fit for the business requirements to have both an &#8216;effective start&#8217; date and an &#8216;effective end&#8217; date on each row.  This can handle a range of the more generic cases of a &#8216;<a href=\"http:\/\/en.wikipedia.org\/wiki\/Temporal_database\">transaction time table&#8217;<\/a>, and it also is much quicker to determine the current value since it will have a NULL end-date.<\/p>\n<p><a href=\"http:\/\/en.wikipedia.org\/wiki\/Joe_Celko\">Joe Celko<\/a>&#8216;s article <a href=\"https:\/\/www.simple-talk.com\/sql\/t-sql-programming\/contiguous-time-periods\/\">Contiguous Time Periods<\/a> gives a good summary of this type of table, and he described an ingenious way of enforcing the basic rules of this type of table solely via constraints, in what he called &#8220;Kuznetsov&#8217;s History Table.&#8221; This is a special version of the temporal price table which has the extra &#8216;<code>previous end-date'<\/code> included in the row.  This allows us to use a foreign key constraint to prevent a row being inserted that doesn&#8217;t have an existing row with an end-date matching the entry in the &#8216;previous-end-date&#8217; column.  While it is an interesting approach, it does have some issues that he and Alex both cover in good detail.  <\/p>\n<p>In his article &#8220;<a href=\"https:\/\/www.simple-talk.com\/sql\/t-sql-programming\/modifying-contiguous-time-periods-in-a-history-table\/\">Modifying Contiguous Time Periods in a History Table<\/a>&#8221; Alex Kuznetsov shows not only how to set this up but also how to maintain such a table, because with such tight constraints it is no mean task.  <\/p>\n<p>In this article I&#8217;ll show how it is possible to design and use such a table, but without the complexity of the  &#8216;<code>previous<\/code><code>-end-date'<\/code> column, yet still using only constraints to enforce data integrity. Then I&#8217;ll show you how to use the MERGE statement to update the table with new prices, while automatically maintaining the effective end date column. Finally, I&#8217;ll then show how it is possible to do updates to this table using only simple CRUD (Create, Read, Update, Delete) operations, hiding the complexity of the actual MERGE process in a trigger.<\/p>\n<h2>Enforcing Effective End-to-start Dates Using Constraints without the Additional Column<\/h2>\n<p>In order to demonstrate this approach, and show how it is used, we&#8217;ll use a simple example. Here is the <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ff848799.aspx\">Data Definition Language<\/a> (DDL) to create two tables.  The first is a simple Products table (which we will populate with some test data) and the second is a <code>ProductPrices<\/code> table with CONSTRAINTs sufficient to enforce this end-to-start date relationship. <\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\tCREATE TABLE dbo.Products\n\t(\n\t&#160;&#160;&#160; ProductID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; INT IDENTITY\n\t&#160;&#160;&#160; ,ProductName&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(50) NOT NULL\n\t&#160;&#160;&#160; ,DiscontinuedDT&#160;&#160;&#160;&#160; DATETIME&#160;&#160;&#160; NOT NULL DEFAULT ('2099-12-31')\n\t&#160;&#160;&#160; ,CONSTRAINT p_pk&#160;&#160;&#160; PRIMARY KEY (ProductID)&#160;&#160;&#160;&#160; \n\t);\n\t\n\tINSERT INTO dbo.Products (ProductName)\n\tVALUES ('Mouse Trap'), ('Better Mouse Trap'), ('Rat Trap');\n\t\n\tSELECT *\n\tFROM dbo.Products;\n\t\n\tCREATE TABLE dbo.ProductPrices\n\t(\n\t&#160;&#160;&#160; ProductID&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160;&#160;INT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NOT NULL\n\t&#160;&#160;&#160; ,EffectiveStartDT&#160;&#160; DATETIME&#160;&#160;&#160; NOT NULL\n\t&#160;&#160;&#160; ,EffectiveEndDT&#160;&#160;&#160;&#160; DATETIME&#160;&#160;&#160; NULL\n\t&#160;&#160;&#160; -- Time dependent attributes\n\t&#160;&#160;&#160; ,ProductPrice&#160;&#160;&#160;&#160;&#160;&#160; MONEY&#160;&#160;&#160;&#160;&#160;&#160; NOT NULL\n\t&#160;&#160;&#160; -- In PK, EffectiveStartDT should occur only once for each product\n\t&#160;&#160;&#160; ,CONSTRAINT pp_pk&#160;&#160; PRIMARY KEY (ProductID, EffectiveStartDT)\n\t&#160;&#160;&#160; --\n\t&#160;&#160;&#160; -- EffectiveEndDT should always be greater than EffectiveStartDT (unless NULL)\n\t&#160;&#160;&#160; ,CONSTRAINT pp_ck1&#160; CHECK (EffectiveEndDT &gt; EffectiveStartDT)&#160; \n\t&#160;&#160;&#160; --\n\t&#160;&#160;&#160; -- ProductPrice should always be greater than zero\n\t&#160;&#160;&#160; ,CONSTRAINT pp_ck2&#160; CHECK (ProductPrice &gt; 0) \n\t&#160;&#160;&#160; -- \n\t&#160;&#160;&#160; -- FK1: Each EffectiveEndDT should be linked to an EffectiveStartDT\n\t&#160;&#160;&#160; ,CONSTRAINT pp_fk1&#160; FOREIGN KEY (ProductID, EffectiveEndDT) \n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; REFERENCES dbo.ProductPrices(ProductID, EffectiveStartDT)\n\t&#160;&#160;&#160; --\n\t&#160;&#160;&#160; -- FK2: Products\/Prices must reflect existing Products\n\t&#160;&#160;&#160; ,CONSTRAINT pp_fk2&#160; FOREIGN KEY (ProductID) \n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; REFERENCES dbo.Products(ProductID)\n\t&#160;&#160;&#160; --\n\t&#160;&#160;&#160; -- EffectiveEndDT must be unique for each row of a product\n\t&#160;&#160;&#160; ,CONSTRAINT pp_u1&#160;&#160; UNIQUE (ProductID, EffectiveEndDT) \n\t);\n\t<\/pre>\n<p>Let&#8217;s describe the CONSTRAINTs we&#8217;ve defined on <code>ProductPrices<\/code>.<\/p>\n<ul>\n<li><code>pp_pk<\/code> &#8211; This is the <a href=\"https:\/\/technet.microsoft.com\/en-us\/library\/ms191236(v=sql.105).aspx\">PRIMARY KEY CONSTRAINT<\/a> that ensures that for any product, the effective start date must be unique across all rows.  That is, the same product cannot have the same effective start date appearing more than once.<\/li>\n<li><code>pp_ck1 <\/code>&#8211; This is a rather simple <a href=\"https:\/\/technet.microsoft.com\/en-us\/library\/ms188258(v=sql.105).aspx\">CHECK CONSTRAINT<\/a> that ensures that any effective end date (on a row) will always be greater than the effective start date on that row.<\/li>\n<li><code>pp_ck2 <\/code>&#8211; This is another fairly simple CHECK CONSTRAINT that ensures that a product&#8217;s price is always greater than zero.  Unless you are giving products away, it is unlikely the price would ever be zero, and quite unlikely that it would ever be negative.  A product&#8217;s price can also not be NULL (as defined by the column itself).  <\/li>\n<li><code>pp_fk1<\/code> &#8211; This is a self-referencing <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms189049.aspx\">FOREIGN KEY (FK) CONSTRAINT<\/a>, that says any row that has an effective end date defined, i.e., it is not NULL, must have a matching row where that row&#8217;s effective start date matches the effective end date.  This enforces the &#8220;contiguous&#8221; nature of the end-to-start date relationships in the table.<\/li>\n<li><code>pp_fk2<\/code> &#8211; This is a FOREIGN KEY CONSTRAINT that ensures that any ProductID entered into the ProductPrices table exists as a product in the Products table.<\/li>\n<li><code>pp_u1<\/code> &#8211; This is a <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms190024.aspx\">UNIQUE CONSTRAINT<\/a> that ensures that each ProductID\/EffectiveEndDT combination exists only once.<\/li>\n<\/ul>\n<p>Let&#8217;s add two additional facts about the way these CONSTRAINTs will work together:<\/p>\n<ul>\n<li>When an effective end date of NULL is inserted into the table, the self-referencing FK CONSTRAINT (<code>pp_fk1<\/code>) allows it, meaning that is the only case where there need not exist a corresponding effective start date.<\/li>\n<li>Because of the UNIQUE CONSTRAINT  \t<code>(pp_u1<\/code>), only one such NULL effective end date can be in the table for a given product.<\/li>\n<\/ul>\n<h2>Populating our Product\/Prices Table<\/h2>\n<p>Now, we will put some data into our <code>ProductPrices<\/code> table with an INSERT, where we have ensured that the contiguous nature of the end-to-start dates by setting them appropriately.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\tINSERT INTO dbo.ProductPrices \n\t(\n\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t)\n\tVALUES (1, '2013-06-04', '2013-06-17', 15.25)\n\t&#160;&#160;&#160; ,(2, '2013-05-01', '2013-08-09', 42.13)\n\t&#160;&#160;&#160; ,(1, '2013-06-17', '2013-06-25', 16.33)\n\t&#160;&#160;&#160; ,(1, '2013-06-25', '2014-02-23', 16.45)\n\t&#160;&#160;&#160; ,(2, '2013-08-09', '2013-08-10', 45.88)\n\t&#160;&#160;&#160; ,(2, '2013-08-10', '2014-03-01', 34.98)\n\t&#160;&#160;&#160; ,(2, '2014-03-01', NULL, 45.18)\n\t&#160;&#160;&#160; ,(1, '2014-02-23', NULL, 16.65)\n\t&#160;&#160;&#160; ,(3, '2013-01-01', NULL, 17.77);\n\t\n\t\n\tSELECT *\n\tFROM dbo.ProductPrices;\n\t<\/pre>\n<p>Our final SELECT returns these rows from the <code>ProductPrices<\/code> table, showing that all is well:<\/p>\n<pre>\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EffectiveEndDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 2013-06-17 00:00:00.000&#160; 15.25\n\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 2013-06-25 00:00:00.000&#160; 16.33\n\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 16.45\n\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 16.65\n\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-05-01 00:00:00.000&#160; 2013-08-09 00:00:00.000&#160; 42.13\n\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-09 00:00:00.000&#160; 2013-08-10 00:00:00.000&#160; 45.88\n\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-10 00:00:00.000&#160; 2014-03-01 00:00:00.000&#160; 34.98\n\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-03-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 45.18\n\t3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-01-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 17.77\n\t<\/pre>\n<p>The NULL <code>EffectiveEndDT <\/code>on each of the products indicates an open-ended time period, meaning that final price extends out into the future without bound.  <\/p>\n<p>Suppose we wish to find the price of a particular product on a specific date.  Let&#8217;s examine a couple of cases:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\tDECLARE @DateOfInterest DATETIME = '2013-12-31';\n\t\n\t-- Find the price effective on 2013-12-31\n\tSELECT a.ProductID, a.ProductName, b.ProductPrice\n\tFROM dbo.Products a\n\tJOIN dbo.ProductPrices b\n\tON a.ProductID = b.ProductID\n\tWHERE a.ProductID = 1 AND \n\t&#160;&#160;&#160; b.EffectiveStartDT &lt;= @DateOfInterest AND\n\t&#160;&#160;&#160; ISNULL(b.EffectiveEndDT, a.DiscontinuedDT) &gt; @DateOfInterest;\n\t\n\t-- Discontinue ProductID=1 at the end of 2014\n\tUPDATE dbo.Products\n\tSET DiscontinuedDT = '2015-01-01'\n\tWHERE ProductID = 1;\n\t\n\tSELECT @DateOfInterest = '2015-01-01';\n\t\n\t-- Find the price effective on 2015-01-01\n\tSELECT a.ProductID, a.ProductName, b.ProductPrice\n\tFROM dbo.Products a\n\tJOIN dbo.ProductPrices b\n\tON a.ProductID = b.ProductID\n\tWHERE a.ProductID = 1 AND \n\t&#160;&#160;&#160; b.EffectiveStartDT &lt;= @DateOfInterest AND\n\t&#160;&#160;&#160; ISNULL(b.EffectiveEndDT, a.DiscontinuedDT) &gt; @DateOfInterest;\n\t<\/pre>\n<p>We locate the proper time record by finding the one for that product that is greater than or equal to the start date and less than the end date. Our first query reports the product price for <code>ProductID<\/code><code>=1<\/code> on 2013-12-31 as:<\/p>\n<pre>\tProductID&#160; ProductName&#160; ProductPrice\n\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; Mouse Trap&#160;&#160; 16.45\n\t<\/pre>\n<p>Suppose we no longer want to sell mouse traps in 2015 because after that time we only want to sell better mouse traps.  We can set the discontinued date of <code>Product=1<\/code> to be 2015-01-01, and then run with the corresponding date of interest accordingly.  The second SELECT query shows this and returns no rows, indicating no price for the product on that date because it has been discontinued.<\/p>\n<p>You do need to make sure that the discontinued date is always later than the latest effective pricing record&#8217;s start date.  But that isn&#8217;t really the point of today&#8217;s exercise.<\/p>\n<h2>Our CONSTRAINTs Maintain Data Integrity for our ProductPrices<\/h2>\n<p>These constraints in our simple product\/price example are enforcing the essential business rules:<\/p>\n<ul>\n<li>All products with a current price must have a single entry that has a NULL end date<\/li>\n<li>An effective end date is always greater than the start date<\/li>\n<li>A valid price is positive and greater than zero<\/li>\n<li>All products must exist in the referenced products table<\/li>\n<li>No product should have more than one row with a NULL end-date <\/li>\n<li>No product should ever have more than one relevant row for any point in time.<\/li>\n<li>For every non-NULL end-date on a row, the next row for that product must have that end-date as the new start-date.<\/li>\n<li>No product can have duplicate start-dates or duplicate end-dates.<\/li>\n<\/ul>\n<p>If we didn&#8217;t enforce these rules, we would allow bad data, such as having an end date overlapping a start date on the following row. This would allow an item to have more than one price at any point in time.  Although this should be caught in the application&#8217;s front end, bad data can get through for one reason or another. When that happens, it must be identified and constraints set in this way will block it.<\/p>\n<p>We have not yet tested the CONSTRAINTs we&#8217;ve created to make sure that they enforce data integrity according to the business rules, so let&#8217;s do that now.<\/p>\n<h3>All products must exist in the referenced products table<\/h3>\n<pre class=\"theme:ssms2012 lang:tsql\">\t-- Fails because ProductID=4 doesn't exist (pp_fk2)\n\tINSERT INTO dbo.ProductPrices\n\t(\n\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t)\n\tVALUES (4, '2014-01-01', NULL, 21.33);\n\t\n\t-- Now we create ProductID=4 \n\tINSERT INTO dbo.Products (ProductName)\n\tVALUES ('Another trap');\n\t\n\tBEGIN TRANSACTION T1;\n\t\n\t-- This now works because ProductID = 4 exists\n\tINSERT INTO dbo.ProductPrices\n\t(\n\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t)\n\tVALUES (4, '2014-01-01', NULL, 21.33);\n\t\n\tSELECT *\n\tFROM dbo.ProductPrices\n\tWHERE ProductID = 4;\n\t\n\t-- But we will roll it back to keep this row out of our test data\n\tROLLBACK TRANSACTION T1;\n\t<\/pre>\n<p>In the above example, the first INSERT fails because <code>ProductID<\/code><code>=4<\/code> does not exist in our Products table (CONSTRAINT <code>pp_fk2<\/code>).  Once we&#8217;ve inserted <code>ProductID<\/code><code>=4<\/code>, the exact same insert works just fine.  Note how at the end we rolled back the transaction we created before our second insert into <code>ProductPrices<\/code>.  We&#8217;ve done this to maintain the previously noted entries in that table, and we will continue to do this for any T-SQL query that would change the data without violating one of our constraints.<\/p>\n<p>Since the INSERT of <code>ProductID<\/code><code>=4<\/code> into the <code>ProductPrices<\/code> table above does not fail, it is possible to seed each product with an open-ended price without any problems.<\/p>\n<h3>An effective end date is always greater than the start date<\/h3>\n<pre class=\"theme:ssms2012 lang:tsql\">\t-- An effective end date that is less than the start date\n\tINSERT INTO dbo.ProductPrices \n\t(\n\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t)\n\tVALUES (4, '2014-01-01', NULL, 12.33) \n\t&#160;&#160;&#160; ,(4, '2014-06-04', '2014-01-01', 18.25);\n\t<\/pre>\n<p>This INSERT would violate <code>pp_ck1<\/code>.<\/p>\n<h3>A valid price is positive and greater than zero<\/h3>\n<pre class=\"theme:ssms2012 lang:tsql\">\t-- A product price that is zero\n\tINSERT INTO dbo.ProductPrices \n\t(\n\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t)\n\tVALUES (4, '2014-01-01', NULL, 0);\n\t<\/pre>\n<p>This insert violates <code>pp_ck2<\/code>.  <code>ProductID<\/code><code>=4<\/code> remains in our Products table because we left it there in the previous set of queries. Remember at this time there are still no price records for <code>ProductID<\/code><code>=4<\/code> (both insert attempts failed).<\/p>\n<h3>No product can have duplicate start-dates or end-dates <\/h3>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\t-- A duplicate effective start date\n\t\tINSERT INTO dbo.ProductPrices \n\t\t(\n\t\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t)\n\t\tVALUES (4, '2013-06-04', '2013-12-31', 18.25)\n\t\t&#160;&#160;&#160;  ,(4, '2013-06-04', NULL, 19.22);<\/pre>\n<p>This insert fails on the PRIMARY KEY CONSTRAINT (<code>pp_pk<\/code>) because we&#8217;re trying to insert the same effective start date more than once.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\t&#160;  -- A duplicate effective end date\n\t\tINSERT INTO dbo.ProductPrices \n\t\t(\n\t\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t)\n\t\tVALUES (4, '2013-06-04', '2013-12-31', 18.25)\n\t\t&#160;&#160;&#160; ,(4, '2013-06-30', '2013-12-31', 19.22);\n\t\t<\/pre>\n<p>This fails because we&#8217;re trying to insert the same effective end date twice, violating our UNIQUE CONSTRAINT (<code>pp_u1<\/code>).  Note that were both end dates NULL, the UNIQUE constraint would still fail (only one NULL end date is allowed for each product).  <\/p>\n<h3>For every non-NULL end-date on a row, the next row for that product must have that end-date as the new start-date<\/h3>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\t-- A gap between the first and second records' effective dates\n\t\tINSERT INTO dbo.ProductPrices \n\t\t(\n\t\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t)\n\t\tVALUES (4, '2013-06-04', '2013-12-31', 18.25)\n\t\t&#160;&#160;&#160; ,(4, '2014-01-01', '2014-08-09', 22.13)\n\t\t&#160;&#160;&#160; ,(4, '2014-08-09', NULL, 25.13);\n\t\t\n\t\t-- An overlap between the first and second record's effective dates\n\t\tINSERT INTO dbo.ProductPrices \n\t\t(\n\t\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t)\n\t\tVALUES (4, '2013-06-04', '2014-01-01', 18.25)\n\t\t&#160;&#160;&#160; ,(4, '2013-12-30', '2014-08-09', 22.13)\n\t\t&#160;&#160;&#160; ,(4, '2014-08-09', NULL, 25.13);\n\t\t<\/pre>\n<p>These two fail the self-referencing FOREIGN KEY CONSTRAINT (<code>pp_fk1<\/code>) because of either a gap or an overlap in the end-to-start date relationship, meaning those rows we&#8217;re trying to insert don&#8217;t have contiguous dates.<\/p>\n<h1>Maintaining our ProductPrices &#8211; The Dilemma<\/h1>\n<p>We&#8217;ve already shown that it is pretty simple to insert a new product&#8217;s prices into our table.  But what of the case where we need to do any of the following:<\/p>\n<ul>\n<li>Insert a new row where a product already has a price record.<\/li>\n<li>Change a product price for an existing row.<\/li>\n<li>Change the effective start date for an existing product\/price.<\/li>\n<li>Delete the row for an effective date.<\/li>\n<\/ul>\n<p>Some of these things are easy to do, while some are less trivial.  For example:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\tBEGIN TRANSACTION T1;\n\t\n\t-- Case 1: Insert a new price row for an existing product\n\tINSERT INTO dbo.ProductPrices\n\t(\n\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t)\n\tVALUES (1, '2013-06-01', '2013-06-04', 18.25);\n\t<\/pre>\n<p>The insert case above works only because we are inserting the first row (prior to the earliest effective date) and because we know what the next effective start date is (2013-06-04).  In order to insert a row anywhere else, it is necessary to modify at least one other row&#8217;s effective end date, and that is not something that can be done with an INSERT.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\t-- Case 2: Change a product price\n\t\tUPDATE dbo.ProductPrices\n\t\tSET ProductPrice = 14.25\n\t\tWHERE ProductID = 1 AND EffectiveStartDT = '2013-06-17'\n\t\t\n\t\tSELECT *\n\t\tFROM dbo.ProductPrices\n\t\tWHERE ProductID = 1;\n\t\t<\/pre>\n<p>The update in Case 2 above works because all we are changing is the product&#8217;s price.  Had we tried instead to modify <code>ProductID<\/code>, <code>EffectiveStartDT<\/code> or <code>EffectiveEndDT<\/code> for that row, it would have violated one of the CONSTRAINTS.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\t-- Case 3: Change the effective start date for the first product\/price\n\t\tUPDATE dbo.ProductPrices\n\t\tSET EffectiveStartDT = '2013-05-01'\n\t\tWHERE ProductID = 1 AND EffectiveStartDT = '2013-06-01'\n\t\t\n\t\tSELECT *\n\t\tFROM dbo.ProductPrices\n\t\tWHERE ProductID = 1;\n\t\t<\/pre>\n<p>While it is possible to change the effective start date of the first row (earliest effective start date), it is not possible to modify the effective start date of any other row.  In order to do that, once again it becomes necessary to update the effective end date of at least one other row, and while that could be done with a carefully crafted UPDATE, it is a non-trivial exercise.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\t-- Case 4: Delete the row for the first product\/price\n\t\tDELETE FROM dbo.ProductPrices\n\t\tWHERE ProductID = 1 AND EffectiveStartDT = '2013-05-01'\n\t\t\n\t\tSELECT *\n\t\tFROM dbo.ProductPrices\n\t\tWHERE ProductID = 1;\n\t\t\n\t\tROLLBACK TRANSACTION T1;\n\t\t<\/pre>\n<p>Once again, it is possible to delete the first effective start date row for any product, but the same cannot be done if the row to be deleted is in the middle or at the end of the set without violating one or more CONSTRAINTs.  Unless of course it was possible to at the same time modify the effective end dates of one or more rows using DELETE (which it is not).<\/p>\n<p>The astute SQL-ers will at this time raise their collective hands and say, &#8220;hey wait a minute.  All those things that you said INSERT, UPDATE and DELETE cannot do could be done by using MERGE instead.&#8221;  And you would be right!  In fact, that is precisely how Alex proposed to manage maintenance of his history tables in the referenced articles.  He simply developed a stored procedure to maintain the tables using a very clever MERGE.  <\/p>\n<p>So clearly we could do the same thing here, albeit a little differently due to the fact that we&#8217;ve constructed our CONSTRAINTs somewhat differently. <\/p>\n<p>To demonstrate this, we&#8217;ll starting with an empty ProductPrices table and insert some sample data.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tINSERT INTO dbo.ProductPrices (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice)\n\t\tVALUES  (1, '2013-01-01', '2013-04-01', 11.11)\n\t\t&#160;&#160;&#160; ,(1, '2013-04-01', '2013-06-01', 12.12)\n\t\t&#160;&#160;&#160; ,(1, '2013-06-01', NULL, 13.13);\n\t\t<\/pre>\n<p>And we can now simply insert a new current price, modifying the existing price by changing the NULL to the end date and inserting a new row with the current price. We have to supply the row to be updated and the new row<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tMERGE dbo.ProductPrices t\n\t\tUSING  (\n\t\tVALUES  (1,'2013-06-01','2013-06-05',14.14),\n\t\t&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;  (1,'2013-06-05', NULL, 14.14)\n\t\t) s (ProductID, EffectiveStartDT,EffectiveEndDT, ProductPrice)\n\t\t&#160; ON s.ProductID=t.ProductID and s.EffectiveStartDT=t.EffectiveStartDT\n\t\t&#160;&#160;&#160; WHEN MATCHED THEN UPDATE SET EffectiveEndDT = s.EffectiveEndDT\n\t\t&#160;&#160;&#160; WHEN NOT MATCHED THEN INSERT (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice)\n\t\tVALUES(s.ProductID, s.EffectiveStartDT, s.EffectiveEndDT, s.ProductPrice);\n\t\t<\/pre>\n<p>This is fine where the SQL is hand-crafted, or you are using an isolation layer of stored procedures.  What if the application has direct access to this table and the ORM (Object Relational Mapper), or UI Widget, being used is able to use only simple SELECT, INSERT, UPDATE and UPDATE statements?<\/p>\n<p>In cases like this it would be impossible to impose any requirement on the application to do a MERGE statement.<\/p>\n<p>We also may not want our application to have to decide what the correct effective end date is when we insert, update or delete any row in our product prices table.  Since the title of this article includes the phrase &#8220;self-maintaining,&#8221; we want our SQL to do just that. Is it possible to isolate all the complexity from the application, yet allow them to access the table directly to maintain it?<\/p>\n<p>While that sounds like a pretty tall order, it probably won&#8217;t surprise you that I&#8217;ve found a way to do all of this thanks to the wonders of a SQL 2012 <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/hh213125.aspx\">LEAD analytical function<\/a>. <\/p>\n<h1>Review of the SQL 2012 LEAD Analytical Function<\/h1>\n<p>Let&#8217;s show a quick and simple example of LEAD when applied to the <code>ProductPrices<\/code> table:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tSELECT *\n\t\t&#160;&#160;&#160; ,NextEffectiveStartDT = LEAD(EffectiveStartDT, 1, 0) OVER\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (PARTITION BY ProductID ORDER BY EffectiveStartDT)\n\t\tFROM dbo.ProductPrices;\n\t\t<\/pre>\n<p>The results produced by this SELECT are:<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EffectiveEndDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice&#160; NextEffectiveStartDT\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 2013-06-17 00:00:00.000&#160; 15.25&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 2013-06-25 00:00:00.000&#160; 16.33&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 16.45&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 16.65&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 1900-01-01 00:00:00.000\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-05-01 00:00:00.000&#160; 2013-08-09 00:00:00.000&#160; 42.13&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-09 00:00:00.000\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-09 00:00:00.000&#160; 2013-08-10 00:00:00.000&#160; 45.88&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-10 00:00:00.000\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-10 00:00:00.000&#160; 2014-03-01 00:00:00.000&#160; 34.98&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-03-01 00:00:00.000\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-03-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160;&#160;&#160;&#160;45.18&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 1900-01-01 00:00:00.000\n\t\t3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-01-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 17.77&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 1900-01-01 00:00:00.000\n\t\t<\/pre>\n<p>Here we have used the optional second and third arguments of LEAD:<\/p>\n<ul>\n<li>The second argument: the offset (number of rows) to look ahead to retrieve the value for <code>EffectiveStartDT<\/code>.<\/li>\n<li>The third argument: by setting the third argument to 0, when there is no following row the <code>NextEffectiveStartDT<\/code> is set to 1900-01-01, which works quite well for the DATETIME data type, but not for other date or time data types.  For those you&#8217;d need to use an explicit date, like &#8216;1900-01-01&#8217;.  Ultimately, you should use any date that isn&#8217;t expected to be within the time period of your effective dates.<\/li>\n<\/ul>\n<p>For each case except the latest effective start date row, the <code>NextEffectiveStartDT<\/code><code><\/code>matches the <code>EffectiveStartDT<\/code> of the following row.  This will be quite useful in what we&#8217;re about to propose.<\/p>\n<h2>Building Blocks of a TRIGGER to Support Self-maintenance of our Effective Dates<\/h2>\n<p>The title of this subsection gives away the show, however as always the devil is in the details.<\/p>\n<p>Before we propose a TRIGGER (an INSTEAD OF TRIGGER), let&#8217;s review a couple of features that are available in all T-SQL TRIGGERS, specifically the virtual tables.<\/p>\n<ul>\n<li><code>INSERTED<\/code> &#8211; The INSERTED virtual table contains one row for each row to be inserted into the table at the time the TRIGGER is fired.  In the case of an INSTEAD OF TRIGGER, these rows have not yet been inserted.  On a DELETE event, the INSERTED virtual table contains no rows, but on INSERT and UPDATE events it will always have at least one row.<\/li>\n<li><code>DELETED<\/code> &#8211; The DELETED virtual table contains one row for each row to be deleted from the table at the time the TRIGGER is fired.  In the case of an INSTEAD OF TRIGGER, these rows have not yet been deleted.  On an INSERT event, the DELETED virtual table contains no rows, but on DELETE and UPDATE events it will always have at least one row.<\/li>\n<\/ul>\n<p>It is also important to note that on an UPDATE event, both INSERTED and DELETED tables contain precisely the same number of rows.  Both tables have all of the columns that correspond to the underlying table on which the TRIGGER is defined.<\/p>\n<p>Suppose that we&#8217;re inside of a TRIGGER, and we run the following query:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\tWITH TargetPrices (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t(\n\t&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t&#160;&#160;&#160; FROM dbo.ProductPrices a\n\t&#160;&#160;&#160; WHERE EXISTS\n\t&#160;&#160;&#160; (\n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM INSERTED x\n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL\n\t&#160;&#160;&#160;&#160; &#160;&#160;&#160;SELECT 1 \n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM DELETED x\n\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t&#160;&#160;&#160; )\n\t)\n\tSELECT *\n\tFROM TargetPrices;<\/pre>\n<p>It should be reasonably obvious, given what we&#8217;ve said about the INSERTED and DELETED virtual tables above, that this query will produce a results set that includes all rows from<code><\/code><code>ProductPrices<\/code> for any <code>ProductID<\/code><code><\/code>that is contained in either of the INSERTED or DELETED virtual tables.  We won&#8217;t show that set, and if you&#8217;re unclear on this you may want to construct your own TRIGGER and put this SELECT statement in it to see the results.<\/p>\n<p>We&#8217;ve called our <a href=\"https:\/\/technet.microsoft.com\/en-us\/library\/ms190766(v=sql.105).aspx\">Common Table Expression<\/a> (<a href=\"http:\/\/dwaincsql.com\/2014\/03\/23\/common-table-expressions-in-sql\/\">CTE<\/a>) <code>TargetPrices<\/code> for a reason.  This will be the target of the actual maintenance work we&#8217;re going to perform.  However we will impose one additional requirement on our TRIGGER, and that is that it should only &#8220;touch&#8221; (maintain) rows that must be maintained based on the underlying operation.  And that&#8217;s usually going to mean not all of the rows in <code>TargetPrices<\/code><code><\/code>should be touched.  We&#8217;ll see how this can be done in a moment.<\/p>\n<p>Let&#8217;s now look at another interesting query, which will probably require some examples to understand it.  Again, we must assume we are operating inside of our TRIGGER for INSERTED and DELETED to be defined, but we can simulate that case using CTEs.  This also happens to be a useful way to simulate code executing in a TRIGGER that uses the INSERTED\/DELETED tables, in case you&#8217;ve ever struggled with that before.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tWITH INSERTED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-10', NULL, CAST(55.55 AS MONEY)\n\t\t),\n\t\t&#160;&#160;&#160; DELETED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, 0, NULL, CAST(0 AS MONEY)\n\t\t&#160;&#160;&#160; WHERE 1=0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- This gives us 0 rows in the DELETED table\n\t\t),\n\t\t&#160;&#160;&#160; TargetPrices AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t&#160;&#160;&#160; FROM dbo.ProductPrices a\n\t\t&#160;&#160;&#160; WHERE EXISTS\n\t\t&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM INSERTED x\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1 \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM DELETED x\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t\t&#160;&#160;&#160; )\n\t\t)\n\t\tSELECT ProductID, EffectiveStartDT, ProductPrice\n\t\tFROM TargetPrices\n\t\tUNION ALL\n\t\tSELECT ProductID, EffectiveStartDT, ProductPrice\n\t\tFROM INSERTED\n\t\tEXCEPT  \n\t\tSELECT ProductID, EffectiveStartDT, ProductPrice\n\t\tFROM DELETED;\n\t\t<\/pre>\n<p>The INSERTED CTE contains one row that we wish to insert and the DELETED CTE contains no rows, simulating what might happen on a single row insert within the TRIGGER.  The results set produced is this.  If there were rows in the DELETED table, those corresponding rows would be removed from the resulting set by the EXCEPT.<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-10 00:00:00.000&#160; 55.55\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 16.33\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 16.45\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23  00:00:00.000&#160; 16.65<\/pre>\n<p>Notice how the new effective start date we want to insert (2013-06-10) appears in the results set.<\/p>\n<p>Now let&#8217;s consider some other cases.  The first simulates updating a price for an existing row (we&#8217;ll show only the INSERTED and DELETED CTEs for brevity). <\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">WITH INSERTED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-17', '2013-06-25', CAST(16.88 AS MONEY)\n\t\t),\n\t\t&#160;&#160;&#160; DELETED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-17', '2013-06-25', CAST(16.33 AS MONEY)\n\t<\/pre>\n<p>Both INSERTED and DELETED include the effective start date of 2013-06-17 for ProductID=1, with DELETED showing the old price and INSERTED showing the new price (the same way the TRIGGER would populate the virtual tables).  These results appear as follows (using the full query above).<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 16.88\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 16.45\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; 16.65\n\t\t<\/pre>\n<p>Note how the target row (effective start date = 2013-06-17) shows only the new product price.<\/p>\n<p>The next case shows the same two CTEs if we were attempting to update the 2013-06-17 record&#8217;s effective start date, without wanting to concern ourselves with the effective end date.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tWITH INSERTED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-16', NULL, CAST(16.33 AS MONEY)\n\t\t),\n\t\t&#160;&#160;&#160; DELETED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-17', '2013-06-25', CAST(16.33 AS MONEY)\n\t\t<\/pre>\n<p>The full query would produce these results, again eliminating the effective start date row for 2013-06-17.<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-16 00:00:00.000&#160; 16.33\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 16.45\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; 16.65\n\t\t<\/pre>\n<p>These results are the final rows we&#8217;d expect.<\/p>\n<p>One more, to illustrate the case of a DELETE.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tWITH INSERTED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, NULL, NULL, CAST(16.33 AS MONEY)\n\t\t&#160;&#160;&#160; WHERE 1=0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- This gives us 0 rows in the INSERTED table\n\t\t),\n\t\t&#160;&#160;&#160; DELETED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-17', '2013-06-25', CAST(16.33 AS MONEY)\n\t\t<\/pre>\n<p>These results are once again the three rows we&#8217;d like to be left with, excluding of course the requisite, contiguous effective end dates.<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 16.45\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; 16.65\n\t\t<\/pre>\n<p>Now we will modify our final query slightly to recalculate (adjust) what the effective end date should be for one of these cases (we&#8217;ll choose the third case where we updated the effective start date for 2013-06-17), using the magic of the SQL 2012 LEAD analytical function.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tWITH INSERTED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-16', NULL, CAST(16.33 AS MONEY)\n\t\t),\n\t\t&#160;&#160;&#160; DELETED (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-17', '2013-06-25', CAST(16.33 AS MONEY)\n\t\t)\n\t\t,\n\t\t&#160;&#160;&#160; TargetPrices AS\n\t\t(\n\t\t&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t&#160;&#160;&#160; FROM dbo.ProductPrices a\n\t\t&#160;&#160;&#160; WHERE EXISTS\n\t\t&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM INSERTED x\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1 \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM DELETED x\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t\t&#160;&#160;&#160; )\n\t\t)\n\t\tSELECT ProductID, EffectiveStartDT\n\t\t&#160;&#160;&#160; ,NextEffectiveStartDT=LEAD(a.EffectiveStartDT, 1, 0) OVER \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (PARTITION BY a.ProductID ORDER BY a.EffectiveStartDT)\n\t\t&#160;&#160;&#160; -- All time-sensitive attributes\n\t\t&#160;&#160;&#160; ,ProductPrice\n\t\tFROM\n\t\t(\n\t\t&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- All time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice\n\t\t&#160;&#160;&#160; FROM TargetPrices\n\t\t&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- All time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice\n\t\t&#160;&#160;&#160; FROM INSERTED\n\t\t&#160;&#160;&#160; EXCEPT \n\t\t&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT&#160;&#160;&#160;&#160; &#160;&#160;&#160;-- All time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice\n\t\t&#160;&#160;&#160; FROM DELETED\n\t\t) a;\n\t\t<\/pre>\n<p>This produces a result set that has the contiguous end date saved in the NextEffectiveStartDT column:<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NextEffectiveStartDT&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 2013-06-16 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-16 00:00:00.000&#160; 2013-06-25 00:00:00.000&#160; 16.33\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 16.45\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; 1900-01-01 00:00:00.000&#160; 16.65\n\t\t<\/pre>\n<p>Note that for the time being, the last row doesn&#8217;t have a NULL effective end date, but we&#8217;ll address that later.<\/p>\n<p>By now you may have deduced what we are about to propose, which is a single INSTEAD OF TRIGGER that fires on INSERT, UPDATE and DELETE, which does its work using a MERGE statement.<\/p>\n<h2>The TRIGGER to Self-maintain Contiguous, Effective Dates<\/h2>\n<p>Without any preamble, we&#8217;ll list out our TRIGGER now.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tCREATE TRIGGER dbo.MaintainEffectiveEndDT\n\t\tON dbo.ProductPrices\n\t\tINSTEAD OF INSERT, UPDATE, DELETE\n\t\tAS\n\t\tBEGIN\n\t\t\n\t\t&#160;&#160;&#160; PRINT 'INSTEAD OF INSERT\/UPDATE\/DELETE TRIGGER';\n\t\t\n\t\t&#160;&#160;&#160; WITH TargetPrices AS\n\t\t&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- All Product\/Price rows where a ProductID appears in either INSERTED or\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- DELETED virtual tables\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM dbo.ProductPrices a\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE EXISTS\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM INSERTED x\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1 \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM DELETED x\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE a.ProductID = x.ProductID\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; )\n\t\t&#160;&#160;&#160; ),\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SourcePrices AS\n\t\t&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- This contains the rows we'll be attempting to MERGE against the Targer Prices\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- with an additional column for the adjusted effective end date\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,NextEffectiveStartDT=LEAD(a.EffectiveStartDT, 1, 0) OVER \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (PARTITION BY a.ProductID ORDER BY a.EffectiveStartDT)\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- All time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- All time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM TargetPrices\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT \n\t\t&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;-- All time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM INSERTED\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EXCEPT \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT ProductID, EffectiveStartDT \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- All time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160;FROM DELETED\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) a\n\t\t&#160;&#160;&#160; )\n\t\t&#160;&#160;&#160; -- Perform the merge on TargetPrices from SourcePrices\n\t\t&#160;&#160;&#160; MERGE TargetPrices t\n\t\t&#160;&#160;&#160; USING SourcePrices s\n\t\t&#160;&#160;&#160; -- Matching on the PRIMARY KEY of the ProductPrices table\n\t\t&#160;&#160;&#160; ON s.ProductID = t.ProductID AND \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; s.EffectiveStartDT = t.EffectiveStartDT\n\t\t&#160;&#160;&#160; -- On a match, only update the row if something has changed \n\t\t&#160;&#160;&#160; WHEN MATCHED AND \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (s.NextEffectiveStartDT &lt;&gt; ISNULL(t.EffectiveEndDT, 0) OR\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Any of the time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; s.ProductPrice &lt;&gt; t.ProductPrice) \n\t\t&#160;&#160;&#160; THEN UPDATE \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; SET EffectiveEndDT = NULLIF(s.NextEffectiveStartDT, 0)\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Include here all of the time-sensitive attributes\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ProductPrice = s.ProductPrice -- ISNULL(s.ProductPrice, t.ProductPrice)\n\t\t&#160;&#160;&#160; WHEN NOT MATCHED -- BY TARGET\n\t\t&#160;&#160;&#160; -- Insert the new row\n\t\t&#160;&#160;&#160; THEN INSERT\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice)\n\t\t&#160;&#160;&#160; -- Using the calculated effective end date\n\t\t&#160;&#160;&#160; VALUES\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (s.ProductID, s.EffectiveStartDT, NULLIF(s.NextEffectiveStartDT, 0), s.ProductPrice)\n\t\t&#160;&#160;&#160; -- If a row is not in our source and it is in the DELETED virtual table\n\t\t&#160;&#160;&#160; -- that means it needs to be deleted from the target\n\t\t&#160;&#160;&#160; WHEN NOT MATCHED BY SOURCE AND EXISTS\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM DELETED x\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE t.ProductID = x.ProductID AND t.EffectiveStartDT = x.EffectiveStartDT\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; )\n\t\t&#160;&#160;&#160; THEN DELETE;\n\t\t\n\t\tEND\n\t\t<\/pre>\n<p>Hopefully, the comments and the previous section&#8217;s examples of LEAD should explain the logic sufficiently to get a basic understanding of what this TRIGGER is doing.  However we&#8217;ll provide some additional explanation because the MERGE itself is not particularly straightforward:<\/p>\n<ul>\n<li>The results from the <code>TargetPrices<\/code> and <code>SourcePrices<\/code> CTEs were described above.<\/li>\n<li><a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms177562.aspx\">NULLIF<\/a> appears in a couple of places.  These are used to convert our invalid dates (1900-01-01) to NULL when it is appropriate to do so.<\/li>\n<li>Our matching criteria <code>(<\/code><code>ProductID<\/code> and <code>EffectiveStartDT<\/code>) is always the PRIMARY KEY for the underlying table.<\/li>\n<li>The WHEN MATCHED clause identifies cases where we have a matching Source=Target row but something has changed &#8211; either <code>EffectiveEndDT<\/code><code><\/code>is different than the calculated, adjusted end date (=<code>NextEffectiveStartDT<\/code>), or the <code>ProductPrice<\/code> (or any of the other time sensitive attributes were they present on this row).  This is one way that we&#8217;re narrowing the actual rows affected by our TRIGGER, to only those we really need to touch.<\/li>\n<li>The WHEN NOT MATCHED (BY TARGET) clause identifies cases where an insert is required.  This is done directly from the source, using <code>NextEffectiveStartDT<\/code> (our adjusted, calculated effective end date) as the effective end date for the inserted row.<\/li>\n<li>The WHEN NOT MATCHED BY SOURCE clause identifies rows in our target to be deleted, but only when those PRIMARY KEYs exist in the DELETED virtual table.<\/li>\n<\/ul>\n<p>You may also be wondering how this is going to work, for those cases above that we said couldn&#8217;t be done for the respective INSERT, UPDATE and DELETE cases.  The reason it will work is that within the MERGE statement, all necessary adjustments are made to the effective end dates prior to the process writing anything to the table, and SQL Server validating the CONSTRAINTs.  Maybe it is simply best to see it in action.<\/p>\n<h2>Testing our Self-maintaining Effective Dates TRIGGER<\/h2>\n<p>Let&#8217;s start with some cases of INSERTs.  In the first, we&#8217;ll truncate our table and restore the original target row set without having to specify the effective end dates for any of the inserted rows (the TRIGGER does this for us).<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\tTRUNCATE TABLE dbo.ProductPrices;\n\t\n\tINSERT INTO dbo.ProductPrices \n\t(\n\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t)\n\tVALUES (1, '2013-06-04', NULL, 15.25)\n\t&#160;&#160;&#160; ,(2, '2013-05-01', NULL, 42.13)\n\t&#160;&#160;&#160; ,(1, '2013-06-17', NULL, 16.33)\n\t&#160;&#160;&#160; ,(1, '2013-06-25', NULL, 16.45)\n\t&#160;&#160;&#160; ,(2, '2013-08-09', NULL, 45.88)\n\t&#160;&#160;&#160; ,(2, '2013-08-10', NULL, 34.98)\n\t&#160;&#160;&#160; ,(2, '2014-03-01', NULL, 45.18)\n\t&#160;&#160;&#160; ,(1, '2014-02-23', NULL, 16.65)\n\t&#160;&#160;&#160; ,(3, '2013-01-01', NULL, 17.77);\n\t\n\tSELECT *\n\tFROM dbo.ProductPrices;\n\t<\/pre>\n<p>The results you should see, are the same as the original results we told you to keep in mind above, but they&#8217;re shown again here to keep them fresh in your mind.<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EffectiveEndDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160; &#160;&#160;&#160;&#160;&#160;&#160;&#160;2013-06-04 00:00:00.000&#160; 2013-06-17 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 2013-06-25 00:00:00.000&#160; 16.33\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 16.45\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;16.65\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-05-01 00:00:00.000&#160; 2013-08-09 00:00:00.000&#160; 42.13\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-09 00:00:00.000&#160; 2013-08-10 00:00:00.000&#160; 45.88\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-10 00:00:00.000&#160; 2014-03-01 00:00:00.000&#160; 34.98\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-03-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;45.18\n\t\t3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-01-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 17.77\n\t\t<\/pre>\n<p>Let&#8217;s look at some additional cases of INSERT.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tBEGIN TRANSACTION T1;\n\t\t\n\t\tINSERT INTO dbo.ProductPrices \n\t\t(\n\t\t&#160;&#160;&#160; ProductID, EffectiveStartDT, EffectiveEndDT, ProductPrice\n\t\t)\n\t\t-- A row in the middle of ProductID=1\n\t\tVALUES (1, '2013-06-18', NULL, 21.50)\n\t\t-- Another row inserted in the middle of ProductID=1\n\t\t&#160;&#160;&#160; ,(1, '2013-06-28', NULL, 33.50)\n\t\t-- A row at the beginning of ProductID=2\n\t\t&#160;&#160;&#160; ,(2, '2013-01-01', NULL, 22.50)\n\t\t-- A row at the end of ProductID=3\n\t\t&#160;&#160;&#160; ,(3, '2014-01-01', NULL, 12.22);\n\t\t\n\t\tSELECT *\n\t\tFROM ProductPrices;\n\t\t\n\t\tROLLBACK TRANSACTION T1;\n\t\t<\/pre>\n<p>The results below highlight the rows that were inserted (in yellow) and those whose effective end date were adjusted (in green).<\/p>\n<pre class=\"theme:ssms2012 lang:tsql mark:4,6,8,14\">\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EffectiveEndDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 2013-06-17 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 2013-06-18 00:00:00.000&#160; 16.33\n\t\t 1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-18 00:00:00.000&#160; 2013-06-25 00:00:00.000&#160; 21.50\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 2013-06-28 00:00:00.000&#160; 16.45\n\t\t 1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-28 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 33.50\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 16.65\n\t\t 2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-01-01 00:00:00.000&#160; 2013-05-01 00:00:00.000&#160; 22.50\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-05-01 00:00:00.000&#160; 2013-08-09 00:00:00.000&#160; 42.13\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-09 00:00:00.000&#160; 2013-08-10 00:00:00.000&#160; 45.88\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-10 00:00:00.000&#160; 2014-03-01 00:00:00.000&#160; 34.98\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-03-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 45.18\n\t\t3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-01-01 00:00:00.000&#160; 2014-01-01 00:00:00.000&#160; 17.77\n\t\t 3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-01-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 12.22\n\t\t<\/pre>\n<p>If we count up the yellow and green highlighted rows, we get seven, and that interestingly corresponds to precisely the number of rows reported in the SQL Server Management Studio (SSMS) Messages pane as having been touched by our TRIGGER:<\/p>\n<pre>\t\tINSTEAD OF INSERT\/UPDATE\/DELETE TRIGGER\n\t\t\n\t\t(7 row(s) affected)\n\t\t<\/pre>\n<p>Now let&#8217;s try some updates, doing so by using a CTE to consolidate the information to update.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tBEGIN TRANSACTION T1;\n\t\t\n\t\tWITH OurUpdates (ProductID, EffectiveStartDT, NewEffectiveStartDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; -- Change the effective start date on a row\n\t\t&#160;&#160;&#160; SELECT 1, CAST('2013-06-25' AS DATETIME)\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CAST('2013-06-16' AS DATETIME), CAST(NULL AS MONEY)\n\t\t&#160;&#160;&#160; UNION ALL \n\t\t&#160;&#160;&#160; -- Change the price on a row\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-17', NULL, 16.88\n\t\t&#160;&#160;&#160; UNION ALL \n\t\t&#160;&#160;&#160; -- Move a row to the beginning\n\t\t&#160;&#160;&#160; SELECT 2, '2013-08-10', '2013-01-01', NULL\n\t\t&#160;&#160;&#160; UNION ALL \n\t\t&#160;&#160;&#160; -- Move a row to the End and change the price\n\t\t&#160;&#160;&#160; SELECT 2, '2013-03-01', '2014-05-01', 11.11\n\t\t)\n\t\tUPDATE a\n\t\tSET EffectiveStartDT&#160;&#160;&#160; = ISNULL(b.NewEffectiveStartDT, a.EffectiveStartDT)\n\t\t&#160;&#160;&#160; ,ProductPrice&#160;&#160;&#160;&#160;&#160;&#160; = ISNULL(b.ProductPrice, a.ProductPrice)\n\t\tFROM dbo.ProductPrices a\n\t\tJOIN OurUpdates b \n\t\tON a.ProductID = b.ProductID AND a.EffectiveStartDT = b.EffectiveStartDT;\n\t\t\n\t\tSELECT *\n\t\tFROM ProductPrices\n\t\tWHERE ProductID IN (1, 2);\n\t\t\n\t\tROLLBACK TRANSACTION T1;\n\t\t<\/pre>\n<p>An important detail of this UPDATE is the use of <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms184325.aspx\">ISNULL<\/a> (or <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms190349.aspx\">COALESCE<\/a> for the purist).  You should do this to ensure (for example) that moving a row without specifying the<code><\/code><code>ProductPrice<\/code>, retains the original product price.<\/p>\n<p>We&#8217;ll see this from these results, where changes are once again highlighted (yellow for moved or updated rows, and green for adjusted effective end dates):<\/p>\n<pre class=\"mark:3,4,6,9\">\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EffectiveEndDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 2013-06-16 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-16 00:00:00.000&#160; 2013-06-17 00:00:00.000&#160; 16.45&#160;&#160;&#160;&#160;&#160;&#160;&#160; &lt;-- was start date 2013-06-25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 16.88\n\t\t1&#160; &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;2014-02-23 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 16.65\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-01-01 00:00:00.000&#160; 2013-05-01 00:00:00.000&#160; 34.98&#160;&#160;&#160;&#160;&#160;&#160;&#160; &lt;-- was start date 2013-08-10\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-05-01 00:00:00.000&#160; 2013-08-09 00:00:00.000&#160; 42.13\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-09 00:00:00.000&#160; 2014-05-01 00:00:00.000&#160; 45.88\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-05-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 11.11&#160;&#160;&#160;&#160;&#160;&#160;&#160; &lt;-- was start date 2014-03-01\n\t\t<\/pre>\n<p>In this case, if we specified the <code>ProductPrice<\/code> as NULL, it is an indicator that we didn&#8217;t want to change it.  In this case, the SSMS Messages pane reports this action for the TRIGGER:<\/p>\n<pre>\t\tINSTEAD OF INSERT\/UPDATE\/DELETE TRIGGER\n\t\t\n\t\t(9 row(s) affected)\n\t\t<\/pre>\n<p>This is three more than the number of highlighted rows, because in the cases where a row is moved (effective start date is changed), it involves a delete and an insert.<\/p>\n<p>Let&#8217;s now try some deletes, once again consolidated into a CTE.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tBEGIN TRANSACTION T1;\n\t\t\n\t\tWITH OurDeletes (ProductID, EffectiveStartDT) AS\n\t\t(\n\t\t&#160;&#160;&#160; -- First row for ProductID=2\n\t\t&#160;&#160;&#160; SELECT 2, CAST('2013-05-01' AS DATETIME)\n\t\t&#160;&#160;&#160; UNION ALL \n\t\t&#160;&#160;&#160; -- Last row for ProductID=2\n\t\t&#160;&#160;&#160; SELECT 2, '2014-03-01'\n\t\t&#160;&#160;&#160; UNION ALL \n\t\t&#160;&#160;&#160; -- A row from the middle of ProductID=1\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-17'\n\t\t&#160;&#160;&#160; UNION ALL \n\t\t&#160;&#160;&#160; -- Another row from the middle of ProductID=1\n\t\t&#160;&#160;&#160; SELECT 1, '2013-06-25'\n\t\t)\n\t\tDELETE FROM a\n\t\tFROM dbo.ProductPrices a\n\t\tJOIN OurDeletes b \n\t\tON a.ProductID = b.ProductID AND a.EffectiveStartDT = b.EffectiveStartDT;\n\t\t\n\t\tSELECT *\n\t\tFROM ProductPrices\n\t\tWHERE ProductID IN (1, 2);\n\t\t\n\t\tROLLBACK TRANSACTION T1;\n\t\t<\/pre>\n<p>The final result set in this case looks like this, where I can&#8217;t exactly highlight the rows that have been deleted (four of those) but I can highlight in green the ones where the effective end date was adjusted.<\/p>\n<pre>\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EffectiveEndDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1 &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;2013-06-04 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 16.65\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-09 00:00:00.000&#160; 2013-08-10 00:00:00.000&#160; 45.88\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-08-10 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160;&#160;34.98\n\t\t<\/pre>\n<p>Here the SSMS Messages Pane reports:<\/p>\n<pre>\t\tINSTEAD OF INSERT\/UPDATE\/DELETE TRIGGER\n\t\t\n\t\t(6 row(s) affected)\n\t\t<\/pre>\n<p>Which makes sense considering the deleted rows (4) plus the highlighted rows (2).<\/p>\n<p>Now none of this is worth a hoot if our TRIGGER doesn&#8217;t work for the case of a MERGE, where we want to insert, update and delete all at the same time.  So let&#8217;s give that a try by building a transaction table in our leading CTE.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tBEGIN TRANSACTION T1;\n\t\t\n\t\tWITH TransTable ([Action], ProductID, EffectiveStartDT, NewEffectiveStartDT, ProductPrice) AS\n\t\t(\n\t\t&#160;&#160;&#160; -- Insert a row for ProductID=1\n\t\t&#160;&#160;&#160; SELECT 'I', 1, '2013-06-09', NULL, 14.29\n\t\t&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160; -- Insert another row for ProductID=1 (at end)\n\t\t&#160;&#160;&#160; SELECT 'I', 1, '2014-06-01', NULL, 18.99\n\t\t&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160; -- Delete a row for ProductID=2 (in the middle)\n\t\t&#160;&#160;&#160; SELECT 'D', 2, '2013-08-09', NULL, NULL\n\t\t&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160; -- Delete the row for ProductID=3 \n\t\t&#160;&#160;&#160; SELECT 'D', 3, '2013-01-01', NULL, NULL\n\t\t&#160;&#160;&#160; UNION ALL\n\t\t&#160;&#160;&#160; -- Move a row for ProductID=2 \n\t\t&#160;&#160;&#160; SELECT 'U', 2, '2013-08-10', '2013-04-01', NULL\n\t\t)\n\t\tMERGE dbo.ProductPrices t\n\t\tUSING TransTable s\n\t\tON s.ProductID = t.ProductID AND s.EffectiveStartDT = t.EffectiveStartDT\n\t\tWHEN MATCHED AND s.[action] = 'U'\n\t\tTHEN UPDATE\n\t\tSET EffectiveStartDT = ISNULL(s.NewEffectiveStartDT, t.EffectiveStartDT)\n\t\t&#160;&#160;&#160; ,ProductPrice&#160;&#160;&#160; = ISNULL(s.ProductPrice, t.ProductPrice)\n\t\tWHEN MATCHED AND s.[action] = 'D'\n\t\tTHEN DELETE\n\t\tWHEN NOT MATCHED AND s.[action] = 'I'&#160;&#160; -- Should be an INSERT\n\t\tTHEN INSERT\n\t\t&#160;&#160;&#160;  (ProductID, EffectiveStartDT, ProductPrice)\n\t\tVALUES\n\t\t&#160;&#160;&#160;  (s.ProductID, s.EffectiveStartDT, s.ProductPrice);\n\t\t\n\t\tSELECT *\n\t\tFROM ProductPrices;\n\t\t\n\t\tROLLBACK TRANSACTION T1;\n\t\t<\/pre>\n<p>Let&#8217;s check to see if the following results are what we expect.\/p&gt;<\/p>\n<pre class=\"mark:3,7,8\">\t\tProductID&#160; EffectiveStartDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; EffectiveEndDT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ProductPrice\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-04 00:00:00.000&#160; 2013-06-09 00:00:00.000&#160; 15.25\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-09 00:00:00.000&#160; 2013-06-17 00:00:00.000&#160; 14.29\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-17 00:00:00.000&#160; 2013-06-25 00:00:00.000&#160; 16.33\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-06-25 00:00:00.000&#160; 2014-02-23 00:00:00.000&#160; 16.45\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-02-23 00:00:00.000&#160; 2014-06-01 00:00:00.000&#160; 16.65\n\t\t1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2014-06-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 18.99\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-04-01 00:00:00.000&#160; 2013-05-01 00:00:00.000&#160; 34.98\n\t\t2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 2013-05-01 00:00:00.000&#160; 2014-03-01 00:00:00.000&#160; 42.13\n\t\t2&#160;&#160;&#160; &#160;&#160;&#160;&#160;&#160;&#160;2014-03-01 00:00:00.000&#160; NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 45.18\n\t\t<\/pre>\n<p>For <code>ProductID<\/code><code>=1<\/code>, we inserted two rows which are highlighted in yellow (second and fifth).  This caused updates (self-maintenance) of the two preceding rows&#8217; effective end dates (highlighted in green).  For <code>ProductID<\/code><code>=2<\/code> we deleted a row (2013-08-09), which we obviously can&#8217;t highlight in yellow, but we can highlight in green the row where the effective end date was adjusted (2013-05-01) as a result.  Note that we also deleted the single row for <code>ProductID<\/code><code>=3<\/code>.  In this case there were no other rows where the end date needed adjustment.  Finally, for <code>ProductID<\/code><code>=2<\/code> we also moved a row (2013-08-10) to a new effective start date (2013-04-01) and this is highlighted in yellow.<\/p>\n<p>In case you&#8217;ve never worked with TRIGGERs in the case of a MERGE statement, they fire depending on what clauses are fired in the MERGE statement (we are talking here about our latest example, and not the MERGE statement in the TRIGGER).  Since our CTE contained transactions that caused each of the three clauses to fire (an UPDATE, an INSERT and a DELETE) we expect that our TRIGGER was fired three times.  The results in the SSMS Messages pane confirms this.<\/p>\n<pre>\t\tINSTEAD OF INSERT\/UPDATE\/DELETE TRIGGER\n\t\t\n\t\t(4 row(s) affected)\n\t\tINSTEAD OF INSERT\/UPDATE\/DELETE TRIGGER\n\t\t\n\t\t(3 row(s) affected)\n\t\tINSTEAD OF INSERT\/UPDATE\/DELETE TRIGGER\n\t\t\n\t\t(3 row(s) affected)\n\t\t<\/pre>\n<p>We&#8217;ll let the interested reader confirm how the row counts for each execution of the TRIGGER came about (hint: the order is INSERT, UPDATE and finally DELETE).  To dig a little deeper into the workings of the TRIGGER, you may want to introduce a couple of SELECTs at the beginning of the TRIGGER and then run the MERGE above again to see what&#8217;s going on.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tSELECT * FROM INSERTED;\n\t\tSELECT * FROM DELETED;\n\t\t\n\t<\/pre>\n<h2>Some Caveats for this Approach<\/h2>\n<p>In a table such as<code><\/code><code>ProductPrices<\/code>, there may be more than one time-sensitive column, although a relational model purist would probably argue this shouldn&#8217;t be.  In the TRIGGER, you&#8217;ll note that in several places there is a comment that indicates &#8220;time-sensitive attributes.&#8221;  In each case, this is a reminder that should you ALTER the table to include more time-sensitive attributes, those must be handled the same way that <code>ProductPrice<\/code> is handled in the various points within that TRIGGER.  In other words, you must change the TRIGGER to include the additional columns.<\/p>\n<p>If you are likely to use a system like this in a team, or if you aren&#8217;t blessed with a perfect memory, it is possible to set a reminder for anyone that modifies (ALTERs) this table.  This can be done through a DATABASE TRIGGER, such as the one that follows.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tCREATE TRIGGER WarnOnTableALTERs\n\t\tON DATABASE\n\t\tFOR ALTER_TABLE\n\t\tAS\n\t\t&#160;&#160;&#160; SET NOCOUNT ON;\n\t\t\n\t\t&#160;&#160;&#160; DECLARE @TablesOnWatch TABLE\n\t\t&#160;&#160;&#160;  (\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; Table_Name&#160;&#160;&#160;&#160;&#160; VARCHAR(100) NOT NULL\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,Table_ID&#160;&#160;&#160;&#160;&#160;&#160; BIGINT NOT NULL\n\t\t&#160;&#160;&#160; );\n\t\t\n\t\t&#160;&#160;&#160; INSERT INTO @TablesOnWatch SELECT 'ProductPrices', OBJECT_ID('ProductPrices', 'U');\n\t\t\n\t\t&#160;&#160;&#160; SELECT [Table Name]=name, Warning='Was ALTERed and has a TRIGGER on watch'\n\t\t&#160;&#160;&#160; FROM sys.all_objects a\n\t\t&#160;&#160;&#160; JOIN @TablesOnWatch b ON a.name = b.Table_Name\n\t\t&#160;&#160;&#160; WHERE a.[object_id] = \n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160; OBJECT_ID(EVENTDATA().value('(\/EVENT_INSTANCE\/ObjectName)[1]','NVARCHAR(MAX)')\n\t\t&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; , 'U')\n\t\tGO\n\t\t<\/pre>\n<p>So now, when anyone comes along and ALTERs this table, a warning message is displayed.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">\t\tBEGIN TRANSACTION T1;\n\t\t\n\t\tALTER TABLE dbo.ProductPrices\n\t\tADD NewTimeSensitiveAttribute INT NULL;\n\t\t\n\t\tROLLBACK TRANSACTION T1;\n\t\t\n\t\tTable Name&#160;&#160;&#160;&#160;&#160; Warning\n\t\tProductPrices&#160;&#160; Was ALTERed and has a TRIGGER on watch\n\t\t<\/pre>\n<p>Yes you can ROLLBACK an ALTER of a TABLE!<\/p>\n<p>The other caveat to this approach, which also affects Alex&#8217;s original (stored procedure) approach, has to do with issues reported about using MERGE.  See &#8220;<a href=\"http:\/\/www.mssqltips.com\/sqlservertip\/3074\/use-caution-with-sql-servers-merge-statement\/\">Use Caution with SQL Server&#8217;s MERGE Statement<\/a>&#8221; by 18 times nominated (as of this writing) <a href=\"http:\/\/mvp.microsoft.com\/en-us\/mvp\/Aaron%20Bertrand-8140\">SQL MVP<\/a> and avid <a href=\"http:\/\/sqlblog.com\/blogs\/aaron_bertrand\/\">blogger<\/a> Aaron Bertrand.  I cannot honestly say how many of these issues have been addressed in SQL 2012, but many of them may not be pertinent to your case.<\/p>\n<h2>Conclusions, Comparisons and Final Words<\/h2>\n<p>It is definitely possible to use constraints to enforce the essential business rules of a temporal table. The necessary updates to the data can be done by using SQL Server&#8217;s MERGE statement, but in several types of application design, this complexity is best hidden from the application.<\/p>\n<p>This article shows how this complexity can be hidden so that even where the application has full direct access to the table, simple CRUD operations can be used, but at the same time the table is still protected from bad data.<\/p>\n<p>I don&#8217;t believe I&#8217;ve built a better mousetrap here.  I have, rather, offered an alternative mousetrap, which may be applicable and\/or effective in some of your use cases.  Here is a summary of the advantages and disadvantages of the two approaches, as I see it.<\/p>\n<\/p>\n<table class=\"MsoTableGrid\">\n<tbody>\n<tr>\n<td valign=\"top\">\n<p><b>Category<\/b><\/p>\n<\/td>\n<td valign=\"top\">\n<p><b>Original Approach<\/b><\/p>\n<\/td>\n<td valign=\"top\">\n<p><b>Alternate Approach<\/b><\/p>\n<\/td>\n<td valign=\"top\">\n<p><b>Remark<\/b><\/p>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\">\n<p>Required SQL Server Version<\/p>\n<\/td>\n<td valign=\"top\">\n<p>2008<\/p>\n<\/td>\n<td valign=\"top\">\n<p>2012<\/p>\n<\/td>\n<td valign=\"top\">\n<p>Original approach is limited by the MERGE,  \t\t\talthough Alex does offer an alternative that works in SQL 2005.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\">\n<p>Additional Storage<\/p>\n<\/td>\n<td valign=\"top\">\n<p>One Column for Previous End Date<\/p>\n<\/td>\n<td valign=\"top\">\n<p>None<\/p>\n<\/td>\n<td valign=\"top\">\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\">\n<p>Implementation<\/p>\n<\/td>\n<td valign=\"top\">\n<p>Stored Procedure<\/p>\n<\/td>\n<td valign=\"top\">\n<p>TRIGGER<\/p>\n<\/td>\n<td valign=\"top\">\n<p>It might be possible to implement the  \t\t\toriginal approach in a TRIGGER.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\">\n<p>Specifying Values on DML operations<\/p>\n<\/td>\n<td valign=\"top\">\n<p>Must specify Effective End Date and Previous  \t\t\tEffective End Date<\/p>\n<\/td>\n<td valign=\"top\">\n<p>Need not specify Effective End Date<\/p>\n<\/td>\n<td valign=\"top\">\n<p>The alternate approach allows the user to  \t\t\tissue normal <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ff848766.aspx\"> \t\t\tData Manipulation Language<\/a>  \t\t\t(DML) INSERTs, UPDATEs, DELETEs and MERGEs to modify data in the table without specifying the effective end  \t\t\tdate.<\/p>\n<p>It may be possible to implement this same  \t\t\teffect in the original approach if done with a MERGE in a TRIGGER, as done in the alternate approach.  \t\t\t<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td valign=\"top\">\n<p>CONSTRAINTs<\/p>\n<\/td>\n<td valign=\"top\">\n<p>All data integrity checks are managed  \t\t\tthrough CONSTRAINTs<\/p>\n<\/td>\n<td valign=\"top\">\n<p>All data integrity checks are managed  \t\t\tthrough CONSTRAINTs<\/p>\n<\/td>\n<td valign=\"top\">\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>With the alternative approach, you don&#8217;t need to specify any adjusted effective end dates when you perform normal DML actions. No additional storage is taken up by the additional column for the previous end date that Alex proposed.<\/p>\n<p>It would also be possible to replace the LEAD analytical function in the TRIGGER with an OUTER APPLY\/SELECT TOP 1\/ORDER BY <code>EffectiveStartDT<\/code> construct (which effectively simulates a LEAD), making for a fully SQL 2008 compatible solution.  However we will refrain from showing that, leaving it to our interested readers, because it is likely its performance would not be as good as using LEAD.<\/p>\n<p>One point not described in the comparison above is how relatively easy it would be to implement this on top of your existing temporal tables.  Simply create the constraints and the trigger on top of your table having effective start\/end dates, and assuming there are no constraint failures (data anomalies) when you create them, your basic CRUD operations should pretty much work without code changes.<\/p>\n<p>Thanks for listening folks!  I hope you find this approach useful and tell me about your success stories.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>&#8216;Temporal&#8217; tables contain facts that are valid for a period of time.  When they are used for financial information they have to be very well constrained to prevent errors getting in and  causing incorrect reporting. This makes them more difficult to maintain. Is it possible to have both the stringent constraints and simple CRUD operations? Well, yes. Dwain Camps patiently explains the whole process.&hellip;<\/p>\n","protected":false},"author":221942,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[4150,4252],"coauthors":[],"class_list":["post-1965","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-sql","tag-t-sql-programming"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1965","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221942"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=1965"}],"version-history":[{"count":5,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1965\/revisions"}],"predecessor-version":[{"id":41135,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1965\/revisions\/41135"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1965"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1965"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1965"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1965"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}