{"id":315,"date":"2007-10-11T00:00:00","date_gmt":"2007-10-11T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/find-missing-date-ranges-in-sql\/"},"modified":"2021-09-29T16:22:15","modified_gmt":"2021-09-29T16:22:15","slug":"find-missing-date-ranges-in-sql","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/find-missing-date-ranges-in-sql\/","title":{"rendered":"Find Missing Date Ranges in SQL"},"content":{"rendered":"<div id=\"pretty\">\n<h3>The Task Defined. <\/h3>\n<p class=\"start\">Imagine this: The customer runs the application that you&#8217;ve designed, and cannot see any sign of business transactions over a time period where he&#8217;d expect to see them. Your manager asks you to investigate. He will almost certainly ask you to find all of the missing days or hours in a date range where no data has been recorded: The date range can be several years.<\/p>\n<p>The situation may become even worse; missing data can have unpleasant consequences, and that fact will make both the customer and the boss very nervous.<\/p>\n<p>As a result, you will be under stress: you have to reassure your boss and the customer, and you need to do that in a very limited amount of time.<\/p>\n<p>In this article, we will discuss a few SQL, one-query techniques that can help you to find the missing dates.<\/p>\n<h3>Preparations <\/h3>\n<p>Firstly, you will need to create and load an auxiliary table that will store the sequence of whole numbers. The volume of numbers in that table will be equal to the number of days in the year 2007 (see Listing1):<\/p>\n<p> <strong>Listing 1<\/strong>&#8211; Create and load an auxiliary tableSET&#160;NOCOUNT&#160;ON; <br \/>DECLARE&#160;@startDate&#160;smalldatetime,&#160;@endDate&#160;smalldatetime <br \/>SELECT&#160;@startDate&#160;=&#160;&#8217;Jan&#160;01,&#160;2007&#8242;,&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;@endDate&#160;=&#160;DATEADD(yy,&#160;1,&#160;@startDate); <\/p>\n<p>IF&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sysobjects&#160; <br \/>&#160;&#160;&#160;WHERE&#160;ID&#160;=&#160;(OBJECT_ID(&#8216;sequence&#8217;))&#160;AND&#160;xtype&#160;=&#160;&#8217;U&#8217;)&#160; <br \/>DROP&#160;TABLE&#160;sequence; <br \/>CREATE&#160;TABLE&#160;sequence(num&#160;INT&#160;NOT&#160;NULL&#160;PRIMARY&#160;KEY); <\/p>\n<p>WITH&#160;numbers&#160;AS <br \/>( <br \/>&#160;SELECT&#160;1&#160;AS&#160;num <br \/>&#160;UNION&#160;ALL <br \/>&#160;SELECT&#160;num&#160;+&#160;1&#160;FROM&#160;numbers&#160; <br \/>&#160;&#160;&#160;&#160;&#160;WHERE&#160;num&#160;&lt;&#160;(SELECT&#160;DATEDIFF(dd,&#160;@startDate,&#160;@endDate)) <br \/>) <br \/>INSERT&#160;INTO&#160;sequence <br \/>SELECT&#160;num&#160;FROM&#160;numbers&#160; <br \/>OPTION&#160;(MAXRECURSION&#160;0); <\/p>\n<p>Secondly, generate some test data and load that data into a sample table. Then produce the gaps, using the next algorithm: delete dates, where the number of days since Jan 01, 1900, divided by 4 or 5 or 6 has zero in remainder. <\/p>\n<p>The second preparation step can be done this way (see Listing2):<\/p>\n<p> <strong>Listing2.<\/strong> Create and load a sample table<code>DECLARE&#160;&#160;&#160;&#160;@iniDate&#160;datetime; <br \/>SELECT&#160;@iniDate&#160;=&#160;'Dec&#160;31,&#160;2006'; <br \/>IF&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sysobjects&#160; <br \/>&#160;&#160;&#160;WHERE&#160;ID&#160;=&#160;(OBJECT_ID('sampleDates'))&#160;AND&#160;xtype&#160;=&#160;'U')&#160; <br \/>DROP&#160;TABLE&#160;sampleDates; <br \/>CREATE&#160;TABLE&#160;sampleDates(theDate&#160;smalldatetime&#160;PRIMARY&#160;KEY) <br \/>INSERT&#160;INTO&#160;sampleDates&#160; <br \/>SELECT&#160;DATEADD(dd,&#160;num,&#160;@iniDate)&#160;theDate <br \/>&#160;&#160;&#160;FROM&#160;sequence; <\/p>\n<p>--&#160;Create&#160;gaps <br \/>DELETE&#160;sampleDates&#160; <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(dd,&#160;0,&#160;theDate)%3&#160;=&#160;0 <br \/>&#160;&#160;&#160;OR&#160;DATEDIFF(dd,&#160;0,&#160;theDate)%4&#160;=&#160;0 <br \/>&#160;&#160;&#160;OR&#160;DATEDIFF(dd,&#160;0,&#160;theDate)%5&#160;=&#160;0; <br \/>SELECT&#160;*&#160;FROM&#160;sampleDates; <\/p>\n<p>Results:<\/p>\n<p>theDate<br \/>-----------------------<br \/>2007-01-02 00:00:00.000<br \/>2007-01-03 00:00:00.000<br \/>2007-01-06 00:00:00.000<br \/>2007-01-09 00:00:00.000<br \/>. . . . . . . . . . . .<br \/>2007-12-20 00:00:00.000<br \/>2007-12-23 00:00:00.000<br \/>2007-12-25 00:00:00.000<br \/>2007-12-28 00:00:00.000<br \/>2007-12-29 00:00:00.000<\/p>\n<p>(146 row(s) affected)<br \/><\/code><\/p>\n<p>Now that we have the auxiliary table and the sample data, we can try out a few techniques. <\/p>\n<h3>Technique #1 -The simple list <\/h3>\n<p>The first technique is quite common and uses an outer join (see Listing3):<\/p>\n<p> <strong>Listing3.<\/strong> Common solution&#8211;&#160;The&#160;solution,&#160;where&#160;missing&#160;days&#160;are&#160;represented&#160;by&#160;NULLs <br \/>SELECT&#160;theDate <br \/>&#160;&#160;&#160;FROM&#160;sequence&#160;t1&#160;LEFT&#160;OUTER&#160;JOIN&#160;sampleDates&#160;t2&#160; <br \/>&#160;&#160;&#160;ON&#160;DATEADD(dd,&#160;t1.num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;)&#160;=&#160;t2.theDate; <\/p>\n<p>Results:<\/p>\n<p>theDate<br \/>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br \/>NULL<br \/>2007-01-02 00:00:00.000<br \/>2007-01-03 00:00:00.000<br \/>NULL<br \/>NULL<br \/>2007-01-06 00:00:00.000<br \/>NULL<br \/>NULL<br \/>2007-01-09 00:00:00.000<br \/>. . . . . . . . . . . .<\/p>\n<p>2007-12-20 00:00:00.000<br \/>NULL<br \/>NULL<br \/>2007-12-23 00:00:00.000<br \/>NULL<br \/>2007-12-25 00:00:00.000<br \/>NULL<br \/>NULL<br \/>2007-12-28 00:00:00.000<br \/>2007-12-29 00:00:00.000<br \/>NULL<br \/>NULL<\/p>\n<p>(365 row(s) affected)<\/p>\n<p>&#8211;&#160;The&#160;solution&#160;that&#160;shows&#160;only&#160;missing&#160;days <br \/>SELECT&#160;DATEADD(dd,&#160;num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;)&#160;missingDate <br \/>&#160;&#160;&#160;FROM&#160;sequence&#160;t1&#160;LEFT&#160;OUTER&#160;JOIN&#160;sampleDates&#160;t2&#160; <br \/>&#160;&#160;&#160;ON&#160;DATEADD(dd,&#160;t1.num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;)&#160;=&#160;t2.theDate <br \/>&#160;&#160;&#160;WHERE&#160;t2.theDate&#160;IS&#160;NULL; <\/p>\n<p>Results:<\/p>\n<p>missingDate<br \/>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br \/>2007-01-01 00:00:00.000<br \/>2007-01-04 00:00:00.000<br \/>2007-01-05 00:00:00.000<br \/>2007-01-07 00:00:00.000<br \/>2007-01-08 00:00:00.000<br \/>2007-01-10 00:00:00.000<br \/>. . . . . . . . . . . . <br \/>2007-12-21 00:00:00.000<br \/>2007-12-22 00:00:00.000<br \/>2007-12-24 00:00:00.000<br \/>2007-12-26 00:00:00.000<br \/>2007-12-27 00:00:00.000<br \/>2007-12-30 00:00:00.000<br \/>2007-12-31 00:00:00.000<\/p>\n<p>(219 row(s) affected)<\/p>\n<p>There is nothing wrong with the solutions shown in the Listing3. However, if you need to find the missing hours in the whole year or the missing days in a few years, these queries may produce a long list of dates, which would be soon become unwieldy and difficult to interpret. <\/p>\n<p>Your manager would not appreciate being given this sort of report.<\/p>\n<p>Therefore, you need to find another solution, which will represent the result in a way that is more convenient for reading and analyzing.<\/p>\n<h3>Technique #2 -finding the ranges<\/h3>\n<p class=\"start\">Think of the missing dates as being a number of gaps in the continuous sequence of the dates. Then, instead of generating each missing date, you can show just the beginning and the end of each gap. The code in the Listing4 demonstrates two variants of such a solution:<\/p>\n<p> <strong>Listing 4.<\/strong> The &#8216;gaps&#8217; solution&#8211;&#160;First&#160;variant&#160;without&#160;SQL&#160;Server&#160;date\/time&#160;function <br \/>SELECT&#160;t1.col1&#160;AS&#160;startOfGap,&#160;MIN(t2.col1)&#160;AS&#160;endOfGap&#160; <br \/>&#160;&#160;&#160;FROM&#160; <br \/>&#160;&#160;&#160;(SELECT&#160;col1&#160;=&#160;theDate&#160;+&#160;1&#160;&#160;FROM&#160;sampleDates&#160;tbl1&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;NOT&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sampleDates&#160;tbl2&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;tbl2.theDate&#160;=&#160;tbl1.theDate&#160;+&#160;1) <br \/>&#160;&#160;&#160;&#160;&#160;&#160;AND&#160;theDate&#160;&lt;&gt;&#160;(SELECT&#160;MAX(theDate)&#160;FROM&#160;sampleDates))&#160;t1 <br \/>&#160;&#160;&#160;INNER&#160;JOIN&#160; <br \/>&#160;&#160;&#160;(SELECT&#160;col1&#160;=&#160;theDate&#160;-&#160;1&#160;&#160;FROM&#160;sampleDates&#160;tbl1&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;NOT&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sampleDates&#160;tbl2&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;tbl1.theDate&#160;=&#160;tbl2.theDate&#160;+&#160;1) <br \/>&#160;&#160;&#160;&#160;&#160;&#160;AND&#160;theDate&#160;&lt;&gt;&#160;(SELECT&#160;MIN(theDate)&#160;FROM&#160;sampleDates))&#160;t2&#160; <br \/>&#160;&#160;&#160;ON&#160;t1.col1&#160;&lt;=&#160;t2.col1 <br \/>&#160;&#160;&#160;GROUP&#160;BY&#160;t1.col1; <\/p>\n<p>Results: <\/p>\n<p>startOfGap endOfGap <br \/>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211; &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211; <br \/>2007-01-04 00:00:00.000 2007-01-05 00:00:00.000 <br \/>2007-01-07 00:00:00.000 2007-01-08 00:00:00.000 <br \/>2007-01-10 00:00:00.000 2007-01-10 00:00:00.000 <br \/>2007-01-12 00:00:00.000 2007-01-13 00:00:00.000 <br \/>. . . . . . . . . . . . . . . . . . . . . . . . <br \/>2007-12-18 00:00:00.000 2007-12-18 00:00:00.000 <br \/>2007-12-21 00:00:00.000 2007-12-22 00:00:00.000 <br \/>2007-12-24 00:00:00.000 2007-12-24 00:00:00.000 <br \/>2007-12-26 00:00:00.000 2007-12-27 00:00:00.000 <\/p>\n<p>(108 row(s) affected) <\/p>\n<p>&#8211;&#160;Second&#160;variant&#160;with&#160;SQL&#160;Server&#160;date\/time&#160;functions <br \/>SELECT&#160;t1.col1&#160;AS&#160;startOfGap,&#160;MIN(t2.col1)&#160;AS&#160;endOfGap&#160; <br \/>&#160;&#160;&#160;FROM&#160; <br \/>&#160;&#160;&#160;(SELECT&#160;col1&#160;=&#160;DATEADD(dd,&#160;1,&#160;theDate)&#160;FROM&#160;sampleDates&#160;tbl1&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;NOT&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sampleDates&#160;tbl2&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;DATEDIFF(dd,&#160;tbl1.theDate,&#160;tbl2.theDate)&#160;=&#160;1) <br \/>&#160;&#160;&#160;&#160;&#160;AND&#160;theDate&#160;&lt;&gt;&#160;(SELECT&#160;MAX(theDate)&#160;FROM&#160;sampleDates))&#160;t1 <br \/>&#160;&#160;&#160;INNER&#160;JOIN&#160; <br \/>&#160;&#160;&#160;(SELECT&#160;col1&#160;=&#160;DATEADD(dd,&#160;-1,&#160;theDate)&#160;FROM&#160;sampleDates&#160;tbl1&#160; <br \/>&#160;&#160;&#160;&#160;&#160;WHERE&#160;NOT&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sampleDates&#160;tbl2&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;DATEDIFF(dd,&#160;tbl2.theDate,&#160;tbl1.theDate)&#160;=&#160;1) <br \/>&#160;&#160;&#160;&#160;&#160;AND&#160;theDate&#160;&lt;&gt;&#160;(SELECT&#160;MIN(theDate)&#160;FROM&#160;sampleDates))&#160;t2&#160; <br \/>&#160;&#160;&#160;&#160;ON&#160;t1.col1&#160;&lt;=&#160;t2.col1 <br \/>&#160;&#160;&#160;&#160;GROUP&#160;BY&#160;t1.col1 <br \/>&#160;&#160;&#160;&#160;ORDER&#160;BY&#160;t1.col1; <\/p>\n<p>Results: <\/p>\n<p>startOfGap endOfGap <br \/>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211; &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211; <br \/>2007-01-04 00:00:00.000 2007-01-05 00:00:00.000 <br \/>2007-01-07 00:00:00.000 2007-01-08 00:00:00.000 <br \/>2007-01-10 00:00:00.000 2007-01-10 00:00:00.000 <br \/>2007-01-12 00:00:00.000 2007-01-13 00:00:00.000 <br \/>. . . . . . . . . . . . . . . . . . . . . . . . <br \/>2007-12-18 00:00:00.000 2007-12-18 00:00:00.000 <br \/>2007-12-21 00:00:00.000 2007-12-22 00:00:00.000 <br \/>2007-12-24 00:00:00.000 2007-12-24 00:00:00.000 <br \/>2007-12-26 00:00:00.000 2007-12-27 00:00:00.000 <\/p>\n<p>(108 row(s) affected) <\/p>\n<p>Both queries from the Listing 4 use self-join and produce the same result in approximately the same time. However, if you test the queries on a larger sample set; for ten years, for example, instead of one year, you will find that the second query runs much faster than the first one.<\/p>\n<p>The reason for such a difference in performance is the way in which the queries deal with dates.<\/p>\n<p>The first query uses an arithmetic operator (+) in order to add one day to the date. The second query for the same operation employs SQL Server built-in function.<\/p>\n<p>(You can read the article <a href=\"http:\/\/www.devx.com\/dbzone\/Article\/34594\">&#8220;Best Practices for Date\/Time Calculations in SQL Server&#8221; <\/a>, which explains, why it is possible to use the arithmetic operators in date\/time calculations and why you should avoid such a usage.<\/p>\n<p>The first query from the Listing 4 is typical in showing how the use of arithmetic operations in date\/time calculations can decrease the queries&#8217; performance.)<\/p>\n<p>So far, we examined the technique #2 on relatively small data sets.<\/p>\n<p>There are, however, the situations when the amount of data that needs to be processed will be significantly larger. For example, your system may collect the POS (point of sale) transactions from multi-chain department and grocery stores located in different countries and (or) time zones. <\/p>\n<p>In that case, if you asked to find the missing minutes (seconds) in a few months or even years, you may find that the fastest (second) query from the Listing 4 is also inefficient.<\/p>\n<p>Then, you will need to enhance your queries in order to get the acceptable execution time. <\/p>\n<p>One possible enhancement would be to employ an auxiliary column, as shown in the Listing 5:<\/p>\n<p> <strong>Listing5.<\/strong> Enhanced solutions for gaps&#8211;&#160;Solution&#160;with&#160;identity&#160;column <br \/>IF&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sysobjects&#160; <br \/>&#160;&#160;&#160;WHERE&#160;ID&#160;=&#160;(OBJECT_ID(&#8216;sampleDates2&#8242;))&#160;AND&#160;xtype&#160;=&#160;&#8217;U&#8217;) <br \/>DROP&#160;TABLE&#160;sampleDates2; <br \/>CREATE&#160;TABLE&#160;sampleDates2(n&#160;INT&#160;IDENTITY(1,1)&#160;PRIMARY&#160;KEY,&#160;theDate&#160;datetime); <br \/>INSERT&#160;INTO&#160;sampleDates2(theDate)&#160;SELECT&#160;theDate&#160;FROM&#160;sampleDates; <\/p>\n<p>SELECT&#160;DATEADD(dd,&#160;1,&#160;a.theDate)&#160;AS&#160;startOfGap,&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;DATEADD(dd,&#160;-1,&#160;b.theDate)&#160;AS&#160;endOfGap <br \/>&#160;&#160;&#160;FROM&#160;sampleDates2&#160;a&#160;INNER&#160;JOIN&#160;sampleDates2&#160;b <br \/>&#160;&#160;&#160;ON&#160;a.n&#160;=&#160;b.n&#160;-&#160;1 <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(dd,&#160;a.theDate,&#160;DATEADD(dd,&#160;-1,&#160;b.theDate))&#160;&lt;&gt;&#160;0 <br \/>GO <\/p>\n<p>&#8211;&#160;Solution&#160;with&#160;ROW_NUMBER()&#160;function <br \/>SELECT&#160;DATEADD(dd,&#160;1,&#160;a.theDate)&#160;AS&#160;startOfGap,&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;DATEADD(dd,&#160;-1,&#160;b.theDate)&#160;AS&#160;endOfGap <br \/>&#160;&#160;&#160;FROM&#160; <br \/>&#160;&#160;&#160;&#160;&#160;(SELECT&#160;ROW_NUMBER()&#160;OVER(ORDER&#160;BY&#160;(theDate))&#160;AS&#160;RowNum,&#160;theDate <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;sampleDates)&#160;a&#160; <br \/>&#160;&#160;&#160;INNER&#160;JOIN&#160; <br \/>&#160;&#160;&#160;&#160;&#160;(SELECT&#160;ROW_NUMBER()&#160;OVER(ORDER&#160;BY&#160;(theDate))&#160;AS&#160;RowNum,&#160;theDate <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;sampleDates)&#160;b <br \/>&#160;&#160;&#160;ON&#160;a.RowNum&#160;=&#160;b.RowNum&#160;-&#160;1 <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(dd,&#160;a.theDate,&#160;DATEADD(dd,&#160;-1,&#160;b.theDate))&#160;&lt;&gt;&#160;0 <br \/>GO <\/p>\n<p>&#8211;&#160;Solution&#160;with&#160;common&#160;table&#160;expression <br \/>WITH&#160;sampleCTE(rowNum,&#160;theDate)&#160;AS&#160; <br \/>( <br \/>&#160;&#160;&#160;SELECT&#160;ROW_NUMBER()&#160;OVER(ORDER&#160;BY&#160;theDate)&#160;AS&#160;RowNum,&#160;theDate <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;sampleDates <br \/>) <br \/>SELECT&#160;DATEADD(dd,&#160;1,&#160;a.theDate)&#160;AS&#160;startOfGap,&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;DATEADD(dd,&#160;-1,&#160;b.theDate)&#160;AS&#160;endOfGap <br \/>&#160;&#160;&#160;FROM&#160;sampleCTE&#160;a&#160;INNER&#160;JOIN&#160;sampleCTE&#160;b <br \/>&#160;&#160;&#160;ON&#160;a.RowNum&#160;=&#160;b.RowNum&#160;-&#160;1 <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(dd,&#160;a.theDate,&#160;DATEADD(dd,&#160;-1,&#160;b.theDate))&#160;&lt;&gt;&#160;0 <\/p>\n<p>The queries from the Listing 5 are based on the idea of SQL Server MVP Erland Sommarskog (Stockholm).<\/p>\n<p>All you need to do is to assign the order number to each row in the table and then to compare the values (they can be dates or numbers or even letters) in the adjacent rows, using the order numbers.<\/p>\n<p>The first query in the Listing 5 uses the identity column in order to enumerate the rows in the table.<\/p>\n<p>The second and third queries use ranking function ROW_NUMBER().<\/p>\n<p>All three queries are much faster than the fastest (second) query from the Listing 4.<\/p>\n<p>Note, that queries from the Listing 5 require column theDate (table sampleDates) to be sorted before assigning the order number. Since table sampleDates has a primary key constraint, the clustered index is also created on column theDate. That means you don&#8217;t need to sort the data in table sampleDates explicitly: clustered index will do that automatically.<\/p>\n<p>One more thing that you should note is the incomplete results, returned by all the queries from the Listing 4 and Listing 5.<\/p>\n<p>Indeed, Jan 01, Dec 30 and Dec 31 are missing dates in table sampleDates. However, they are not shown in the result as the gaps, because all the queries treat Jan 02, 2007 as the beginning and Dec 29, 2007 as the end of the sample data set.<\/p>\n<p>In order to get the correct result, you need to insert the dates Dec 31, 2006 and Jan 01, 2008 into the table sampleDates.<\/p>\n<h3>Technique #3 -the Pivot table <\/h3>\n<p>One more technique presents missing dates in the form of the spreadsheet. <\/p>\n<p>This type of output is one of the most attractive for the human eye and probably is the favorite document type of your boss.<\/p>\n<p>The technique shown in the Listing 6, uses SQL Server 2005 PIVOT operator in order to produce tabular output:<\/p>\n<p> <strong>Listing6.<\/strong> Solution with PIVOT operatorSELECT&#160;* <br \/>&#160;&#160;FROM <br \/>&#160;&#160;(SELECT&#160; <br \/>CONVERT(VARCHAR(13),DATEADD(dd,num,&#8217;Dec&#160;31,&#160;2006&#8242;),107)&#160;AS&#160;&#160;missingDate, <br \/>&#160;&#160;DATEPART(yy,&#160;DATEADD(dd,&#160;num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;))&#160;YearNum, <br \/>&#160;&#160;DATEPART(wk,&#160;DATEADD(dd,&#160;num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;))&#160;Wk_Year, <br \/>&#160;&#160;DATENAME(dw,&#160;DATEADD(dd,&#160;num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;))&#160;weekDayName <br \/>&#160;&#160;&#160;FROM&#160;sequence&#160;t1&#160;LEFT&#160;OUTER&#160;JOIN&#160;sampleDates&#160;t2&#160; <br \/>&#160;&#160;&#160;ON&#160;DATEADD(dd,&#160;t1.num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;)&#160;=&#160;t2.theDate <br \/>&#160;&#160;&#160;WHERE&#160;t2.theDate&#160;IS&#160;NULL <br \/>&#160;&#160;)&#160;sourceTable <br \/>PIVOT&#160; <br \/>(MIN(missingDate) <br \/>FOR&#160;weekDayName&#160; <br \/>IN <br \/>(Sunday,&#160;Monday,&#160;Tuesday,&#160;Wednesday,&#160;Thursday,&#160;Friday,&#160;Saturday))&#160;AS&#160;pivotTable; <\/p>\n<p>The beauty of the query from the Listing 6 is not only in its cross-tab view. That query also shows days of the week, which is very convenient, when you need to analyze business (application) that runs not whole week, let us say 5-6 days per week. In that case, the missing Saturdays and Sundays will be considered as weekends, but not as the missing days.<\/p>\n<h3>How To Find The Missing Hours <\/h3>\n<p>This bonus example shows how to apply the above techniques to the case of missing hours <\/p>\n<p>You can also use this paragraph as a reference, when you need to find missing minutes or seconds.<\/p>\n<p>First, create a sample data set (see Listing 7):<\/p>\n<p> <strong>Listing7.<\/strong> Create sample data set for hours&#8211;&#160;Generate&#160;hours&#160;for&#160;the&#160;year&#160;2007 <br \/>DECLARE&#160;&#160;&#160;&#160;@iniDate&#160;smalldatetime; <br \/>SELECT&#160;@iniDate&#160;=&#160;&#8217;Dec&#160;31,&#160;2006&#8242;; <br \/>IF&#160;EXISTS(SELECT&#160;*&#160;FROM&#160;sysobjects&#160; <br \/>&#160;&#160;&#160;WHERE&#160;ID&#160;=&#160;(OBJECT_ID(&#8216;sampleDates_2&#8242;))&#160;AND&#160;xtype&#160;=&#160;&#8217;U&#8217;)&#160; <br \/>DROP&#160;TABLE&#160;sampleDates_2; <br \/>CREATE&#160;TABLE&#160;sampleDates_2(theDate&#160;smalldatetime&#160;PRIMARY&#160;KEY); <\/p>\n<p>INSERT&#160;INTO&#160;sampleDates_2 <br \/>SELECT&#160;DATEADD(hh,num-1,&#160;days)&#160;hours&#160; <br \/>&#160;&#160;&#160;FROM&#160;(SELECT&#160;DATEADD(dd,&#160;num,&#160;@iniDate)&#160;days <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;sequence)&#160;t1 <br \/>&#160;&#160;&#160;CROSS&#160;JOIN&#160;sequence&#160;t2&#160; <br \/>&#160;&#160;&#160;WHERE&#160;num&#160;&lt;=24 <br \/>&#160;&#160;&#160;ORDER&#160;BY&#160;hours <\/p>\n<p>&#8211;&#160;Create&#160;gaps&#160;in&#160;the&#160;sequence&#160;of&#160;hours <br \/>DELETE&#160;sampleDates_2&#160; <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(hh,&#160;0,&#160;theDate)%4&#160;=&#160;0&#160; <br \/>&#160;&#160;&#160;AND&#160;DATEDIFF(dd,&#160;&#8217;Jan&#160;01,&#160;2007&#8242;,theDate)&#160;&lt;&#160;100; <br \/>DELETE&#160;sampleDates_2&#160; <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(hh,&#160;0,&#160;theDate)%5&#160;=&#160;0&#160; <br \/>&#160;&#160;&#160;AND&#160;DATEDIFF(dd,&#160;&#8217;Jan&#160;01,&#160;2007&#8242;,theDate)&#160;BETWEEN&#160;50&#160;AND&#160;150; <br \/>DELETE&#160;sampleDates_2&#160; <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(hh,&#160;0,&#160;theDate)%7&#160;=&#160;0 <br \/>&#160;&#160;&#160;AND&#160;DATEDIFF(dd,&#160;&#8217;Jan&#160;01,&#160;2007&#8242;,theDate)&#160;BETWEEN&#160;75&#160;AND&#160;325; <\/p>\n<p>&#8211;&#160;Just&#160;to&#160;make&#160;the&#160;last&#160;hour&#160;of&#160;the&#160;year&#160;missing&#160; <br \/>DELETE&#160;sampleDates_2&#160;WHERE&#160;theDate&#160;=&#160;&#8217;2007-12-31&#160;23:00:00&#8242;; <\/p>\n<p>&#8211;&#160;In&#160;order&#160;to&#160;get&#160;the&#160;very&#160;first&#160;gap&#160;in&#160;the&#160;beginning&#160;of&#160;the&#160;year&#160;and&#160; <br \/>&#8211;&#160;the&#160;very&#160;last&#160;gap&#160;at&#160;the&#160;end&#160;of&#160;the&#160;year,&#160;add&#160;the&#160;next&#160;dates&#160; <br \/>INSERT&#160;INTO&#160;sampleDates_2&#160;VALUES(&#8216;Dec&#160;31,&#160;2006&#160;23:00:00&#8217;); <br \/>INSERT&#160;INTO&#160;sampleDates_2&#160;VALUES(&#8216;Jan&#160;01,&#160;2008&#160;00:00:00&#8217;); <\/p>\n<p>The following script shows the solutions for missing hours (see Listing 8):<\/p>\n<p> <strong>Listing8.<\/strong> Solutions for missing hours&#8211;&#160;1)&#160;The&#160;gaps&#160;solution&#160;for&#160;missing&#160;hours <br \/>WITH&#160;sampleCTE(rowNum,&#160;theDate)&#160;AS&#160; <br \/>( <br \/>&#160;&#160;&#160;SELECT&#160;ROW_NUMBER()&#160;OVER(ORDER&#160;BY&#160;(theDate))&#160;AS&#160;RowNum,&#160;theDate <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;sampleDates_2 <br \/>) <br \/>SELECT&#160;DATEADD(hh,&#160;1,&#160;a.theDate)&#160;AS&#160;startOfGap,&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;DATEADD(hh,&#160;-1,&#160;b.theDate)&#160;AS&#160;endOfGap <br \/>&#160;&#160;&#160;FROM&#160;sampleCTE&#160;a&#160;INNER&#160;JOIN&#160;sampleCTE&#160;b <br \/>&#160;&#160;&#160;ON&#160;a.RowNum&#160;=&#160;b.RowNum&#160;-&#160;1 <br \/>&#160;&#160;&#160;WHERE&#160;DATEDIFF(hh,&#160;a.theDate,&#160;DATEADD(hh,&#160;-1,&#160;b.theDate))&#160;&lt;&gt;&#160;0; <\/p>\n<p>&#8211;&#160;2)&#160;The&#160;cross-tab&#160;solution&#160;that&#160;counts&#160;the&#160;number&#160;of&#160;missing&#160;hours&#160;per&#160;each&#160;day <br \/>WITH&#160;sourceTable(hours,&#160;days,&#160;weekDayName)&#160;AS <br \/>( <br \/>&#160;&#160;&#160;SELECT&#160;&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;t3.hours,&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;CAST(DATEDIFF(dd,&#160;0,&#160;t3.hours)&#160;AS&#160;smalldatetime)&#160;days, <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;DATENAME(dw,&#160;t3.hours)&#160;weekDayName <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;(SELECT&#160;DATEADD(hh,num-1,&#160;days)&#160;hours,&#160;t2.num&#160; <br \/>&#160;&#160;&#160;FROM&#160; <br \/>&#160;&#160;&#160;(SELECT&#160;DATEADD(dd,&#160;num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;)&#160;days <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;sequence)&#160;t1&#160;CROSS&#160;JOIN&#160;sequence&#160;t2&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;num&#160;&lt;=24)&#160;t3&#160;LEFT&#160;OUTER&#160;JOIN&#160;sampleDates_2&#160;t4&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;ON&#160;t3.hours&#160;=&#160;t4.theDate <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;t4.theDate&#160;IS&#160;NULL <br \/>) <br \/>SELECT&#160;* <br \/>&#160;&#160;FROM&#160;sourceTable <br \/>&#160;&#160;PIVOT&#160; <br \/>(COUNT(hours) <br \/>FOR&#160;weekDayName&#160; <br \/>IN <br \/>(Sunday,&#160;Monday,&#160;Tuesday,&#160;Wednesday,&#160;Thursday,&#160;Friday,&#160;Saturday))&#160;AS&#160;pivotTable; <\/p>\n<p>&#8211;&#160;3)&#160;The&#160;cross-tab&#160;solution&#160;that&#160;shows&#160;missing&#160;hours&#160;per&#160;each&#160;day <br \/>SELECT&#160;*&#160; <br \/>&#160;&#160;FROM <br \/>&#160;&#160;(SELECT&#160;&#160;CONVERT(VARCHAR(15),&#160;t3.hours,&#160;108)&#160;hours, <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;t3.num&#160;-&#160;1&#160;AS&#160;dayHour, <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;DATEPART(mm,&#160;t3.hours)&#160;monthNum, <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;DATEPART(dd,&#160;t3.hours)&#160;dateNum,&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;CONVERT(VARCHAR(13),&#160;t3.hours,&#160;107)&#160;days <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;(SELECT&#160;DATEADD(hh,num-1,&#160;days)&#160;hours,&#160;t2.num&#160; <br \/>&#160;&#160;&#160;FROM&#160; <br \/>&#160;&#160;&#160;(SELECT&#160;DATEADD(dd,&#160;num,&#160;&#8217;Dec&#160;31,&#160;2006&#8242;)&#160;days <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;FROM&#160;sequence)&#160;t1&#160;CROSS&#160;JOIN&#160;sequence&#160;t2&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;num&#160;&lt;=24)&#160;t3&#160;LEFT&#160;OUTER&#160;JOIN&#160;sampleDates_2&#160;t4&#160; <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;ON&#160;t3.hours&#160;=&#160;t4.theDate <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;WHERE&#160;t4.theDate&#160;IS&#160;NULL <br \/>)&#160;sourceTable <br \/>PIVOT&#160; <br \/>(MIN(hours) <br \/>FOR&#160;dayHour&#160; <br \/>IN <br \/>([0],[1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12], <br \/>[13],[14],[15],[16],[17],[18],[19],[20],[21],[22],[23]))&#160;AS&#160;pivotTable; <\/p>\n<p>The first solution from the Listing 8 shows the missing hours as the beginning and the end of the gap.<\/p>\n<p>The second solution shows the number of missing hours per each day<\/p>\n<p>If this is not enough, the third solution will give you detailed information about missing hours.<\/p>\n<h3>The Task Completed: Here Are The Results<\/h3>\n<p class=\"start\">You found missing dates and sent the results to your boss. After short investigation, the problem was identified: some data has been deleted from database by mistake, during the application upgrade.<\/p>\n<p>In that situation, everybody relied on you and once again, you rose to the challenge. You found the old backup, restored the database under another name and moved the missing data to the original database. <\/p>\n<p>Here is the result of your efforts: <\/p>\n<p>The customer is satisfied. The manager impressed by your skills. You can be proud of your achievements.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Often, the quickest way to determine whether you have missing data in a table such a ledger or journal is to see if there are gaps in the dates where one wouldn&#8217;t expect them. But how do you do that in an emergency, particularly in a large table, when every minute counts?<br \/>\n&hellip;<\/p>\n","protected":false},"author":221828,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[4150,4252,4826],"coauthors":[11311],"class_list":["post-315","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-sql","tag-t-sql-programming","tag-tsql-sql-sql-server-date-sequel"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/315","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\/221828"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=315"}],"version-history":[{"count":3,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/315\/revisions"}],"predecessor-version":[{"id":66955,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/315\/revisions\/66955"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=315"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=315"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=315"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=315"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}