MySQL how to fill missing dates in range?

前端 未结 5 671
离开以前
离开以前 2020-11-21 06:47

I have a table with 2 columns, date and score. It has at most 30 entries, for each of the last 30 days one.

date      score
-----------------
1.8.2010  19
2         


        
相关标签:
5条回答
  • 2020-11-21 07:02

    I'm not a fan of the other answers, requiring tables to be created and such. This query does it efficiently without helper tables.

    SELECT 
        IF(score IS NULL, 0, score) AS score,
        b.Days AS date
    FROM 
        (SELECT a.Days 
        FROM (
            SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS Days
            FROM       (SELECT 0 AS a 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
            CROSS JOIN (SELECT 0 AS a 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
            CROSS JOIN (SELECT 0 AS a 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
        ) a
        WHERE a.Days >= curdate() - INTERVAL 30 DAY) b
    LEFT JOIN your_table
        ON date = b.Days
    ORDER BY b.Days;
    

    So lets dissect this.

    SELECT 
        IF(score IS NULL, 0, score) AS score,
        b.Days AS date
    

    The if will detect days that had no score and set them to 0. b.Days is the configured amount of days you chose to get from the current date, up to 1000.

        (SELECT a.Days 
        FROM (
            SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS Days
            FROM       (SELECT 0 AS a 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
            CROSS JOIN (SELECT 0 AS a 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
            CROSS JOIN (SELECT 0 AS a 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
        ) a
        WHERE a.Days >= curdate() - INTERVAL 30 DAY) b
    

    This subquery is something I saw on stackoverflow. It efficiently generates a list of the past 1000 days from the current date. The interval (currently 30) in the WHERE clause at the end determines which days are returned; the maximum is 1000. This query could be easily modified to return 100s of years worth of dates, but 1000 should be good for most things.

    LEFT JOIN your_table
        ON date = b.Days
    ORDER BY b.Days;
    

    This is the part that brings your table that contains the score into it. You compare to the selected date range from the date generator query to be able to fill in 0s where needed (the score will be set to NULL initially, because it is a LEFT JOIN; this is fixed in the select statement). I also order it by the dates, just because. This is preference, you could also order by score.

    Before the ORDER BY you could easily join with your table about user info you mentioned with your edit, to add that last requirement.

    I hope this version of the query helps someone. Thanks for reading.

    0 讨论(0)
  • 2020-11-21 07:04

    You can accomplish this by using a Calendar Table. That's a table which you create once and fill with a date range (e.g. one dataset for each day 2000-2050; that depends on your data). Then you can make an outer join of your table against the calendar table. If a date is missing in your table, you return 0 for the score.

    0 讨论(0)
  • 2020-11-21 07:11

    Time went by since this question was asked. MySQL 8.0 was released in 2018 and added support for recursive common table expressions, which provide an elegant, state-of-the-art way to solve this question.

    The following query can be used to generate a list of dates, say for the first 15 days of August 2010:

    with recursive all_dates(dt) as (
        -- anchor
        select '2010-08-01' dt
            union all 
        -- recursion with stop condition
        select dt + interval 1 day from all_dates where dt + interval 1 day <= '2010-08-15'
    )
    select * from all_dates
    

    You can then left join this resultset with your table to generate the expected output:

    with recursive all_dates(dt) as (
        -- anchor
        select '2010-08-01' dt
            union all 
        -- recursion with stop condition
        select dt + interval 1 day from all_dates where dt + interval 1 day <= '2010-08-15'
    )
    select d.dt date, coalesce(t.score, 0) score
    from all_dates d
    left join mytable t on t.date = d.dt
    order by d.dt
    

    Demo on DB Fiddle:

    date       | score
    :--------- | ----:
    2010-08-01 |    19
    2010-08-02 |    21
    2010-08-03 |     0
    2010-08-04 |    14
    2010-08-05 |     0
    2010-08-06 |     0
    2010-08-07 |    10
    2010-08-08 |     0
    2010-08-09 |     0
    2010-08-10 |    14
    2010-08-11 |     0
    2010-08-12 |     0
    2010-08-13 |     0
    2010-08-14 |     0
    2010-08-15 |     0
    
    0 讨论(0)
  • 2020-11-21 07:26

    Michael Conard answer is great but I needed intervals of 15 minutes where the time must always start at the top of every 15th minute:

    SELECT a.Days 
    FROM (
        SELECT FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60)) - INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE AS Days
        FROM       (SELECT 0 AS a 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
        CROSS JOIN (SELECT 0 AS a 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
        CROSS JOIN (SELECT 0 AS a 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
    ) a
    WHERE a.Days >= curdate() - INTERVAL 30 DAY
    

    This will set the current time to the previous round 15th minute:

    FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60))
    

    And this will remove time with a 15 minute step:

    - INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE
    

    If there's a simpler way to do it, please let me know.

    0 讨论(0)
  • 2020-11-21 07:27

    MySQL doesn't have recursive functionality, so you're left with using the NUMBERS table trick -

    1. Create a table that only holds incrementing numbers - easy to do using an auto_increment:

      DROP TABLE IF EXISTS `example`.`numbers`;
      CREATE TABLE  `example`.`numbers` (
        `id` int(10) unsigned NOT NULL auto_increment,
         PRIMARY KEY  (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
      
    2. Populate the table using:

      INSERT INTO `example`.`numbers`
        ( `id` )
      VALUES
        ( NULL )
      

      ...for as many values as you need.

    3. Use DATE_ADD to construct a list of dates, increasing the days based on the NUMBERS.id value. Replace "2010-06-06" and "2010-06-14" with your respective start and end dates (but use the same format, YYYY-MM-DD) -

      SELECT `x`.*
        FROM (SELECT DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY)
                FROM `numbers` `n`
               WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` -1 DAY) <= '2010-06-14' ) x
      
    4. LEFT JOIN onto your table of data based on the time portion:

         SELECT `x`.`ts` AS `timestamp`,
                COALESCE(`y`.`score`, 0) AS `cnt`
           FROM (SELECT DATE_FORMAT(DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY), '%m/%d/%Y') AS `ts`
                   FROM `numbers` `n`
                  WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY) <= '2010-06-14') x
      LEFT JOIN TABLE `y` ON STR_TO_DATE(`y`.`date`, '%d.%m.%Y') = `x`.`ts`
      

    If you want to maintain the date format, use the DATE_FORMAT function:

    DATE_FORMAT(`x`.`ts`, '%d.%m.%Y') AS `timestamp`
    
    0 讨论(0)
提交回复
热议问题