在 sql 结果中填充空日期的最直接方法是啥(在 mysql 或 perl 端)?

Posted

技术标签:

【中文标题】在 sql 结果中填充空日期的最直接方法是啥(在 mysql 或 perl 端)?【英文标题】:What is the most straightforward way to pad empty dates in sql results (on either mysql or perl end)?在 sql 结果中填充空日期的最直接方法是什么(在 mysql 或 perl 端)? 【发布时间】:2010-09-09 16:45:43 【问题描述】:

我正在从 mysql 表中构建一个快速 csv,其查询如下:

select DATE(date),count(date) from table group by DATE(date) order by date asc;

然后将它们转储到 perl 中的文件中:

while(my($date,$sum) = $sth->fetchrow) 
    print CSV "$date,$sum\n"

但数据中存在日期差距:

| 2008-08-05 |           4 | 
| 2008-08-07 |          23 | 

我想用零计数条目填充数据以填补缺失的日期:

| 2008-08-05 |           4 | 
| 2008-08-06 |           0 | 
| 2008-08-07 |          23 | 

我用每月几天的数组和一些数学计算了一个非常尴尬(而且几乎肯定有问题)的解决方法,但是在 mysql 或 perl 方面必须有一些更直接的方法。

我为什么这么笨,有什么天才的想法/耳光吗?


我最终使用了一个存储过程,它为相关日期范围生成了一个临时表,原因有两个:

我知道每次我要查找的日期范围 不幸的是,有问题的服务器不是我可以在 atm 上安装 perl 模块的服务器,而且它的状态已经足够陈旧,以至于它没有远程安装任何东西 Date::-y

perl 的 Date/DateTime 迭代答案也很好,我希望我可以选择多个答案!

【问题讨论】:

【参考方案1】:

当您在服务器端需要类似的东西时,您通常会创建一个包含两个时间点之间所有可能日期的表,然后将该表与查询结果左连接。像这样的:

create procedure sp1(d1 date, d2 date)
  declare d datetime;

  create temporary table foo (d date not null);

  set d = d1
  while d <= d2 do
    insert into foo (d) values (d)
    set d = date_add(d, interval 1 day)
  end while

  select foo.d, count(date)
  from foo left join table on foo.d = table.date
  group by foo.d order by foo.d asc;

  drop temporary table foo;
end procedure

在这种特殊情况下,最好在客户端进行一些检查,如果当前日期不是 previos+1,则添加一些附加字符串。

【讨论】:

【参考方案2】:

当我不得不处理这个问题时,为了填写缺失的日期,我实际上创建了一个参考表,其中只包含我感兴趣的所有日期,并在日期字段中加入了数据表。它很粗糙,但它有效。

SELECT DATE(r.date),count(d.date) 
FROM dates AS r 
LEFT JOIN table AS d ON d.date = r.date 
GROUP BY DATE(r.date) 
ORDER BY r.date ASC;

至于输出,我只使用SELECT INTO OUTFILE 而不是手动生成 CSV。让我们也不必担心转义特殊字符。

【讨论】:

【参考方案3】:

不笨,这不是 MySQL 所做的,插入空的日期值。我在 perl 中通过两步过程执行此操作。首先,将查询中的所有数据加载到按日期组织的哈希中。然后,我创建一个 Date::EzDate 对象并按天递增,所以...

my $current_date = Date::EzDate->new();
$current_date->'default' = 'YEAR-MONTH NUMBER BASE 1-DAY OF MONTH';
while ($current_date <= $final_date)

    print "$current_date\t|\t%hash_o_data$current_date";  # EzDate provides for     automatic stringification in the format specfied in 'default'
    $current_date++;

其中最终日期是另一个 EzDate 对象或包含日期范围结束的字符串。

EzDate 目前不在 CPAN 上,但您可能会找到另一个 perl mod 来进行日期比较并提供日期增量器。

【讨论】:

【参考方案4】:

您可以使用DateTime 对象:

use DateTime;
my $dt;

while ( my ($date, $sum) = $sth->fetchrow )  
    if (defined $dt) 
        print CSV $dt->ymd . ",0\n" while $dt->add(days => 1)->ymd lt $date;
    
    else 
        my ($y, $m, $d) = split /-/, $date;
        $dt = DateTime->new(year => $y, month => $m, day => $d);
    
    print CSV, "$date,$sum\n";

上面的代码所做的是将最后打印的日期保存在一个 DateTime对象$dt,并且当当前日期超过一天时 将来,它会将$dt 增加一天(并打印一行到 CSV) 直到与当前日期相同。

这样您就不需要额外的表格,也不需要获取所有的 提前行。

【讨论】:

【参考方案5】:

我希望你能解决剩下的问题。

select  * from (
select date_add('2003-01-01 00:00:00.000', INTERVAL n5.num*10000+n4.num*1000+n3.num*100+n2.num*10+n1.num DAY ) as date from
(select 0 as num
   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) n1,
(select 0 as num
   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) n2,
(select 0 as num
   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) n3,
(select 0 as num
   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) n4,
(select 0 as num
   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) n5
) a
where date >'2011-01-02 00:00:00.000' and date < NOW()
order by date

select n3.num*100+n2.num*10+n1.num as date

你会得到一列数字从 0 到 max(n3)*100+max(n2)*10+max(n1)

