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


70

我有一个包含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天中的每一分。我希望我现在更加清楚。


1
是的,有可能,但是为什么要这么做呢?
NullUserException

1
我还是不明白。如果可以用绘制图形的方式填补这些空白,并且可以节省一些开销,则不要从数据库中获取不必要的数据。
NullUserException

3
但是然后我必须为USERID选择数据,例如获取20行日期并计分,然后我必须循环使用服务器端语言(ASP)来检查30天之前是否有日期,如果不是make 0 else使数据库值...这不是从数据库30个值中获取并仅构造字符串更消耗吗?
2010年

Answers:


58

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. 根据时间部分将联接左移到数据表中:

       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`
    

如果要维护日期格式,请使用DATE_FORMAT函数

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

2
谢谢。您是否建议使用此方法进行快速操作,以免使用这种方法并进行服务器端计算?
Jerry2

5
@ Jerry2:我的首选是在数据库中进行尽可能多的数据处理,但缺少真正涉及的演示材料。我不羡慕在应用程序代码中执行此操作,只要它是一次数据库访问即可...
OMG Ponies 2010年

1
为了使用索引,条件(WHERE和ON子句)可以重写为WHERE n.id < DATEDIFF('2010-06-14', '2010-06-06')LEFT JOIN TABLE y ON y.date = DATE_FORMAT(x.ts, '%d.%m.%Y')
Paul Spiegel

1
例如WHERE 'y'.'score' = 2,一旦我添加WHERE子句,所有填写的日期就不再显示
Seba M

21

我不喜欢其他答案,因此需要创建表。该查询在没有帮助程序表的情况下可以高效地完成它。

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;

因此,让我们对此进行剖析。

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

if将检测没有得分的天并将其设置为0。b.Days是您选择从当前日期获取的已配置天数,最多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

这个子查询是我在stackoverflow上看到的。它可以有效地生成从当前日期开始的过去1000天的列表。最后的WHERE子句中的间隔(当前为30)确定返回的天数。最大值为1000。可以轻松修改此查询以返回100多年的日期,但是1000对于大多数情况而言应该是好的。

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

这是将包含分数的表带入其中的部分。您可以与日期生成器查询中选择的日期范围进行比较,以便在需要的地方填写0(分数将设置为NULL初始值,因为它是LEFT JOIN;;这在select语句中是固定的)。我也按日期排序,仅是因为。这是首选项,您也可以按分数排序。

在此之前,ORDER BY您可以轻松地加入有关您在编辑中提到的用户信息的表,以添加最后一项要求。

我希望此版本的查询对某人有所帮助。谢谢阅读。


16

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


1
是的,但是数字表更灵活-请参见我的答案作为示例。IE:如果现在您也需要序列号怎么办?您是否需要每种数据类型的表?
OMG小马

2
需要序号将是另一种用例;-)如果必须针对不同的DBMS(即Oracle,MySQL,SQL-Server),则您的方法将需要稍微修改的语句,并且我怀疑DATE_ADD方法比日历表要慢(但我认为这与这里无关)
Soundlink 2010年


6
http://www.media-division.com/using-mysql-generate-daily-sales-reports-filled-gaps/上有一个创建日历表的简便方法。而且,尽管如上所述,@ omg-ponies,数字技巧几乎与拥有日历表一样快,但有时使用时髦的技巧可能会产生误导。特别是如果您希望其他开发人员将来维护您的代码。
obaranovsky '16

1
与数字表解决方案相比,日历表可以让您编写简单的查询,例如SELECT c.date, COALESCE(y.score, 0) AS cnt FROM calendar c LEFT JOIN y ON y.date = c.date WHERE c.date BETWEEN '2010-06-06' AND '2010-06-14'
Paul Spiegel

10

自问这个问题以来,时间流逝。MySQL 8.0于2018年发布,并增加了对递归通用表表达式的支持,它们提供了一种优雅,最新的方式来解决此问题。

以下查询可用于生成日期列表,例如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

DB Fiddle上的演示

日期| 得分
:--------- | ----:
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

3
谢谢!能够轻松地对此进行修改,并且只需几分钟即可使用!
乔纳森·费舍尔

4

迈克尔·科纳德(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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.