views:

323

answers:

5

I have the following query that runs in my Oracle database and I want to have the equivalent for a SQL Server 2008 database:

SELECT TRUNC( /* Midnight Sunday */
         NEXT_DAY(SYSDATE, 'SUN') - (7*LEVEL)
       ) AS week_start,
       TRUNC( /* 23:59:59 Saturday */
         NEXT_DAY(NEXT_DAY(SYSDATE, 'SUN') - (7*LEVEL), 'SAT') + 1
       ) - (1/(60*24)) + (59/(60*60*24)) AS week_end
FROM DUAL
CONNECT BY LEVEL <= 4 /* Get the past 4 weeks */

What the query does is get the start of the week and the end of the week for the last 4 weeks. The number of weeks is arbitrary and should be easily modified in the SQL Server query I want. It generates data like the following:

WEEK_START          WEEK_END
2010-03-07 00:00:00 2010-03-13 23:59:59
2010-02-28 00:00:00 2010-03-06 23:59:59
...

The part I'm currently stuck on translating is CONNECT BY LEVEL since it seems SQL Server 2008 doesn't have an equivalent. I would prefer to simply adjust a line like CONNECT BY LEVEL <= 4 and have the query generate more or fewer weeks (i.e., I don't want to have to adjust multiple UNION ALL statements).

Edit: here's what I have so far that gets the beginning and end of the current week:

   SELECT week_start,
          DATEADD(SECOND, -1, DATEADD(DAY, 7, week_start)) AS week_end
   FROM (
          SELECT CAST(
                   CONVERT(
                     VARCHAR(10),
                     DATEADD(DAY, 1-DATEPART(DW, GETDATE()), GETDATE()),
                     111
                   ) AS DATETIME
                 ) AS week_start
        ) AS week_start_view

I don't mind if the query shows the current week start and end date or if it starts at the previous week.

+1  A: 

All you need is a set:

;WITH cte(n) AS
(
    SELECT 0
    UNION ALL SELECT 1
    UNION ALL SELECT 2
    UNION ALL SELECT 3
)
  SELECT week_start,
          DATEADD(SECOND, -1, DATEADD(DAY, 7, week_start)) AS week_end
   FROM (
          SELECT CAST(
                   CONVERT(
                     VARCHAR(10),
                     DATEADD(WEEK, -n, DATEADD(DAY, 1-DATEPART(DW, GETDATE()), GETDATE())),
                     111
                   ) AS DATETIME
                 ) AS week_start
                           FROM cte
        ) AS week_start_view;

However I will caution you that if your data is datetime and you are going to use these boundaries for query ranges, you should use an open-ended range, e.g. >= 03/07 and < 03/14. This way you don't miss out any rows that happened between 23:59:59 and midnight; as rare as they may be, they could be important.

Aaron Bertrand
Also if you already have a Numbers table, you could simply replace the CTE and FROM with "FROM dbo.Numbers WHERE Number BETWEEN 1 AND 4"
Aaron Bertrand
I like this. I bet the `WITH` could be changed to be more like my `level_view` (see http://stackoverflow.com/questions/2404084/help-translate-this-week-query-from-oracle-pl-sql-to-sql-server-2008/2404501#2404501) and then I could specify a number instead of doing multiple unions.
Sarah Vessels
The UNION is pretty simple to throw together when the number of rows is this small, and a Numbers table is much easier than deriving a row number from an existing table and relying on the fact that it will always have enough rows to satisfy your query. A lot of people are against an auxiliary table (not sure why); personally I think they are very useful. YMMV.
Aaron Bertrand
I wrote a bit about the numbers table here: http://sqlblog.com/blogs/aaron_bertrand/archive/2009/10/07/bad-habits-to-kick-using-a-loop-to-populate-a-table.aspx and a long time ago here: http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-numbers-table.html
Aaron Bertrand
A: 

Here's what I came up with. It's slightly janky in that it relies on at least one of my tables having rows >= the number of weeks I want to select. I could adjust it slightly to include the current week.

   SELECT week_start,
          DATEADD(SECOND, -1, DATEADD(DAY, 7, week_start)) AS week_end
   FROM (
          SELECT CAST(
                   CONVERT(
                     VARCHAR(10),
                     DATEADD(
                       DAY,
                       1-DATEPART(DW, GETDATE()),
                       DATEADD(DAY, -7*level, GETDATE())
                     ),
                     111
                   ) AS DATETIME
                 ) AS week_start
          FROM (
                 SELECT level
                 FROM (
                        SELECT ROW_NUMBER() OVER(ORDER BY RAND()) AS level
                        FROM my_table_with_at_least_21_rows
                      ) AS all_rows_view
                 WHERE level <= 21
               ) AS level_view
        ) AS week_start_view

This gets the past 21 weeks, starting at last week. Here's sample data:

week_start                  week_end
2010-02-28 00:00:00.000     2010-03-06 23:59:59.000
2010-02-21 00:00:00.000     2010-02-27 23:59:59.000
2010-02-14 00:00:00.000     2010-02-20 23:59:59.000
2010-02-07 00:00:00.000     2010-02-13 23:59:59.000
...
Sarah Vessels
1) how did "4" become "21" in level comparison? 2) have a look at the master..spt_values table
devio
I clarified my question later to say that I wanted to be able to adjust the number of weeks generated. Sorry, I should also have phrased my question to say that the number of weeks generated is arbitrary and should be easily modified.
Sarah Vessels
A: 

