76

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

4

6 回答 6

59

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`
    

如果要保持日期格式,请使用DATE_FORMAT 函数

DATE_FORMAT(`x`.`ts`, '%d.%m.%Y') AS `timestamp`
于 2010-08-21T20:49:16.123 回答
25

我不喜欢其他答案,需要创建表等。此查询在没有帮助表的情况下有效地执行此操作。

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

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

于 2018-06-18T22:02:51.640 回答
20

自从提出这个问题以来,时间已经过去了。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
于 2019-12-20T23:24:56.080 回答
16

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

于 2010-08-21T20:58:44.100 回答
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

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

于 2019-05-28T02:50:32.863 回答
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
于 2021-02-06T11:57:53.840 回答