generate days from date range

后端 未结 29 2489
渐次进展
渐次进展 2020-11-21 05:19

I would like to run a query like

select ... as days where `date` is between \'2010-01-20\' and \'2010-01-24\'

And return data like:

29条回答
  •  再見小時候
    2020-11-21 06:19

    As stated (or at least alluded to) in many of the wonderful answers already given, this problem is easily solved once you have a set of numbers to work with.

    Note: The following is T-SQL but it's simply my particular implementation of general concepts already mentioned here and on the internet at large. It should be relatively simple to convert the code to your dialect of choice.

    How? Consider this query:

    SELECT DATEADD(d, N, '0001-01-22')
    FROM Numbers -- A table containing the numbers 0 through N
    WHERE N <= 5;
    

    The above produces the date range 1/22/0001 - 1/27/0001 and is extremely trivial. There are 2 key pieces of information in the above query: the start date of 0001-01-22 and the offset of 5. If we combine these two pieces of information then we obviously have our end date. Thus, given two dates, generating a range can be broken down like so:

    • Find the difference between two given dates (the offset), easy:

      -- Returns 125 SELECT ABS(DATEDIFF(d, '2014-08-22', '2014-12-25'))

      Using ABS() here ensures that the date order is irrelevant.

    • Generate a limited set of numbers, also easy:

      -- Returns the numbers 0-2 SELECT N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1 FROM(SELECT 'A' AS S UNION ALL SELECT 'A' UNION ALL SELECT 'A')

      Notice we don't actually care what we're selecting FROM here. We just need a set to work with so that we count the number of rows in it. I personally use a TVF, some use a CTE, others use a numbers table instead, you get the idea. I advocate for using the most performant solution that you also understand.

    Combining these two methods will solve our problem:

    DECLARE @date1 DATE = '9001-11-21';
    DECLARE @date2 DATE = '9001-11-23';
    
    SELECT D = DATEADD(d, N, @date1)
    FROM (
        SELECT N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1
        FROM (SELECT 'A' AS S UNION ALL SELECT 'A' UNION ALL SELECT 'A') S
    ) Numbers
    WHERE N <= ABS(DATEDIFF(d, @date1, @date2));
    

    The above example is horrible code but demonstrates how everything comes together.

    More Fun

    I need to do this kind of thing a lot so I encapsulated the logic into two TVFs. The first generates a range of numbers and the second uses this functionality to generate a range of dates. The math is to ensure that input order doesn't matter and because I wanted to use the full range of numbers available in GenerateRangeSmallInt.

    The following function takes ~16ms of CPU time to return the maximum range of 65536 dates.

    CREATE FUNCTION dbo.GenerateRangeDate (   
        @date1 DATE,   
        @date2 DATE   
    )   
    RETURNS TABLE
    WITH SCHEMABINDING   
    AS   
    RETURN (
        SELECT D = DATEADD(d, N + 32768, CASE WHEN @date1 <= @date2 THEN @date1 ELSE @date2 END)
        FROM dbo.GenerateRangeSmallInt(-32768, ABS(DATEDIFF(d, @date1, @date2)) - 32768)
    );
    
    GO
    
    CREATE FUNCTION dbo.GenerateRangeSmallInt (
        @num1 SMALLINT = -32768
      , @num2 SMALLINT = 32767
    )
    RETURNS TABLE
    WITH SCHEMABINDING
    AS
    RETURN (
        WITH Numbers(N) AS (
            SELECT N FROM(VALUES
                (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 16
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 32
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 48
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 64
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 80
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 96
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 112
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 128
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 144
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 160
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 176
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 192
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 208
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 224
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 240
              , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 256
            ) V (N)
        )
        SELECT TOP(ABS(CAST(@num1 AS INT) - CAST(@num2 AS INT)) + 1)
               N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) + CASE WHEN @num1 <= @num2 THEN @num1 ELSE @num2 END - 1
        FROM Numbers A
           , Numbers B
    );
    

提交回复
热议问题