Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MySQL how to fill missing dates in range?

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.8.2010  21 4.8.2010  14 7.8.2010  10 10.8.2010 14 

My problem is that some dates are missing - I want to see:

date      score ----------------- 1.8.2010  19 2.8.2010  21 3.8.2010  0 4.8.2010  14 5.8.2010  0 6.8.2010  0 7.8.2010  10 ... 

What I need from the single query is to get: 19,21,9,14,0,0,10,0,0,14... That means that the missing dates are filled with 0.

I know how to get all the values and in server side language iterating through dates and missing the blanks. But is this possible to do in mysql, so that I sort the result by date and get the missing pieces.

EDIT: In this table there is another column named UserID, so I have 30.000 users and some of them have the score in this table. I delete the dates every day if date < 30 days ago because I need last 30 days score for each user. The reason is I am making a graph of the user activity over the last 30 days and to plot a chart I need the 30 values separated by comma. So I can say in query get me the USERID=10203 activity and the query would get me the 30 scores, one for each of the last 30 days. I hope I am more clear now.

like image 973
Jerry2 Avatar asked Aug 21 '10 20:08

Jerry2


People also ask

How can I get missing date between two dates in MySQL?

SELECT DATE(r1. reportdate) + INTERVAL 1 DAY AS missing_date FROM Reports r1 LEFT OUTER JOIN Reports r2 ON DATE(r1. reportdate) = DATE(r2. reportdate) - INTERVAL 1 DAY WHERE r1.

How do I auto populate a date in MySQL?

You can use now() with default auto fill and current date and time for this. Later, you can extract the date part using date() function. Let us set the default value with some date.

How do I select a date range in MySQL?

MySQL retrieves and displays DATETIME values in ' YYYY-MM-DD hh:mm:ss ' format. The supported range is '1000-01-01 00:00:00' to '9999-12-31 23:59:59' . The TIMESTAMP data type is used for values that contain both date and time parts. TIMESTAMP has a range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC.


2 Answers

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` 
like image 96
OMG Ponies Avatar answered Oct 10 '22 03:10

OMG Ponies


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.

like image 27
Michael Conard Avatar answered Oct 10 '22 02:10

Michael Conard