{"id":502,"date":"2009-01-18T00:00:00","date_gmt":"2009-01-18T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/temporal-data-techniques-in-sql\/"},"modified":"2021-09-29T16:22:08","modified_gmt":"2021-09-29T16:22:08","slug":"temporal-data-techniques-in-sql","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/temporal-data-techniques-in-sql\/","title":{"rendered":"Temporal Data Techniques in SQL"},"content":{"rendered":"<h2>Holidays<\/h2>\n<p class=\"start\">Nobody agrees on holidays. There are the obvious international differences &#8211; the Fourth of July doesn&#8217;t have the same appeal to the UK that it does to the US, and most Americans have no idea what <a href=\"http:\/\/en.wikipedia.org\/wiki\/Bank_holiday\">Bank holidays<\/a> are, and why the banks deserve them. This can be a real problem in certain industries.\u00a0The ocean freight and maritime industries publish a list of all the non-working holidays for every seaport on Earth. Air freight is not so bothered by this, but strong dock worker unions mean that if you show up in a port on the wrong day, you can pay extra docking fees.\u00a0<\/p>\n<p>Holidays fall into two types. Some can be computed from a rule and some are simply declared by an authority. The declared holidays are usually civic ones and they might change.\u00a0 The computed ones that involve lunar and\/or solar cycles are mostly religious. You also have holidays that appear only once, such as a &#8220;National Day of Mourning&#8221; for the death of an important political figure.\u00a0<\/p>\n<p>But even the familiar holidays are harder to compute than you think.\u00a0 Every programmer is aware that <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/calculating-easter-the-longest-scientific-project-ever\/\">Easter has a pretty elaborate formula<\/a>. But nobody asks &#8220;Which Easter?&#8221; we are to compute.\u00a0 Orthodox and Catholic (which is used by Protestants) dates do not agree.\u00a0 For example, in 2008 the Catholic Easter was on March 10 and the Greek Orthodox Easter on April 14; but by 2010 they will both be on March 22.\u00a0 <em>You can get a <a href=\"http:\/\/5ko.free.fr\/en\/easter.php\">side by side comparison at Side-by-side Easter calendar reference for the 21st century<\/a>.\u00a0 <\/em><\/p>\n<p>Some holidays are longer than others. The obvious example is the Weekends.\u00a0 That usually means Saturdays and Sundays in Western countries.\u00a0 In the Islamic world, it means Fridays.\u00a0 And probably in my lifetime, we will probably see the four day work week in some countries.\u00a0 Then there is <a href=\"http:\/\/en.wikipedia.org\/wiki\/Hanukkah\">Hanukkah<\/a> and <a href=\"http:\/\/en.wikipedia.org\/wiki\/Eid_ul-Fitr\">Eid ul-Fitr<\/a>, which last for several days.\u00a0<\/p>\n<p>In short, holidays are just too complicated and arbitrary to put in a function, VIEW or a query.\u00a0 I wish that there was even an ISO Standard for just classifying types of holidays.\u00a0 What this means is that you cannot help but use a Calendar table for holidays and you have to do most of the work for yourself.\u00a0 We will get to that in another section.\u00a0<\/p>\n<h2>Leap Seconds and Trivia\u00a0\u00a0<\/h2>\n<p class=\"start\">Be grateful that most commercial work is done at the level of days and perhaps time of day, and not to the accuracy of decimal fractions of seconds. That leads to the choice of atomic versus the astronomical clocks, if you have to co-ordinate events at the nanosecond level.\u00a0 Scientific work has to cope with leap-seconds.\u00a0 These are positive or negative corrections defined by the International Earth Rotation and Reference System Service to keep the atomic clocks within 0.9 seconds of the planet&#8217;s actual rotation.\u00a0 They announced on 2008 July 4 that the world&#8217;s atomic clocks will be adding another second at the end of this year.\u00a0 I hope this does not spoil your New Years Eve plans.\u00a0<\/p>\n<h2><b>Calendar Tables <\/b><\/h2>\n<p class=\"start\">The simplest Calendar is just a single column with the dates for a particular range.\u00a0 Since it is all key, we know that It is as normalized as it can get.\u00a0 Since there are 365.2422 days per year, a 100 year calendar is pretty small and would fit into main storage in most modern machines.\u00a0 It is also of pretty limited use without other data that we want to look up.\u00a0<\/p>\n<p>Since we already talked about holidays, the first impulse is to add a second column with a flag for &#8220;work day&#8221; or &#8220;holiday&#8221; to the table, something like this skeleton.\u00a0<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">CREATE TABLE Calendar\r\n(cal_date DATE NOT NULL PRIMARY KEY,\r\n\u00a0business_day_flg INTEGER DEFAULT 1 NOT NULL\r\n\u00a0 CHECK (business_day_flg IN (0,1)), \r\n\u00a0...); \r\n<\/pre>\n<p>The most common calculations you need are the number of business days between two dates or the nearest business day to a given single date.\u00a0 You can do this with the integer flag and earlier SQLs with a skeleton like this:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">SELECT SUM(C.business_day_flg) AS business_day_cnt\r\n\u00a0 FROM Calendar AS C, Foobar AS F\r\n\u00a0WHERE C.cal_date BETWEEN F.foo_start_date AND F.foo_end_date\r\n\u00a0\u00a0 AND ..;\r\n<\/pre>\n<p>This interferes with the ability to get other data out of Foobar without adding <code>GROUP BY<\/code> clauses.\u00a0 Today you can use the windowed aggregates functions &#8211; that is the new ones with an <code>OVER() <\/code>clause.\u00a0<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">SELECT F.*, -- use real column names in production code\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 SUM(C.business_day_flg) \r\n\u00a0 \u00a0\u00a0\u00a0\u00a0\u00a0OVER (ORDER BY C.cal_date) AS business_day_cnt\r\n\u00a0 FROM Calendar AS C, Foobar AS F\r\n\u00a0WHERE C.cal_date BETWEEN F.foo_start_date AND F.foo_end_date\r\n\u00a0\u00a0 AND ..;\r\n<\/pre>\n<p>This is still not as good as we can make it.\u00a0 Let me take time out to discuss the Julian date, which is often misused.\u00a0 Julian date is technically a count of days used by astronomers for observations (http:\/\/en.wikipedia.org\/wiki\/Julian_date).\u00a0 It is a large integer number and has some rules for computing it.\u00a0 Only astronomers care about it, and the rest of us get it confused with a Julian-IZED date,<\/p>\n<p>The Julianized date is a count of days from a base day, used in business.\u00a0 You see it as the &#8220;day within year&#8221; number on the bottom of the pages of a desk calendar.\u00a0 If your time frame is a calendar year, it will be between 1 and 365 or 366 days.\u00a0 It is called the ordinal day format (&#8220;yyyy-ddd&#8221;) in ISO-8601.\u00a0 But you can Julianize any time frame you wish such as the entire range covered by your Calendar table.\u00a0 Instead of a flag, you simply repeat the Julianized number for holidays and use integer math to get the counts.\u00a0 Here is a skeleton.\u00a0<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">CREATE TABLE Calendar\r\n(cal_date DATE NOT NULL PRIMARY KEY,\r\n\u00a0julian_business_nbr INTEGER NOT NULL,\r\n\u00a0...);\r\n\u00a0\r\nINSERT INTO Calendar VALUES ('2007-04-05', 42);\r\nINSERT INTO Calendar VALUES ('2007-04-06', 43); -- Good Friday\r\nINSERT INTO Calendar VALUES ('2007-04-07', 43);\r\nINSERT INTO Calendar VALUES ('2007-04-08', 43); -- Easter Sunday\r\nINSERT INTO Calendar VALUES ('2007-04-09', 44);\r\nINSERT INTO Calendar VALUES ('2007-04-10', 45); -- Tuesday, back to work \r\n<\/pre>\n<p>To compute the business days from &#8216;2007-04-05&#8217;, Thursday before Easter in 2007 to &#8216;2007-04-10&#8217;, the next Tuesday:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">SELECT (C2.julian_business_nbr - C1.julian_business_nbr -1) AS business_day_cnt \r\n\u00a0 FROM Calendar AS C1, Calendar AS C2\r\n\u00a0WHERE C1.cal_date = '2007-04-05'\r\n\u00a0\u00a0 AND C2.cal_date = '2007-04-10';\r\n<\/pre>\n<p>This saves having to scan all of the rows within the range to get a sum.\u00a0 The -1 is to avoid counting the current day as one of the business days, but you might have a different rule in your company. \u00a0<\/p>\n<h2><b>Report Range Tables <\/b><\/h2>\n<p>A common application is have special reporting periods, perhaps based on the fiscal calendar or business defined events (&#8220;Annual Going out Of Business Sale!&#8221; was one of my favorites).\u00a0<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">CREATE TABLE ReportRanges\r\n(report_name VARCHAR(30) NOT NULL PRIMARY KEY,\r\n\u00a0report_start_date DATE DEFAULT CURRENT_TIMESTAMP NOT NULL, \r\n\u00a0report_end_date DATE NOT NULL,\r\n\u00a0CONSTRAINT date_ordering \r\n\u00a0 CHECK (report_start_date &lt;= report_end_date), \r\n\u00a0etc);\r\n<\/pre>\n<p>These reports can overlap; a fiscal quarter will be contained in the range of its fiscal year.\u00a0 There can be gaps between them; we have to wait a year between each &#8220;Annual Going out Of Business Sale!&#8221; and there might be long stretches of time without any special sales.\u00a0 But we want to know their ranges so that the table is fairly constant once it is created.\u00a0<\/p>\n<p>The basic skeleton for use with these tables is<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">SELECT R.report_name, &lt;&lt; summary computations &gt;&gt; \r\n\u00a0 FROM ReportRanges AS R, [Events] AS E\r\n\u00a0WHERE E.event_date BETWEEN R.report_start_date AND report_end_date \r\n\u00a0\u00a0 AND R.report_name IN (&lt;&lt;report name list&gt;&gt;)\r\n\u00a0GROUP BY R.report_name; \r\n<\/pre>\n<p>The &lt;&lt;report name list&gt;&gt; would probably be events nested inside each other, like fiscal quarters inside a fiscal year.\u00a0 While this is a useful and portable programming trick, you need to consider replacing it with the newer OLAP extensions to the <code>GROUP BY <\/code>clause such as <code>ROLLUP<\/code> and <code>CUBE.\u00a0<\/code><\/p>\n<h2><b>History Tables <\/b><\/h2>\n<p class=\"start\">Another form of calendar table records the history of events.\u00a0 You will see history tables used in Data Warehouses and queries dealing with things that have duration. If you go to the French Quarter in New Orleans, there is a lamp post with a historical plaque on it which gives the years of the Spanish domination, French domination, American domination, Confederate domination and finally, the second American domination.\u00a0 The last entry has a blank space to be filled in later and empty room on the plaque beneath that line.\u00a0<\/p>\n<p>Now look at the <code>NULL<\/code>-able <b>end_date<\/b> in the skeleton that follows.\u00a0 It is useful because SQL does not have a token for &#8220;Eternity&#8221; to show that something is not yet finished like New Orleans.\u00a0 Using a dummy date in the distant future for &#8220;Eternity&#8221; has portability problems.\u00a0 And it can mess up temporal math if you don&#8217;t catch it in <code>CASE<\/code> expressions.\u00a0 There should be, at most, one <code>NULL<\/code> for each item in this inventory history to model the current price.\u00a0 It is also possible that we know a future ending data for an item &#8211; look at the expiration dates on food.\u00a0\u00a0<\/p>\n<p>To make this easier to see, let me use a history table for price changes.\u00a0 So a basic history table looks like this skeleton:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">CREATE TABLE PriceHistory\r\n(ean CHAR(13) NOT NULL\u00a0\u00a0 -- industry standard barcode \r\n\u00a0 REFERENCES Inventory(ean),\r\n\u00a0price_prev_date DATE NOT NULL, \r\n\u00a0price_start_date DATE DEFAULT CURRENT_TIMESTAMP NOT NULL, \r\n\u00a0price_end_date DATE, -- NULL means current price \r\n\u00a0item_price DECIMAL (12,4) NOT NULL\r\n\u00a0 CHECK (item_price &gt; 0.0000), \r\n\u00a0etc.\r\n\u00a0 -- now we add constraints \r\n\u00a0\r\n--uniqueness \r\nPRIMARY KEY (ean, price_start_date),\r\nUNIQUE (ean, price_end_date), \r\nUNIQUE (ean, prev_end_date),\r\n\u00a0\r\n--ordering of events \r\nCHECK (price_start_date &lt;= price_end_date),\r\n-- Optionally, CHECK (price_start_date &lt; price_end_date\r\nCHECK (prev_end_date &lt;= price_start_date),\r\n-- Optionally, CHECK (price_start_date = price_prev_date + INTERVAL '1' DAY), -- prevents gaps\r\n\u00a0\r\n-- self-referencing constraint \r\nCONSTRAINT ean_prev_end_date\r\n\u00a0FOREIGN KEY (ean, prev_end_date)\r\n\u00a0REFERENCES PriceHistory (ean, price_end_date)\r\n\u00a0);\r\n<\/pre>\n<p>The first three uniqueness constraints are fairly obvious.\u00a0 The<code> EAN<\/code> and the start of a price have to be unique and not <code>NULL<\/code>-able so we have a natural primary key.\u00a0 The ending dates, current and previous, might have <code>NULL<\/code>s, so we need to use a <code>UNIQUE<\/code> constraint.\u00a0<\/p>\n<p>The next constraints give an ordering to each event, namely, <code>price_prev_date<\/code> is on or before <b><code>price_start_date<\/code><\/b>, which is on or before <b><code>price_end_date<\/code><\/b>.\u00a0 The reason for not putting this into a single <code>BETWEEN<\/code> predicate is that each constraint will have a name in production code that will show up in error messages so we want to be exact.\u00a0<\/p>\n<p>The self-referencing constraint is a trick from <span class=\"fontsize2author\">Alex Kuznetsov.\u00a0 It says that all the previous price ending dates were really ending dates for some time period.\u00a0 You will want to play with options to get them to fit your own business rules.\u00a0 <\/span><\/p>\n<p>It is also a good idea to have a VIEW with the current data: \u00a0<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">CREATE VIEW CurrentPrices (..)\r\nAS\r\nSELECT ..\r\n\u00a0 FROM PriceHistory\r\n\u00a0WHERE price_end_date IS NULL\r\n\u00a0\u00a0\u00a0 OR price_end_date &gt;= CURRENT_TIMESTAMP;\r\n\u00a0\r\n<\/pre>\n<p>You use a <code>BETWEEN<\/code> predicate to get the appropriate price for a given order on a particular date.\u00a0<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">SELECT ..\r\n\u00a0 FROM PriceHistory AS H, Orders AS O\r\n\u00a0WHERE O.sales_date BETWEEN H.price_start_date\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 AND COALESCE (price_end_date, CURRENT_TIMESTAMP); -- or other known data as needed\r\n\u00a0\r\n<\/pre>\n<p>You can also enforce the &#8220;one <code>NULL<\/code> per item&#8221; with a trigger.\u00a0 If your SQL has <code>CREATE ASSERTION<\/code> or a full implementation of the <code>CHECK()<\/code> constraints in SQL-92, you can do it with declarative code.\u00a0 But there is a sneaky trick that uses the <code>WITH CHECK OPTION <\/code>on a<code> VIEW<\/code> to fake it; here is the skeleton:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">CREATE VIEW Prices (..)\r\nAS (\r\nSELECT ...\r\n\u00a0 FROM PriceHistory AS P1 \r\n\u00a0WHERE NOT EXISTS\r\n\u00a0\u00a0\u00a0\u00a0\u00a0 (SELECT *\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 FROM PriceHistory AS P2 \r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 GROUP BY ean\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 HAVING COUNT(*) - COUNT(price_end_date) &gt; 1)\r\n\u00a0\u00a0\u00a0 )\r\nWITH CHECK OPTION\r\n<\/pre>\n<p>This forces you to update through the <b>VIEW<\/b> so that the <b>WITH CHECK OPTION<\/b> is always used. However, if you go directly to the base table, it can be circumvented.\u00a0 Now your problem is to write a stored procedure that will update the table and insert a new row.\u00a0 You can do this with a single <b>MERGE <\/b>statement, or with a short block of procedural code:<\/p>\n<pre class=\"theme:ssms2012 lang:tsql highlight:0 decode:true\">CREATE PROCEDURE UpdateItemPrice\r\n@in_ean CHAR(13), @new_item_price DECIMAL (12,4)\r\nAS\r\n-- LANGUAGE SQL\r\nBEGIN ATOMIC\r\nUPDATE PriceHistory\r\n\u00a0\u00a0 SET price_end_date = CURRENT_TIMESTAMP\r\n\u00a0WHERE ean = @in_ean;\r\nINSERT INTO PriceHistory (ean, price_prev_date, price_start_date, price_end_date, item_price)\r\nVALUES (@in_ean, CURRENT_TIMESTAMP, DATEADD(DAY,1,CURRENT_TIMESTAMP), NULL, @new_item_price);\r\nEND\r\n<\/pre>\n<p>This will make the price change go into effect tomorrow.<\/p>\n<p class=\"note\">In his next article, Joe gets distracted from temporal data by reading the Manga Guide to Databases, and decides, instead, to write about relational division.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In the first part of this series on Temporal Data, Joe explained how it is that the Common Era calendar is irregular and mentioned that, although there are ANSI\/ISO Standards for temporal operations in SQL, every vendor has something different.  Now, he discusses other factors to take into account when using temporal data such as Holidays, and discusses a few techniques using Calendar, Report Usage and History tables&hellip;<\/p>\n","protected":false},"author":96214,"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":[6781],"class_list":["post-502","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\/502","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\/96214"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=502"}],"version-history":[{"count":5,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/502\/revisions"}],"predecessor-version":[{"id":73057,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/502\/revisions\/73057"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=502"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=502"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=502"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=502"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}