由于这里我们将 max n3 设为 3,因此 SELECT 将返回 399,加上 0 -> 400 条记录(日历中的日期)。

您可以通过限制来调整您的动态日历,例如,从您必须的 min(date) 到 now()。

【讨论】:

KryItsov - 请你解释一下为什么我们需要 max(n3)*100?因为我们没有任何 3 位数的日期,所以我想知道如何使用它。【参考方案6】:

由于您不知道差距在哪里,但您想要从列表中的第一个日期到最后一个日期的所有值(大概),请执行以下操作:

use DateTime;
use DateTime::Format::Strptime;
my @row = $sth->fetchrow;
my $countdate = strptime("%Y-%m-%d", $firstrow[0]);
my $thisdate = strptime("%Y-%m-%d", $firstrow[0]);

while ($countdate) 
  # keep looping countdate until it hits the next db row date
  if(DateTime->compare($countdate, $thisdate) == -1) 
    # counter not reached next date yet
    print CSV $countdate->ymd . ",0\n";
    $countdate = $countdate->add( days => 1 );
    $next;
  

  # countdate is equal to next row's date, so print that instead
  print CSV $thisdate->ymd . ",$row[1]\n";

  # increase both
  @row = $sth->fetchrow;
  $thisdate = strptime("%Y-%m-%d", $firstrow[0]);
  $countdate = $countdate->add( days => 1 );

嗯,结果比我想象的要复杂.. 我希望这是有道理的!

【讨论】:

【参考方案7】:

我认为该问题最简单的通用解决方案是创建一个 Ordinal 表,其中包含您需要的最高行数(在您的情况下为 31*3 = 93)。

CREATE TABLE IF NOT EXISTS `Ordinal` (
  `n` int(10) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (`n`)
);
INSERT INTO `Ordinal` (`n`)
VALUES (NULL), (NULL), (NULL); #etc

接下来,从Ordinal 对您的数据执行LEFT JOIN。这是一个简单的案例,获取上周的每一天:

SELECT CURDATE() - INTERVAL `n` DAY AS `day`
FROM `Ordinal` WHERE `n` <= 7
ORDER BY `n` ASC

您需要对此进行更改的两件事是起点和间隔。为了清楚起见,我使用了SET @var = 'value' 语法。

SET @end = CURDATE() - INTERVAL DAY(CURDATE()) DAY;
SET @begin = @end - INTERVAL 3 MONTH;
SET @period = DATEDIFF(@end, @begin);

SELECT @begin + INTERVAL (`n` + 1) DAY AS `date`
FROM `Ordinal` WHERE `n` < @period
ORDER BY `n` ASC;

因此,如果您要加入以获取过去三个月每天的消息数量,那么最终代码将如下所示:

SELECT COUNT(`msg`.`id`) AS `message_count`, `ord`.`date` FROM (
    SELECT ((CURDATE() - INTERVAL DAY(CURDATE()) DAY) - INTERVAL 3 MONTH) + INTERVAL (`n` + 1) DAY AS `date`
    FROM `Ordinal`
    WHERE `n` < (DATEDIFF((CURDATE() - INTERVAL DAY(CURDATE()) DAY), ((CURDATE() - INTERVAL DAY(CURDATE()) DAY) - INTERVAL 3 MONTH)))
    ORDER BY `n` ASC
) AS `ord`
LEFT JOIN `Message` AS `msg`
  ON `ord`.`date` = `msg`.`date`
GROUP BY `ord`.`date`

提示和评论:

您的查询中最难的部分可能是确定限制Ordinal 时使用的天数。相比之下,将整数序列转换为日期很容易。 您可以使用Ordinal 来满足您的所有不间断序列需求。只需确保它包含的行数多于最长序列即可。 您可以在 Ordinal 上对多个序列使用多个查询,例如列出过去七 (1-7) 周的每个工作日 (1-5)。 您可以通过将日期存储在Ordinal 表中来加快速度,但灵活性会降低。这样你只需要一张Ordinal 表,无论你使用多少次。不过,如果速度值得,请尝试 INSERT INTO ... SELECT 语法。

【讨论】:

【参考方案8】:

使用一些 Perl 模块进行日期计算,例如推荐的 DateTime 或 Time::Piece(来自 5.10 的核心)。只需增加日期和打印日期,0 到日期将匹配当前日期。

【讨论】:

【参考方案9】:

我不知道这是否可行,但是如果您创建一个包含所有可能日期的新表怎么样(如果日期范围会发生不可预测的变化,这可能是这个想法的问题...... .) 然后在两个表上进行左连接?如果有大量可能的日期,或者无法预测第一个和最后一个日期,我想这是一个疯狂的解决方案,但如果日期范围是固定的或易于计算,那么这可能会奏效。

【讨论】:

以上是关于在 sql 结果中填充空日期的最直接方法是啥(在 mysql 或 perl 端)?的主要内容,如果未能解决你的问题,请参考以下文章

在 SQL 中找到两个集合的最紧凑和最有效的方法是啥? [复制]

在 Flutter 中使用设置的 FPS 绘制大量简单、填充颜色的矩形的最高效方法是啥?

Python - 生成填充的最有效方法是啥?

创建零填充 JavaScript 数组的最有效方法是啥?

在pyspark中将值随机更改为空值的最有效方法是啥?

通过sql(presto)填充空组的未来日期