MySQL如何填写范围内的缺失日期?

时间:2010-08-21 20:30:23

标签: mysql sql gaps-and-islands

我有一个包含2列,日期和分数的表格。它最多有30个条目,过去30天每个条目一个。

date      score
-----------------
1.8.2010  19
2.8.2010  21
4.8.2010  14
7.8.2010  10
10.8.2010 14

我的问题是缺少某些日期 - 我想看看:

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
...

单个查询需要的是:19,21,9,14,0,0,10,0,0,14 ......这意味着缺少的日期用0填充。

我知道如何获取所有值并在服务器端语言中迭代日期并错过空白。但这是否可以在mysql中进行,因此我按日期对结果进行排序并获取缺失的部分。

编辑:在此表中有另一列名为UserID,因此我有30.000个用户,其中一些在此表中有分数。如果日期<我删除了每天的日期。 30天前,因为我需要每个用户最近30天的分数。原因是我正在制作过去30天内用户活动的图表,并绘制图表我需要用逗号分隔的30个值。所以我可以在查询中告诉我USERID = 10203活动,查询会得到30分,过去30天每一分。我希望我现在更清楚了。

6 个答案:

答案 0 :(得分:55)

MySQL没有递归功能,因此您只需使用NUMBERS表技巧 -

  1. 创建一个只保存递增数字的表 - 使用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. 使用以下方法填充表格:

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

    ...根据需要提供尽可能多的价值。

  3. 使用DATE_ADD构建日期列表,根据NUMBERS.id值增加日期。将“2010-06-06”和“2010-06-14”替换为您的相应开始日期和结束日期(但使用相同格式,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到您的数据表:

       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`
    
  5. 如果您想维护日期格式,请使用DATE_FORMAT function

    DATE_FORMAT(`x`.`ts`, '%d.%m.%Y') AS `timestamp`
    

答案 1 :(得分:14)

您可以使用日历表来完成此操作。这是一个您创建一次并填充日期范围的表格(例如,2000-2050每天的一个数据集;这取决于您的数据)。然后,您可以根据日历表创建表的外部联接。如果您的表中缺少日期,则返回0作为分数。

答案 2 :(得分:8)

我不是其他答案的粉丝,需要创建表格等等。没有帮助表,这个查询可以有效地完成它。

%macro FILENAME (I_FROM=1, I_TO=&I_FROM, J_FROM=1, J_TO=&J_FROM);

 %local I J;

 %do I = &I_FROM %to &I_TO;
 %do J = &J_FROM %to &J_TO;

 filename inf_&I._&J. 'C:\Users\Main_&I.\final_complete_&I.&J.csv';

%end;
%end;

%mend;
%FILENAME(J_TO=2); */

所以我们要解析一下。

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;

if将检测没有得分的天数并将其设置为0. b.Days是您选择从当前日期获得的天数,最多为1000天。

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

这个子查询是我在stackoverflow上看到的。它可以有效地生成当前日期过去1000天的列表。最后WHERE子句中的间隔(当前为30)确定返回哪些天数;最大值为1000.此查询可以很容易地修改为返回100年的日期值,但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

这是将包含分数的表格带入其中的部分。您可以与日期生成器查询中的所选日期范围进行比较,以便能够在需要时填写0(分数最初将设置为LEFT JOIN your_table ON date = b.Days ORDER BY b.Days; ,因为它是NULL;这在选择声明)。我也按日期订购,只是因为。这是首选,您也可以按分数订购。

LEFT JOIN之前,您可以轻松加入关于编辑中提到的用户信息的表格,以添加最后一项要求。

我希望此版本的查询可以帮助某人。谢谢你的阅读。

答案 3 :(得分:0)

迈克尔·康纳德(Michael Conard)的回答很好,但我需要间隔15分钟,而时间必须始终在每15分钟的顶部开始:

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

这会将当前时间设置为前15分钟:

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

这将节省15分钟的时间:

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

如果有更简单的方法,请告诉我。

答案 4 :(得分:0)

自从提出这个问题以来,时间流逝。 MySQL 8.0于2018年发布,并增加了对recursive common table expressions的支持,它提供了一种优雅,先进的方法来解决此问题。

以下查询可用于生成日期列表,例如2010年8月的前15天:

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

然后您可以将此left join与表一起使用以生成预期的输出:

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

答案 5 :(得分:0)

您可以通过插入直接从开始日期到今天使用

        with recursive all_dates(dt) as (
        -- anchor
        select '2021-01-01' dt
            union all 
        -- recursion with stop condition
        INSERT IGNORE  INTO mytable (date,score) VALUES (dt + interval 1 day ,0 )  where dt + interval 1 day <= curdate()
    )
    select * from all_dates