based on your code:

SELECT DATEADD(day, weeks.week * -7, week_start) AS week_start,
       DATEADD(SECOND, -1, DATEADD(DAY, (weeks.week-1) * -7, week_start)) AS week_end
FROM (
      SELECT CAST(
               CONVERT(
                 VARCHAR(10),
                 DATEADD(DAY, 1-DATEPART(DW, GETDATE()), GETDATE()),
                 111
               ) AS DATETIME
             ) AS week_start
     ) AS week_start_view,
     ( SELECT 0 AS week UNION SELECT 1 UNION SELECT 2 UNION SELECT 3) AS weeks
devio
+2  A: 

Use (but don't forget to vote for Sarah):

WITH dates AS (
    SELECT DATEADD(DD, 
                   1 - DATEPART(DW, CONVERT(VARCHAR(10), starting_date, 111)), 
                   CONVERT(VARCHAR(10), starting_date, 111)
                   ) AS midnight
      FROM (SELECT DATEADD(WEEK, -3, GETDATE()) AS starting_date) AS starting_date_view
    UNION ALL
    SELECT DATEADD(DD, 7, midnight)
      FROM dates
     WHERE DATEADD(DD, 7, midnight) < GETDATE()) 
SELECT midnight AS week_start,
       DATEADD(SS, -1, DATEADD(DAY, 7, midnight)) AS week_end
  FROM dates

Previously:

  1. Set the first day of the week to be Sunday with the SET DATEFIRST command:

    SET DATEFIRST 7
    
  2. The SQL Server 2005+ equivalent to Oracle's CONNECT BY LEVEL is the recursive CTE (ANSI standard btw). Use:

    WITH dates AS (
       SELECT DATEADD(DD, 
                      1 - DATEPART(DW, CONVERT(VARCHAR(10), GETDATE(), 111)), 
                      CONVERT(VARCHAR(10), GETDATE(), 111)) AS date
       UNION ALL
       SELECT DATEADD(dd, 7, d.date)
         FROM dates d
        WHERE DATEADD(dd, 7, d.date) <= DATEADD(dd, 4*7, GETDATE()))
    SELECT t.date AS week_start,
           DATEADD(ss, -1, DATEADD(DAY, 7, t.date)) AS week_end
      FROM dates t
    

See this link for explaining how to get the first day of the week. To alter the number of weeks, change the DATEADD(dd, 4*7, GETDATE()), where 4 represents the number of weeks you want generated.

OMG Ponies
I like that yours is simpler than my solution and that it still has a single point where I can change how many weeks are generated. I don't know if I'll have access to modify the first day of the week, though.
Sarah Vessels
Also, I didn't know what CTE was, but a quick Google search found this: Common Table Expressions http://msdn.microsoft.com/en-us/library/ms186243.aspx
Sarah Vessels
Wait, just noticed: what is the 'dates' table in your query? Is that built into SQL Server 2008, kind of like Oracle's `DUAL` table?
Sarah Vessels
@Sarah Vessels: SQL Server doesn't need `FROM DUAL` for an equivalent query. Common Table Expression (CTE) is SQL Server terminology for the `WITH` syntax, supported by Oracle 9i+ - Oracle calls it Subquery Factoring.
OMG Ponies
@OMG Ponies: While Oracle has had the `WITH` clause for awhile, Oracle did not support recursion in the `WITH` clause until recently, 11g R2 I believe.
Shannon Severance
@Sarah: The dates relation is being created by the `WITH` clause. It is nothing like dual.
Shannon Severance
@Shannon: der, thanks. That'll teach me to start reading from the middle of a query...
Sarah Vessels
@OMG Ponies: just got a chance to run your query. Your `week_end` is off: should be `DATEADD(SS, -1, DATEADD(DAY, 7, t.date)) AS week_end`. Also, the query appears to be going into the future (going up into 2011 if I increase the number of weeks) instead of the past.
Sarah Vessels
@Sarah Vessels: Thanks, corrected. Reversing the date calculation is easy - add the `-` to the number specified in the `DATEADD` function. Try the query now.
OMG Ponies
@OMG Ponies: I started working with your query and tried that, but making that a negative number results in only a single week--the current one--being shown. Also, running your updated query still shows an incorrect end date: `2010-03-07 00:00:00.000 2010-02-27 23:59:59.000`. I got your query working, though: http://stackoverflow.com/questions/2404084/help-translate-this-week-query-from-oracle-pl-sql-to-sql-server-2008/2420377#2420377
Sarah Vessels
+3  A: 

I modified OMG Ponies' answer because it looked like a good idea, it just had the wrong week_end value on each week and also showed future weeks instead of past weeks. I came up with the following:

WITH dates AS (
  SELECT DATEADD(
           DD, 
           1 - DATEPART(DW, CONVERT(VARCHAR(10), starting_date, 111)), 
           CONVERT(VARCHAR(10), starting_date, 111)
         ) AS midnight
  FROM (
         SELECT DATEADD(WEEK, -3, GETDATE()) AS starting_date
       ) AS starting_date_view

  UNION ALL

  SELECT DATEADD(DD, 7, midnight)
  FROM dates
  WHERE DATEADD(DD, 7, midnight) < GETDATE()
) SELECT midnight AS week_start,
         DATEADD(SS, -1, DATEADD(DAY, 7, midnight)) AS week_end
  FROM dates

It generates the past 4 weeks:

week_start                 week_end
2010-02-14 00:00:00.000    2010-02-20 23:59:59.000
2010-02-21 00:00:00.000    2010-02-27 23:59:59.000
2010-02-28 00:00:00.000    2010-03-06 23:59:59.000
2010-03-07 00:00:00.000    2010-03-13 23:59:59.000

This is better than my previous answer, I think, because it does not rely on another table having a particular number of rows. The number of weeks generated can be changed by altering only one digit: the 3 in SELECT DATEADD(WEEK, -3, GETDATE()) AS starting_date. The current week is included, and that digit represents how many additional weeks prior to the current week should be shown.

Iterate over Past Weeks up to Now, Excluding Current Week

Update: and here's a version that excludes the current week:

WITH dates AS (
  SELECT DATEADD(
           DD,
           1 - DATEPART(DW, CONVERT(VARCHAR(10), starting_date, 111)), 
           CONVERT(VARCHAR(10), starting_date, 111)
         ) AS midnight_sunday
  FROM (
         SELECT DATEADD(WEEK, -4, GETDATE()) AS starting_date
       ) AS starting_date_view

  UNION ALL

  SELECT DATEADD(DD, 7, midnight_sunday)
  FROM dates
  WHERE DATEADD(DD, 7, midnight_sunday) <
        DATEADD(
          DD,
          1 - DATEPART(DW, CONVERT(VARCHAR(10), GETDATE(), 111)), 
          CONVERT(VARCHAR(10), GETDATE(), 111)
        )
) SELECT midnight_sunday AS week_start,
         DATEADD(SS, -1, DATEADD(DAY, 7, midnight_sunday)) AS week_end
  FROM dates

Its results:

week_start                 week_end
2010-02-07 00:00:00.000    2010-02-13 23:59:59.000
2010-02-14 00:00:00.000    2010-02-20 23:59:59.000
2010-02-21 00:00:00.000    2010-02-27 23:59:59.000
2010-02-28 00:00:00.000    2010-03-06 23:59:59.000

Iterate over Past Months up to Now

I later found a need for a monthly version of this query. Here is that modification:

WITH dates AS (
  SELECT CAST(
        FLOOR(CAST(starting_date AS DECIMAL(12, 5))) -
        (DAY(starting_date) - 1) AS DATETIME
      ) AS month_start
  FROM (
         SELECT DATEADD(MONTH, -3, GETDATE()) AS starting_date
       ) AS starting_date_view

  UNION ALL

  SELECT DATEADD(MONTH, 1, month_start)
  FROM dates
  WHERE DATEADD(MONTH, 1, month_start) < GETDATE()
) SELECT month_start,
         DATEADD(SS, -1, DATEADD(MONTH, 1, month_start)) AS month_end
  FROM dates
  ORDER BY month_start DESC
Sarah Vessels
+1: Kudos! I'll update mine so I'm not incorrect.
OMG Ponies