【问题标题】:MySQL how to fill missing dates in range?MySQL如何填充范围内的缺失日期?
【发布时间】:2011-04-02 02:49:44
【问题描述】:

我有一个包含 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 个用户,其中一些用户在这个表中得分。如果日期

【问题讨论】:

  • 是的,有可能,但是为什么你会这样做?
  • 我还是不明白。如果您可以用绘制图表的任何内容来填补这些空白,请不要从数据库中获取不必要的数据,这样可以节省一些开销。
  • 但是我必须选择 USERID 的数据,例如我得到 20 行日期并得分,然后我必须循环使用我的服务器端语言 (ASP) 来检查是否有日期30 天前,如果不是 make 0 ,否则将数据库值...这不是从数据库中获取 30 个值并构造字符串更消耗吗?

标签: mysql sql recursive-query gaps-and-islands date-arithmetic


【解决方案1】:

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

        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

【讨论】:

    【解决方案2】:

    自从提出这个问题以来,时间已经过去了。 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

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

    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】:

      我不喜欢其他答案,要求创建表格等。此查询无需辅助表即可高效完成。

      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 之前,您可以轻松地加入您在编辑时提到的用户信息的表格,以添加最后一个要求。

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

      【讨论】:

      • 缺点是这需要在每次运行查询时生成日期,而且您需要将此代码复制并粘贴到需要它的所有查询中。我可以看到一个论点,说在内存中生成序列比从磁盘读取它要快(尽管它可能会被缓存在内存中),但我仍然想创建一个函数或查看创建序列,以便我可以重用代码。 (意味着反对在数据库中创建对象的论点变得无声。)
      【解决方案5】:

      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 function

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

      【讨论】:

      • 谢谢。这是一个快速的操作,您会建议不要使用这种方法并进行服务器端计算吗?
      • @Jerry2:我的偏好是在数据库中进行尽可能多的数据处理,而不是真正涉及的演示内容。我不羡慕在应用程序代码中这样做,只要它是一次访问数据库...
      • 为了使用索引,条件(WHERE 和 ON 子句)可以重写为 WHERE n.id &lt; DATEDIFF('2010-06-14', '2010-06-06')LEFT JOIN TABLE y ON y.date = DATE_FORMAT(x.ts, '%d.%m.%Y')
      • 只要我添加了 WHERE 子句,例如 WHERE 'y'.'score' = 2,所有填充的日期都不再显示
      • @SebaM :那是因为WHERE 子句在 加入之后应用。因此,您从完整范围开始,对数据进行左连接,然后删除一堆行。您真正想要做的是过滤 y 表作为连接的一部分... SELECT * FROM x LEFT JOIN y ON y.foo = x.bah AND y.score = 2
      【解决方案6】:

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

      【讨论】:

      • 没错,但数字表更灵活 - 请参阅我的答案以获取示例。 IE:如果现在你也需要序列号怎么办?您想要每种数据类型的表吗?
      • 需要序列号将是另一个用例 ;-) 如果您必须针对不同的 DBMS(即 Oracle、MySQL、SQL-Server),您的方法需要稍微修改一下语句,我怀疑DATE_ADD 方法比日历表慢(但我认为这与这里无关)
      • http://www.media-division.com/using-mysql-generate-daily-sales-reports-filled-gaps/ 有一个创建日历表的简单易用的程序。虽然,正如上面提到的@omg-ponies,数字技巧几乎和日历表一样快,但有时使用时髦的技巧可能会产生误导。特别是如果您希望其他开发人员将来维护您的代码。
      • 与 numbers-table-solution 相比,日历表可以让您编写简单的查询,例如 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'
      猜你喜欢
      • 2021-03-19
      • 2015-10-19
      • 2023-03-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多