generate days from date range

后端 未结 29 2475
渐次进展
渐次进展 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:17

    It's a good idea with generating these dates on the fly. However, I do not feel myself comfortable to do this with quite large range so I've ended up with the following solution:

    1. Created a table "DatesNumbers" that will hold numbers used for dates calculation:
    CREATE TABLE DatesNumbers (
        i MEDIUMINT NOT NULL,
        PRIMARY KEY (i)
    )
    COMMENT='Used by Dates view'
    ;
    
    1. Populated the table using above techniques with numbers from -59999 to 40000. This range will give me dates from 59999 days (~164 years) behind to 40000 days (109 years) ahead:
    INSERT INTO DatesNumbers
    SELECT 
        a.i + (10 * b.i) + (100 * c.i) + (1000 * d.i) + (10000 * e.i) - 59999 AS i
    FROM 
      (SELECT 0 AS i UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS a,
      (SELECT 0 AS i UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS b,
      (SELECT 0 AS i UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS c,
      (SELECT 0 AS i UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS d,
      (SELECT 0 AS i UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS e
    ;
    
    1. Created a view "Dates":
    SELECT
          i,
          CURRENT_DATE() + INTERVAL i DAY AS Date
    FROM
        DatesNumbers
    

    That's it.

    • (+) Easy to read queries
    • (+) No on the fly numbers generations
    • (+) Gives dates in the past and in the future and there is NO UNION in view for this as in this post.
    • (+) "In the past only" or "in the future only" dates could be filtered using WHERE i < 0 or WHERE i > 0 (PK)
    • (-) 'temporary' table & view is used
    0 讨论(0)
  • 2020-11-21 06:18
    WITH
      Digits AS (SELECT 0 D UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9),
      Dates AS (SELECT affffdate('1970-01-01',t4.d*10000 + t3.d*1000 + t2.d*100 + t1.d*10 +t0.d) AS date FROM Digits AS t0, Digits AS t1, Digits AS t2, Digits AS t3, Digits AS t4)
    SELECT * FROM Dates WHERE date BETWEEN '2017-01-01' AND '2017-12-31'
    
    0 讨论(0)
  • 2020-11-21 06:18

    One more solution for mysql 8.0.1 and mariadb 10.2.2 using recursive common table expressions:

    with recursive dates as (
        select '2010-01-20' as date
        union all
        select date + interval 1 day from dates where date < '2010-01-24'
    )
    select * from dates;
    
    0 讨论(0)
  • 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
    );
    
    0 讨论(0)
  • 2020-11-21 06:20

    Accepted answer didn't work for PostgreSQL (syntax error at or near "a").

    The way you do this in PostgreSQL is by using generate_series function, i.e.:

    SELECT day::date
    FROM generate_series('2010-01-20', '2010-01-24', INTERVAL '1 day') day;
    
        day
    ------------
     2010-01-20
     2010-01-21
     2010-01-22
     2010-01-23
     2010-01-24
    (5 rows)
    
    0 讨论(0)
提交回复
热议问题