{"id":1987,"date":"2015-04-22T00:00:00","date_gmt":"2015-04-22T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/time-slots-an-essential-extension-to-calendar-tables\/"},"modified":"2021-09-29T16:21:28","modified_gmt":"2021-09-29T16:21:28","slug":"time-slots-an-essential-extension-to-calendar-tables","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/time-slots-an-essential-extension-to-calendar-tables\/","title":{"rendered":"Time Slots &#8211; An Essential Extension to Calendar Tables"},"content":{"rendered":"<div class=\"article-content\">\n<p class=\"start\"> \tOftentimes you&#8217;ll need to generate a table of time slots to use in much the same way as a <a href=\"http:\/\/dwaincsql.com\/2014\/03\/30\/calendar-tables-in-t-sql\/\">Calendar table<\/a>. You might want to use this to do aggregations for reports, such as the amount of trading by the time of day, and for doing this a table of time-slots within a day makes it easy, and more flexible. You can, of course, do this without any Time-Slot tables until things get more complicated, as when the business only runs two shifts every twenty-four hours, and you only need the time slots from 05:00 to 20:00 (sixteen time slots with eight hours in each shift), or when you need to generate time slots of over an hour (like every four hours) or minutes, etc. <\/p>\n<p>  &#8220;Generate them when you need them.&#8221;<br \/> \t&#8212; <a href=\"http:\/\/mvp.microsoft.com\/en-us\/mvp\/Jeff%20Moden-4020758\">SQL MVP Jeff Moden<\/a> on time slots  <\/p>\n<p> \tIndeed, since we&#8217;re all probably familiar with <a href=\"http:\/\/dwaincsql.com\/2014\/03\/27\/tally-tables-in-t-sql\/\">Tally tables<\/a>, whether in a physical table or one that is generated in-line, this is relatively easy to accomplish. <\/p>\n<h2>Using Time-slots: A Real World Example <\/h2>\n<p> \tSuppose we run a manufacturing business that has three machines producing widgets. Each machine produces a discrete quantity of widgets, but spits out that discrete quantity at irregular intervals. Let&#8217;s create some sample data for this. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">-- A table holding our widget-producing machines\nCREATE TABLE dbo.Machines\n(\n    MachineID       SMALLINT IDENTITY PRIMARY KEY\n    ,MachineName    VARCHAR(30)\n);\nGO\n\n-- Sample data for the three widget-producing machines\nINSERT INTO dbo.Machines (MachineName)\nVALUES('Widget Maker 1'),('Widget Maker 2'),('Widget Maker 3');\nGO\n\n-- A table holding our production results\nCREATE TABLE dbo.MachineProduction\n(\n    MachineID       SMALLINT\n    ,TimeProduced   DATETIME\n    ,QtyOfWidgets   INT\n    ,PRIMARY KEY (MachineID, TimeProduced)\n);\nGO\n\n-- Sample data for production results\nWITH ProductionDays (pd) AS\n(\n    -- Three days worth\n    SELECT '2015-04-01' \n    UNION ALL SELECT '2015-04-02'\n    UNION ALL SELECT '2015-04-03'\n),\n    Tally (n) AS\n(\n    -- A Tally table that produces 1,000 rows\n    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))\n    FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) a(n)               \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n)\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n)\n)\nINSERT INTO dbo.MachineProduction\n(\n    MachineID, TimeProduced, QtyOfWidgets\n)\nSELECT MachineID, TimeProduced\n    -- Random number of widgets produced (between 1 and 20)\n    ,1+ABS(CHECKSUM(NEWID()))%20\nFROM\n(\n    -- Use DISTINCT here to avoid PRIMARY KEY duplicates\n    SELECT DISTINCT MachineID\n        -- We'll add to our production date (pd) a random number of seconds between\n        -- 1 and 86,400 (number of seconds in a day) to get a discrete time of day, \n        -- then create a Cartesian product against this and our production machines\n        ,TimeProduced=DATEADD(second, ABS(CHECKSUM(NEWID()))%86400, pd)\n    FROM ProductionDays a\n    CROSS APPLY Tally b\n    CROSS APPLY (VALUES(1),(2),(3)) c (MachineID)\n) a\n-- Create some gaps where the machines were down (not producing anything) on those days\nWHERE NOT ((MachineID = 1 AND DATEADD(day, DATEDIFF(day, 0, TimeProduced), 0) = '2015-04-02') OR \n    (MachineID = 2 AND DATEADD(day, DATEDIFF(day, 0, TimeProduced), 0) = '2015-04-01'));\n<\/pre>\n<p> \tNote that the WHERE filter creates some gaps simulating a case where MachineID=1 was not operating on 2015-04-02 and MachineID=2 was not operating on 2015-04-01. <\/p>\n<p> \tNow suppose we&#8217;d like to track our widgets production every six hours (our time slot increment) for the three days we&#8217;ve created our data, including those time slots where a machine produced nothing. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">WITH TimeSlotsOffsets \n    (TimeSlot, StartOffsetHrs, EndOffsetHrs)\nAS\n(\n    -- Six hours at a time\n    SELECT      CAST('00:00' AS TIME), 0, 6\n    UNION ALL SELECT '06:00', 6, 12\n    UNION ALL SELECT '12:00', 12, 18\n    UNION ALL SELECT '18:00', 18, 24\n),\n    ProductionDays (pd) AS\n(\n    -- Three days worth\n    SELECT '2015-04-01' \n    UNION ALL SELECT '2015-04-02'\n    UNION ALL SELECT '2015-04-03'\n)\nSELECT TimeSlot     = [BaseDateWithTimeSlotStart]\n    ,a.MachineID\n    ,QtyofWidgets   = ISNULL(b.QtyOfWidgets, 0)\nFROM\n(\n    -- Create Cartesian product\n    SELECT c.MachineID\n        ,[BaseDate] = a.pd\n        ,[BaseDateWithTimeSlotStart]    = DATEADD(hour, b.StartOffsetHrs, a.pd)\n        ,[BaseDateWithTimeSlotEnd]      = DATEADD(hour, b.EndOffsetHrs, a.pd)\n    -- Production days of interest\n    FROM ProductionDays a\n    -- Time slots of 6 hours\n    CROSS JOIN TimeSlotsOffsets b\n    -- Unique MachineIDs\n    CROSS JOIN dbo.Machines c\n) a\nOUTER APPLY \n(\n    -- Sum the widgets produced\n    SELECT b.MachineID, QtyOfWidgets=SUM(b.QtyOfWidgets)\n    FROM dbo.MachineProduction b\n    WHERE a.MachineID = b.MachineID AND\n        -- where production time is within the time slot period\n        b.TimeProduced &gt;= a.[BaseDateWithTimeSlotStart] AND \n            b.TimeProduced &lt; a.[BaseDateWithTimeSlotEnd] \n    GROUP BY b.MachineID\n) b\nORDER BY MachineID, [BaseDateWithTimeSlotStart];<\/pre>\n<p> \tA troubling aspect, to me at least, of the above code is that we have essentially hard-coded our time slots. However it does produce the results we have asked for. In the above, we&#8217;ve created a Cartesian product of machines by days by time slots, and then retrieved the total widgets created in each time slot. <\/p>\n<p> \tLet&#8217;s suppose one of our pesky business users comes along and tells us he&#8217;d like to change the time slot duration to six hours. Since we hard-coded that the first time, we&#8217;d now like to anticipate that he may change it again, and let&#8217;s also add something just a bit more complex, such as a running total by machine by day. So we&#8217;ll add a local variable that specifies the time slot duration, and use it to dynamically generate our time slots within the TimeSlotsOffsets <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms175972.aspx\">Common Table Expression<\/a> (<a href=\"http:\/\/dwaincsql.com\/2014\/03\/23\/common-table-expressions-in-sql\/\">CTE<\/a>). Other than adding the running total by day, we&#8217;ll still calculate the same Cartesian product of machines by days by time slots and we&#8217;re good to go. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">DECLARE @TimeSlotHrsDuration    SMALLINT = 6;     -- 6 hours\n\nWITH Tally (n) AS\n(\n    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1      -- Zero based\n    FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) a (n)\n), \n    TimeSlotsOffsets (StartOffsetHrs, EndOffsetHrs)\nAS\n(\n    -- Varying duration for each time slot\n    SELECT n*@TimeSlotHrsDuration, (1+n)*@TimeSlotHrsDuration     \n    FROM Tally\n    WHERE n &lt; 24 \/ @TimeSlotHrsDuration\n),\n    ProductionDays (pd) AS\n(\n    -- Three days worth\n    SELECT '2015-04-01' \n    UNION ALL SELECT '2015-04-02'\n    UNION ALL SELECT '2015-04-03'\n)\nSELECT TimeSlot     = [BaseDateWithTimeSlotStart]\n    ,a.MachineID\n    ,QtyofWidgets   = ISNULL(b.QtyOfWidgets, 0)\n    ,DailyQty       = ISNULL(SUM(b.QtyOfWidgets) OVER \n        (\n            PARTITION BY a.MachineID, [BaseDate]\n            ORDER BY [BaseDateWithTimeSlotStart]\n            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n        ), 0)\nFROM\n(\n    -- Create Cartesian product\n    SELECT c.MachineID\n        ,[BaseDate] = a.pd\n        ,[BaseDateWithTimeSlotStart]    = DATEADD(hour, b.StartOffsetHrs, a.pd)\n        ,[BaseDateWithTimeSlotEnd]      = DATEADD(hour, b.EndOffsetHrs, a.pd)\n    -- Production days of interest\n    FROM ProductionDays a\n    -- Time slots of 6 hours\n    CROSS JOIN TimeSlotsOffsets b\n    -- Unique MachineIDs\n    CROSS JOIN dbo.Machines c\n) a\nOUTER APPLY \n(\n    -- Sum the widgets produced\n    SELECT b.MachineID, QtyOfWidgets=SUM(b.QtyOfWidgets)\n    FROM dbo.MachineProduction b\n    WHERE a.MachineID = b.MachineID AND\n        -- where production time is within the time slot period\n        b.TimeProduced &gt;= a.[BaseDateWithTimeSlotStart] AND \n            b.TimeProduced &lt; a.[BaseDateWithTimeSlotEnd] \n    GROUP BY b.MachineID\n) b\nORDER BY MachineID, [BaseDateWithTimeSlotStart];<\/pre>\n<p> \tIf you run the setup data and the query, it should produce a results set that looks something like the following. Of course your results will vary due to the random test data that we set up, however you should at least see the same gaps in production due to the machine outages we introduced into the test data. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">TimeSlot                 MachineID  QtyofWidgets  DailyQty\n2015-04-01 00:00:00.000  1          2226          2226\n2015-04-01 06:00:00.000  1          2955          5181\n2015-04-01 12:00:00.000  1          2548          7729\n2015-04-01 18:00:00.000  1          2622          10351\n2015-04-02 00:00:00.000  1          0             0\n2015-04-02 06:00:00.000  1          0             0\n2015-04-02 12:00:00.000  1          0             0\n2015-04-02 18:00:00.000  1          0             0\n2015-04-03 00:00:00.000  1          2753          2753\n2015-04-03 06:00:00.000  1          2795          5548\n2015-04-03 12:00:00.000  1          2555          8103\n2015-04-03 18:00:00.000  1          2639          10742\n2015-04-01 00:00:00.000  2          0             0\n2015-04-01 06:00:00.000  2          0             0\n2015-04-01 12:00:00.000  2          0             0\n2015-04-01 18:00:00.000  2          0             0\n2015-04-02 00:00:00.000  2          2499          2499\n2015-04-02 06:00:00.000  2          2608          5107\n2015-04-02 12:00:00.000  2          2500          7607\n2015-04-02 18:00:00.000  2          2741          10348\n2015-04-03 00:00:00.000  2          2550          2550\n2015-04-03 06:00:00.000  2          2528          5078\n2015-04-03 12:00:00.000  2          2563          7641\n2015-04-03 18:00:00.000  2          2676          10317\n2015-04-01 00:00:00.000  3          2537          2537\n2015-04-01 06:00:00.000  3          2627          5164\n2015-04-01 12:00:00.000  3          2508          7672\n2015-04-01 18:00:00.000  3          2611          10283\n2015-04-02 00:00:00.000  3          2752          2752\n2015-04-02 06:00:00.000  3          2614          5366\n2015-04-02 12:00:00.000  3          2778          8144\n2015-04-02 18:00:00.000  3          2528          10672\n2015-04-03 00:00:00.000  3          2500          2500\n2015-04-03 06:00:00.000  3          2427          4927\n2015-04-03 12:00:00.000  3          2665          7592\n2015-04-03 18:00:00.000  3          2599          10191<\/pre>\n<p> \tThis query generates a total of 36 rows (3 days x 3 machines x 4 time slots per day). <\/p>\n<p> \tIn this article, we&#8217;re going to show you how to remove a whole bunch of extraneous code, such as the two CTEs, and minimize the effort to create the Cartesian product across any duration of time slot using a function I&#8217;ll call <code>CalculateTimeSlots<\/code>, which will be introduced just a bit later. <\/p>\n<h2>An In-line Tally Table for Generating Time Slots<\/h2>\n<p> \tTo generate time slots of any duration, even down to the second, we can use an in-line Tally table such as the one below. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">WITH Tally (n) AS\n(\n    SELECT \n        ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1                   -- zero-based\n    -- Returns exactly 86400 rows (number of seconds in a day)\n    FROM       (VALUES(0),(0),(0),(0),(0),(0))                         a(n) -- 6 rows \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         d(n) -- x10 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         e(n) -- x10 rows\n)                                                                           -- = 86,400 rows\nSELECT n\nFROM Tally;<\/pre>\n<p> \tOur Tally table has been specifically designed to return exactly 86,400 rows, which just happens to correspond to the number of seconds in a day. It is a zero-based Tally table, so we can use it directly as a time offset. <\/p>\n<h2>Time Slots &#8211; Hours or Minutes in a Day<\/h2>\n<p> \tBy adding a TOP clause to our Tally table along with a little <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms186819.aspx\">DATEADD<\/a> logic, we can quite easily create any number of time slots that are based on hours, minutes or seconds. The TOP clause will minimize the actual number of rows returned by the Tally table, to just what we need for our time slot duration. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">WITH Tally (n) AS\n(\n    SELECT TOP (24)\n        ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1                   -- zero-based\n    -- Returns exactly 86400 rows (number of seconds in a day)\n    FROM       (VALUES(0),(0),(0),(0),(0),(0))                         a(n) -- 6 rows \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         d(n) -- x10 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         e(n) -- x10 rows\n)                                                                           -- = 86,400 rows\n-- 24 time slots, one for each hour of the day\nSELECT TimeSlot=DATEADD(hour, n, CAST('00:00' AS TIME))\nFROM Tally;\n\nWITH Tally (n) AS\n(\n    SELECT TOP (3600)\n        ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1                   -- zero-based\n    -- Returns exactly 86400 rows (number of seconds in a day)\n    FROM       (VALUES(0),(0),(0),(0),(0),(0))                         a(n) -- 6 rows \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         d(n) -- x10 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         e(n) -- x10 rows\n)                                                                           -- = 86,400 rows\n-- 3600 time slots, one for each minute of the day\nSELECT TimeSlot=DATEADD(minute, n, CAST('00:00' AS TIME))\nFROM Tally;<\/pre>\n<p> \tNotice how we&#8217;ve adjusted the TOP clause in each case to the number of time slots we want to generate, and then used the return value from the Tally table to add either hours or minutes to midnight. <\/p>\n<p> \tWe could just as easily have adjusted both the base time and the number of time slots (in TOP) to give us our two eight hour shifts that run from 05:00 to 20:00. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">WITH Tally (n) AS\n(\n    SELECT TOP (16)\n        ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1                   -- zero-based\n    -- Returns exactly 86400 rows (number of seconds in a day)\n    FROM       (VALUES(0),(0),(0),(0),(0),(0))                         a(n) -- 6 rows \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         d(n) -- x10 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         e(n) -- x10 rows\n)                                                                           -- = 86,400 rows\n-- 16 time slots starting at 05:00, one for each hour in two 16 hour shifts\nSELECT TimeSlot=DATEADD(hour, n, CAST('05:00' AS TIME))\nFROM Tally;<\/pre>\n<h2>Creating Time Slots across Multiple Days<\/h2>\n<p> \tIf we combine our time slot generator with a Calendar table, we can generate our time slots across multiple calendar days. In the next example, I use an in-line, <a href=\"https:\/\/technet.microsoft.com\/en-us\/library\/ms191165(v=sql.105).aspx\">Table Valued Function<\/a> (iTVF) that appears in the blog entry I linked to earlier about Calendar tables. It is also included in the resources file that appears with this article in case you&#8217;d like to follow along and try some of the examples here. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">WITH Tally (n) AS\n(\n    SELECT TOP (24)\n        ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1                   -- zero-based\n    -- Returns exactly 86400 rows (number of seconds in a day)\n    FROM       (VALUES(0),(0),(0),(0),(0),(0))                         a(n) -- 6 rows \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         d(n) -- x10 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         e(n) -- x10 rows\n)                                                                           -- = 86,400 rows\n-- 24 time slots, one for each hour of the day\nSELECT TimeSlot=DATEADD(hour, DATEDIFF(hour, 0, DATEADD(hour, n, CAST('00:00' AS TIME))), [Date])\nFROM dbo.GenerateCalendar('2015-04-01', 2) a\nCROSS APPLY Tally b;\n<\/pre>\n<p> \tMy <code>GenerateCalendar<\/code> FUNCTION returns [Date] (actually a <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms187819.aspx\">DATETIME<\/a>) as one of its columns. It is necessary to convert the time slot to a time offset in hours using <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/ms189794.aspx\">DATEDIFF<\/a>, and then add this back to the [Date] column to get our time slot within that specific day. The query above returns 48 rows (some omitted for brevity). <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">TimeSlot\n2015-04-01 00:00:00.000\n2015-04-01 01:00:00.000\n2015-04-01 02:00:00.000\n-- 18 rows redacted\n2015-04-01 21:00:00.000\n2015-04-01 22:00:00.000\n2015-04-01 23:00:00.000\n2015-04-02 00:00:00.000\n2015-04-02 01:00:00.000\n2015-04-02 02:00:00.000\n-- 18 rows redacted\n2015-04-02 21:00:00.000\n2015-04-02 22:00:00.000\n2015-04-02 23:00:00.000\n<\/pre>\n<h2>Another Example: Fifteen Minute Time Slots<\/h2>\n<p> \tSuppose we want to generate time slots that are precisely fifteen minutes apart. We know that there are 1,440 (24 x 60) minutes in a day, so there should be one-fourth this number of time periods if we want them spaced 15 minutes apart (so 96 time slots). <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">WITH Tally (n) AS\n(\n    SELECT TOP (96)\n        ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1                   -- zero-based\n    -- Returns exactly 86400 rows (number of seconds in a day)\n    FROM       (VALUES(0),(0),(0),(0),(0),(0))                         a(n) -- 6 rows \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         d(n) -- x10 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         e(n) -- x10 rows\n)                                                                           -- = 86,400 rows\n-- 3600 time slots, one for each minute of the day\nSELECT TimeSlot=DATEADD(minute, n*15, CAST('00:00' AS TIME))\nFROM Tally;<\/pre>\n<p> \tWe&#8217;ve had to do two things to our query: 1) modify the TOP clause to return precisely the correct number of 15 minute time slots we calculated earlier and 2) multiply our Tally return column by that same 15 minutes. The results are as we expect, once again abbreviated for clarity. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">TimeSlot\n00:00:00.0000000\n00:15:00.0000000\n00:30:00.0000000\n00:45:00.0000000\n01:00:00.0000000\n01:15:00.0000000\n... 84 rows redacted  \n22:30:00.0000000\n22:45:00.0000000\n23:00:00.0000000\n23:15:00.0000000\n23:30:00.0000000\n23:45:00.0000000\n<\/pre>\n<h2>Clock Arithmetic is for Geeks, so let&#8217;s just Do It Once<\/h2>\n<p> \tI don&#8217;t know how all of my readers feel about this, but personally I hate clock arithmetic. All of those magic numbers &#8211; seconds in a day, minutes in a day, etc. are just something I hate having to remember all the time. So while Jeff may be right, that you can easily generate time slots of any sort when you need them, I&#8217;d prefer to only have to do it once so I don&#8217;t have to remember where to divide by 1,440, or do anything else that requires me to think about this for more than a microsecond. <\/p>\n<p> \tPerhaps it might be possible to construct a nice, tidy little iTVF to satisfy the most common time slot needs. After a bit of fiddling around, I came up with this, which can also be found in the attached resources file. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE FUNCTION [dbo].[CalculateTimeSlots]\n-- A function to calculate time slots over a twenty-four hour period\n (\n    -- @SlotType: 'hour', 'minute' or 'second' (invalid defaults to 'hour')\n    -- Other allowed abbreviations as in DATEADD\/DATEDIFF are also supported\n    -- for these three slot types.\n    @SlotType           VARCHAR(6)   \n    -- @SlotDuration: Must be a zero remainder divisor of @SlotType\n    --  For @SlotType='second' then when 60%@SlotDuration = 0 the increment is valid  \n    --  For @SlotType='minute' then when 60%@SlotDuration = 0 the increment is valid  \n    --  For @SlotType='hour' then when 24%@SlotDuration = 0 the increment is valid\n    --  If invalid, defaults to 1  \n    ,@SlotDuration      SMALLINT \n    ,@BaseDate          DATETIME \n    ,@NumberOfDays      INT      \n) RETURNS TABLE WITH SCHEMABINDING\nRETURN\nWITH Tally (n) AS\n(\n    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1                   -- zero-based\n    -- Returns exactly 86400 rows (number of seconds in a day)\n    FROM       (VALUES(0),(0),(0),(0),(0),(0))                         a(n) -- 6 rows \n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n) -- x12 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         d(n) -- x10 rows\n    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))         e(n) -- x10 rows\n),                                                                          -- = 86,400 rows\n    MagicNumbersForHMS AS\n(\n    -- All clock arithmetic magic numbers are consolidated here to drive the calculations\n    -- that will follow.\n    SELECT SlotType = 'second', SlotOffset=1, UnitsPerNext=60, UnitsPerDay=86400\n    UNION ALL SELECT  'minute',            60,             60,             1440\n    UNION ALL SELECT  'hour',              3600,           24,             24\n),\n    MiscParameters (t, SlotOffset, SlotDuration, TimeSlots, BaseDate, NoDays) AS\n(\n    -- Start with the base offset as a TIME datatype\n    SELECT CAST('00:00' AS TIME)\n        ,b.SlotOffset\n        -- Set SlotDuration to 1 if not evenly divisible by specified @SlotDuration\n        ,CASE UnitsPerNext % @SlotDuration \n              WHEN 0 \n              THEN @SlotDuration \n              ELSE 1 \n              END\n        -- Calculate the number of time slots for this call\n        ,CASE\n              WHEN @SlotDuration &lt;= 0 OR UnitsPerNext%@SlotDuration &lt;&gt; 0\n              THEN UnitsPerDay\n              ELSE UnitsPerDay \/ ISNULL(@SlotDuration, 1)\n              END\n        -- Pass the BaseDate forward\n        ,@BaseDate\n        -- Calculate the number of days (1 if not specified) for the Cartesian product\n        ,CASE WHEN @BaseDate IS NULL OR NULLIF(@NumberOfDays, 0) IS NULL\n              THEN 1 \n              ELSE ABS(@NumberOfDays) \n              END\n    FROM \n    (\n        -- Default the @SlotType to hours if invalid\n        SELECT SlotType=CASE \n              -- Valid for hours\n              WHEN @SlotType IN ('hour', 'hh') \n              THEN 'hour' \n              -- Valid for minutes\n              WHEN @SlotType IN ('minute', 'mi', 'n') \n              THEN 'minute' \n              -- Valid for seconds\n              WHEN @SlotType IN ('second','ss', 's') \n              THEN 'second' \n              ELSE 'hour' \n              END\n    ) a\n    JOIN MagicNumbersForHMS b ON a.SlotType = b.SlotType\n)\nSELECT [TimeSlot]                   = DATEADD(second, c.n * a.SlotOffset * a.SlotDuration, t) \n    -- Offset based on @SlotType\n    ,[TimeSlotOffsetUnits]          = c.n * a.SlotDuration \n    -- Offset based on seconds\n    ,[TimeSlotOffsetSeconds]        = c.n * a.SlotOffset * SlotDuration\n    -- Extended return columns starting with base date\n    ,[BaseDate]                     = CAST(b.[Date] AS DATE)\n    -- Base date plus offset\n    ,[BaseDateWithTimeSlotStart]    = DATEADD(second, c.n*a.SlotOffset*a.SlotDuration, b.[Date])\n    -- Base date plus offset plus duration\n    ,[BaseDateWithTimeSlotEnd]      = DATEADD(second, (c.n+1)*a.SlotOffset*a.SlotDuration, b.[Date])\nFROM MiscParameters a\n-- CROSS APPLY to generate the requisite number of days from the extended parameters\n-- (1 day\/row if not specified)\nCROSS APPLY \n(\n    SELECT TOP (a.NoDays) [Date]=b.n + a.BaseDate \n    FROM Tally b\n    ORDER BY n \n) b\n-- Generate the Cartesian product of days vs. time slots\nCROSS APPLY \n(\n    SELECT TOP (a.TimeSlots) c.n\n    FROM Tally c\n    ORDER BY n\n) c;<\/pre>\n<p> \tWe&#8217;ve created the arguments to this function based on the premise that your time slots are either in hours, minutes or seconds. You&#8217;ll need to do a bit of work if you want to extend it to milliseconds or microseconds. <\/p>\n<p> \tLet&#8217;s describe each of the arguments, including the last two which I call &#8220;extended arguments&#8221; because of the additional value they add. <\/p>\n<ul>\n<li><code>@SlotType<\/code> (VARCHAR(6)) is passed as &#8220;second&#8221; for seconds, &#8220;minute&#8221; for minutes and &#8220;hour&#8221; for hours. It also supports the standard abbreviation for these &#8220;dateparts&#8221; as defined for the DATEADD and DATEDIFF T-SQL functions, in the hopes that this makes them easier to remember.<\/li>\n<li><code>@SlotDuration<\/code> (SMALLINT) is the duration you want for each time slot. For example, time slots that are in hours where you want 24 of them in a day are each one hour in duration. <code>@SlotDuration<\/code> must be specified as an integer that equally divides into the number of units of <code>@SlotType<\/code> that are present in the next larger time unit. For example, in a minute there are sixty seconds, so if you specify <code>@SlotType<\/code> = &#8216;second&#8217; then <code>@SlotDuration<\/code> must be either 1, 2, 3, 4, 5, 6, 10, 12, 15 or 30 (a number that divides into 60 with no remainder).<\/li>\n<li><code>@BaseDate<\/code> (DATETIME) is the first of two extended arguments that can be used if you want your time slots to include not just a time component but also a date component. Coupled with additional return columns, this can be quite useful in cases where you need your time slots generated across many days.<\/li>\n<li><code>@NumberOfDays<\/code> (INT) is the second of two extended arguments that (in conjunction with <code>@BaseDate<\/code>) specifies the number of days or dates for which you want the time slots enumerated.<\/li>\n<\/ul>\n<p> \tWe&#8217;ll clarify each of these arguments along with the returned columns from the function as we look at some examples, starting with an easy one. The comments in the function hopefully explain what the function does. Using the extended parameters allows you to generate a <a href=\"http:\/\/en.wikipedia.org\/wiki\/Cartesian_product\">Cartesian product<\/a> of days by time slots. <\/p>\n<p> \tTo return 24 time slots of one hour, any of these parameter combinations will work: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT TimeSlot\nFROM dbo.CalculateTimeSlots(NULL, NULL, NULL, NULL);\n-- or\n--FROM dbo.CalculateTimeSlots('hour', NULL, NULL, NULL);\n-- or\n--FROM dbo.CalculateTimeSlots('hour', 1, NULL, NULL);\n-- or\n--FROM dbo.CalculateTimeSlots('minute', 60, NULL, NULL) a;<\/pre>\n<p> \tTo handle our two eight hour shifts starting at 05:00, we simply add a WHERE filter. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT TimeSlot\nFROM dbo.CalculateTimeSlots('hour', 1, NULL, NULL)\n-- or\n--FROM dbo.CalculateTimeSlots('hour', NULL, NULL, NULL)\n-- or\n--FROM dbo.CalculateTimeSlots('hour', 99, NULL, NULL)\nWHERE TimeSlot BETWEEN '05:00' AND '20:00';\n<\/pre>\n<p> \tYou can try the alternate FROM combinations to see how an invalid slot increment gets translated to one. <\/p>\n<p> \tTo get our 96 time slots at 15 minute intervals, the call is equally simple. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT TimeSlot\nFROM dbo.CalculateTimeSlots('minute', 15, NULL, NULL);\n<\/pre>\n<p> \tWe can generate our time slots across two days using our GenerateCalendar function like we did before: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">-- 24 time slots, one for each hour of the day using GenerateCalendar\nSELECT TimeSlot1    = DATEADD(hour, DATEDIFF(hour, 0, [TimeSlot]), [Date])\n    ,TimeSlot2      = DATEADD(hour, TimeSlotOffsetUnits, [Date])\n    ,TimeSlot2      = DATEADD(second, TimeSlotOffsetSeconds, [Date])\nFROM dbo.GenerateCalendar('2015-04-01', 2) a\nCROSS APPLY dbo.CalculateTimeSlots('hour', 1, NULL, NULL) b; -- 2=hourly time slots\n<\/pre>\n<p> \tOr we can simply use the extended call parameters to simplify the query even further. In this case, the four columns all return the same amounts (shown so you&#8217;ll understand just how much more simplified this query is). <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">-- 24 time slots, one for each hour of the day without GenerateCalendar\nSELECT BaseDateWithTimeSlotStart                       -- Use the extended return column\n    -- These calculations could be done but are not necessary\n    ,TimeSlot1    = DATEADD(hour, DATEDIFF(hour, 0, [TimeSlot]), CAST(a.BaseDate AS DATETIME))\n    ,TimeSlot2      = DATEADD(hour, TimeSlotOffsetUnits, CAST(a.BaseDate AS DATETIME))\n    ,TimeSlot2      = DATEADD(second, TimeSlotOffsetSeconds, CAST(a.BaseDate AS DATETIME))\nFROM dbo.CalculateTimeSlots('hour', 1, '2015-04-01', 2) a;  -- 2=hourly time slots\/2 days<\/pre>\n<p> \tTo me that looks much easier on the eyes than what we did before! Even with the Timeslot1, TimeSlot2, TimeSlot3 variables which I included for illustration of you can use other columns returned by the FUNCTION. <\/p>\n<h2>Calculations vs. Lookups<\/h2>\n<p> &#8220;Since SQL is a database language, we prefer to do look ups and not calculations.&#8221; <br \/> \t&#8211; <a href=\"http:\/\/en.wikipedia.org\/wiki\/Joe_Celko\">Joe Celko<\/a> (prolific author of books on things SQL) <\/p>\n<p> \tAh yes, the on-going debate on whether we should do calculations or lookups has raised its ugly head yet again! <\/p>\n<p> \tIn that Calendar table link that I gave you earlier and also in another blog (<a href=\"http:\/\/dwaincsql.com\/2015\/03\/31\/an-easter-sql\/\">An Easter SQL<\/a>), we showed how you could do either calculations (if you prefer) or lookups if that&#8217;s where your preferences are. <\/p>\n<p> \tTime slot lookups pose a slightly more challenging conundrum than do holiday tables though. Let&#8217;s look at some of the ways we could construct them. <\/p>\n<h3>Hourly or By Every Fifteen Minutes (Separate Time Slot Tables)<\/h3>\n<p> \tWe could create a separate table for every time slot combination we need for the use-cases in our application. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">-- An hourly time slots table\nCREATE TABLE dbo.TimeSlots_Hourly\n(\n    TimeSlot    TIME PRIMARY KEY\n);\nGO\n\nINSERT INTO dbo.TimeSlots_Hourly (TimeSlot)\nSELECT TimeSlot\nFROM dbo.CalculateTimeSlots('hour', 1, NULL, NULL);\n\nSELECT * FROM dbo.TimeSlots_Hourly;\nGO\n\n-- A time slots table with one row for every 15 minutes\nCREATE TABLE dbo.TimeSlots_QuarterHourly\n(\n    TimeSlot    TIME PRIMARY KEY\n);\nGO\n\nINSERT INTO dbo.TimeSlots_QuarterHourly (TimeSlot)\nSELECT TimeSlot\nFROM dbo.CalculateTimeSlots('minute', 15, NULL, NULL);\n\nSELECT * FROM dbo.TimeSlots_QuarterHourly;<\/pre>\n<p> \tThat seems a little redundant to me but if it floats your boat, go for it! <\/p>\n<h3>Hourly or By Every Fifteen Minutes (A Single Time Slot Table)<\/h3>\n<p> \tAn alternative could be to create a generic time slot lookup that handles multiple time slot types, like this: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">-- Time slots table where SlotType determines type\nCREATE TABLE dbo.TimeSlots\n(\n    SlotType    VARCHAR(20)\n    ,TimeSlot   TIME\n    ,PRIMARY KEY (SlotType, TimeSlot)\n);\nGO\n\nWITH SlotTypes (LookupKey, SlotType, SlotIncrement) AS\n(\n    SELECT 'Hourly', 'hour', 1\n    UNION ALL SELECT 'QtrHourly', 'minute', 15\n)\nINSERT INTO dbo.TimeSlots (SlotType, TimeSlot)\nSELECT LookupKey, TimeSlot\nFROM SlotTypes\nCROSS APPLY dbo.CalculateTimeSlots(SlotType, SlotIncrement, NULL, NULL);\n\nSELECT * FROM dbo.TimeSlots;\nGO<\/pre>\n<p> \tThen it just becomes a matter of remembering all of the <code>SlotTypes<\/code> you defined. <\/p>\n<p> \tYou could of course use two time slot definition columns as part of the PRIMARY KEY, for example where they are identical to the arguments to the CalculateTimeSlots FUNCTION. We&#8217;ll let you decide if that is better than using the FUNCTION directly. <\/p>\n<h3>Various Time Slots in Different Columns of a Single Time Slots Table<\/h3>\n<p> \tAnother way perhaps, is something like the following where your table rows correspond to time slots by second (the PRIMARY KEY), and other necessary time slots are created as additional columns that are sparsely populated. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE TABLE dbo.TimeSlots\n(\n    TimeSlotsBySecond   TIME PRIMARY KEY\n    ,TimeSlotByMinute   TIME NULL\n    ,TimeSlotByQtrHour  TIME NULL\n    ,TimeSlotByHour     TIME NULL\n);\n\nCREATE NONCLUSTERED INDEX ts_ix1 ON dbo.TimeSlots (TimeSlotByMinute);\nCREATE NONCLUSTERED INDEX ts_ix2 ON dbo.TimeSlots (TimeSlotByQtrHour);\nCREATE NONCLUSTERED INDEX ts_ix3 ON dbo.TimeSlots (TimeSlotByHour);\nGO\n\n-- Columns after TimeSlotBySecond are sparsely populated\nINSERT INTO dbo.TimeSlots \n(\n    TimeSlotsBySecond, TimeSlotByMinute, TimeSlotByQtrHour, TimeSlotByHour\n)\nSELECT TimeSlotsBySecond=a.TimeSlot\n    ,TimeSlotByMinute=b.TimeSlot\n    ,TimeSlotByQtrHour=c.TimeSlot\n    ,TimeSlotByHour=d.TimeSlot\nFROM dbo.CalculateTimeSlots('second', 1, NULL, NULL) a\nLEFT JOIN dbo.CalculateTimeSlots('minute', 1, NULL, NULL) b \nON a.TimeSlot = b.TimeSlot\nLEFT JOIN dbo.CalculateTimeSlots('minute', 15, NULL, NULL) c \nON a.TimeSlot = c.TimeSlot\nLEFT JOIN dbo.CalculateTimeSlots('hour', 1, NULL, NULL) d \nON a.TimeSlot = d.TimeSlot\n\nSELECT *\nFROM dbo.TimeSlots;\n<\/pre>\n<p> \tNow I&#8217;m not saying I recommend this, but at least with the indexing defined when you do a query such as the following: <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT TimeSlot=TimeSlotByQtrHour\nFROM dbo.TimeSlots\nWHERE TimeSlotByQtrHour BETWEEN '08:00' AND '15:00';\n<\/pre>\n<p> \tYou will get a NONCLUSTERED INDEX SEEK in the execution plan. <\/p>\n<p> \t<img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2179-60321820-f40d-47bd-a725-983cd70b2e52.png\" alt=\"2179-60321820-f40d-47bd-a725-983cd70b2e5\" \/><\/p>\n<p> \tBut this does mean you&#8217;d have quite a few INDEXes! <\/p>\n<p> \tSo the choice is up to you: 1) calculate them on the fly with home grown code for each case, 2) use an iTVF to hide the magic of the time slot calculations or 3) choose one of the lookup table examples I&#8217;ve provided. <\/p>\n<h2>Using Time-slots: A Real World Example (Revisited)<\/h2>\n<p>All that is left now is to use our nifty new CalculateTimeSlots function to simplify our real world example.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">DECLARE @TimeSlotHrsDuration    SMALLINT = 6;     -- 6 hours\n \nSELECT TimeSlot     = [BaseDateWithTimeSlotStart]\n    ,a.MachineID\n    ,QtyofWidgets   = ISNULL(b.QtyOfWidgets, 0)\n    ,DailyQty       = ISNULL(SUM(b.QtyOfWidgets) OVER \n        (\n            PARTITION BY a.MachineID, [BaseDate]\n            ORDER BY [BaseDateWithTimeSlotStart]\n            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n        ), 0)\nFROM\n(\n    -- Create Cartesian product\n    SELECT b.MachineID, a.[BaseDate] \n        ,a.[BaseDateWithTimeSlotStart]\n        ,a.[BaseDateWithTimeSlotEnd]\n    -- Time slots of 6 hours\n    FROM dbo.CalculateTimeSlots('hour', @TimeSlotHrsDuration, '2015-04-01', 3) a\n    -- Unique MachineIDs\n    CROSS JOIN dbo.Machines b\n) a\nOUTER APPLY \n(\n    -- Sum the widgets produced\n    SELECT b.MachineID, QtyOfWidgets=SUM(b.QtyOfWidgets)\n    FROM dbo.MachineProduction b\n    WHERE a.MachineID = b.MachineID AND\n        -- where production time is within the time slot period\n        b.TimeProduced &gt;= a.[BaseDateWithTimeSlotStart] AND \n            b.TimeProduced &lt; a.[BaseDateWithTimeSlotEnd] \n    GROUP BY b.MachineID\n) b\nORDER BY MachineID, [BaseDateWithTimeSlotStart];<\/pre>\n<p> Notice how both CTEs have been eliminated, and we simplified the section containing the Cartesian product.  Obviously, additional local variables can be added or calculated to generate and pass the third and fourth parameters to our iTVF function call.<\/p>\n<p> This is but one of many real world examples where I&#8217;ve found it necessary to generate varying durations of time slots across a varying number of days.  Sometimes the time slots are by hours, other times by multiple (such as fifteen) minutes, etc.  So this is a generalized tool for generating those time slots when you need them.<\/p>\n<h2>Conclusions<\/h2>\n<p>We&#8217;ve described three different ways you can generate fixed duration time slots within a day. <\/p>\n<ul>\n<li>Generate them with ad-hoc (or home grown) code for the particular case.<\/li>\n<li>Use the <code>CalculateTimeSlots<\/code> FUNCTION, which allows you to generate the Cartesian product of days by time slots if you need to.<\/li>\n<li>Use a lookup table, for which you can vary the content according to your needs.<\/li>\n<\/ul>\n<p> \tWhile the debate will probably rage on, whether to use lookups vs. calculations, I think my preference in this case is going to be the second alternative (my <code>CalculateTimeSlots<\/code> FUNCTION). <\/p>\n<p> \tWe haven&#8217;t shown it but your time slots are easily combined with an existing Calendar table (dbo.Calendar) if you have one so that you can filter by date and\/or by time slot. <\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT TimeSlot, BaseDateWithTimeSlotStart, BaseDateWithTimeSlotEnd\nFROM dbo.Calendar a\nCROSS APPLY CalculateTimeSlots('hour', 3, a.[Date], 1) b\nWHERE a.[Date] BETWEEN '2015-01-01' AND '2015-01-05' AND\n    b.TimeSlot BETWEEN '06:00' AND '12:00';\n-- Results:\nTimeSlot          BaseDateWithTimeSlotStart  BaseDateWithTimeSlotEnd\n06:00:00.0000000  2015-01-01 06:00:00.000    2015-01-01 09:00:00.000\n09:00:00.0000000  2015-01-01 09:00:00.000    2015-01-01 12:00:00.000\n12:00:00.0000000  2015-01-01 12:00:00.000    2015-01-01 15:00:00.000\n06:00:00.0000000  2015-01-02 06:00:00.000    2015-01-02 09:00:00.000\n09:00:00.0000000  2015-01-02 09:00:00.000    2015-01-02 12:00:00.000\n12:00:00.0000000  2015-01-02 12:00:00.000    2015-01-02 15:00:00.000\n06:00:00.0000000  2015-01-03 06:00:00.000    2015-01-03 09:00:00.000\n09:00:00.0000000  2015-01-03 09:00:00.000    2015-01-03 12:00:00.000\n12:00:00.0000000  2015-01-03 12:00:00.000    2015-01-03 15:00:00.000\n06:00:00.0000000  2015-01-04 06:00:00.000    2015-01-04 09:00:00.000\n09:00:00.0000000  2015-01-04 09:00:00.000    2015-01-04 12:00:00.000\n12:00:00.0000000  2015-01-04 12:00:00.000    2015-01-04 15:00:00.000\n06:00:00.0000000  2015-01-05 06:00:00.000    2015-01-05 09:00:00.000\n09:00:00.0000000  2015-01-05 09:00:00.000    2015-01-05 12:00:00.000\n12:00:00.0000000  2015-01-05 12:00:00.000    2015-01-05 15:00:00.000\n\n<\/pre>\n<p> \tThanks for listening folks, and I hope that if you ever need to generate fixed duration time slots to support your application, you&#8217;ll find this article and embrace whichever technique suits your fancy. <\/p>\n<\/p><\/div>\n","protected":false},"excerpt":{"rendered":"<p>After answering many forum entries from developers asking for help with dealing with SQL that involved time intervals and ranges, Dwain dreamed of a generalized tool that  sets up time slots of various sorts without the need to experiment; that could do the heavy lifting, so that developers could do aggregations and reports based on time intervals without the hard graft. Here is Dwain&#8217;s dream made reality. &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":[4383,4150,4252,6032,6033],"coauthors":[],"class_list":["post-1987","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-cte","tag-sql","tag-t-sql-programming","tag-tally-tables","tag-time-slots"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1987","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=1987"}],"version-history":[{"count":3,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1987\/revisions"}],"predecessor-version":[{"id":92519,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1987\/revisions\/92519"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1987"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1987"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1987"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1987"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}