【问题标题】:How to Efficiently Find Number of Specific Day Between Two Dates in MySQL?如何有效地查找 MySQL 中两个日期之间的特定天数?
【发布时间】:2016-12-19 18:01:19
【问题描述】:

之前已经问过这个问题的不同变体,但没有针对我正在寻找的用例。我想为 MySQL 表的每一行找到两个日期之间的具体工作日数,然后用该操作的结果更新每一行的列。这是 ETL 过程的一部分,如果可能的话,我希望将其保存在存储过程中。

数据

日期是 DATE 类型,我想找到特定的编号,因为如果在一周中的那一天发生记录,我有 7 个带有标志的 day 列。像这样(1 是星期一):

day_1 | day_2 | day_3 | day_4 | day_5 | day_6 | day_7
----- | ----- | ----- | ----- | ----- | ----- | -----
0     | 1     | 0     | 1     | 1     | 0     | 1

示例用例

我这样做是因为我试图找到输入数据中不可用的时间范围内的行频率(称为input)。因此,对于具有startend 日期值为2016-01-012016-03-01 的记录,我想知道该记录仅从2016-01-012016-01-31 出现的频率,包括在内。我最初尝试通过制作一个包含未来多年所有日期值的表格来做到这一点,例如:

datevalue
---------
2016-01-01
2016-01-02
...

然后在start_dateend_date 上将input 加入该表,然后在计算每天的数量时进行汇总,如下所示:

SUM(CASE WHEN WEEKDAY(B.datevalue) + 1 = 1 THEN 1 ELSE 0 END) * day_1 +
        SUM(CASE WHEN WEEKDAY(B.datevalue) + 1 = 2 THEN 1 ELSE 0 END) * day_2 +
        SUM(CASE WHEN WEEKDAY(B.datevalue) + 1 = 3 THEN 1 ELSE 0 END) * day_3 +
        SUM(CASE WHEN WEEKDAY(B.datevalue) + 1 = 4 THEN 1 ELSE 0 END) * day_4 +
        SUM(CASE WHEN WEEKDAY(B.datevalue) + 1 = 5 THEN 1 ELSE 0 END) * day_5 +
        SUM(CASE WHEN WEEKDAY(B.datevalue) + 1 = 6 THEN 1 ELSE 0 END) * day_6 +
        SUM(CASE WHEN WEEKDAY(B.datevalue) + 1 = 7 THEN 1 ELSE 0 END) * day_7 AS adj_total_frequency

这在较小的数据集上运行良好,但 input 有超过 3000 万条记录,当我尝试在该程序上运行时,它运行了 36 个小时才被我杀死。

在 MySQL 中有更有效的方法吗?

【问题讨论】:

  • 您可以预先计算WEEKDAY(B.datevalue) + 1并将其存储在datevalue的表中;不知道它会加快多少。另一种方法是不给endstart 加上日期,并计算出从中删除适当天数的计算。
  • 预先计算 WEEKDAY() + 1 将仅聚合 dates 表中的 datevalue 列的查询加快了 25%,但我不知道这种改进是否呈线性关系。你是什​​么意思不约会?
  • 哦,我不知道“不”是怎么进来的。我的意思是替代 datesub。

标签: mysql date join group-by


【解决方案1】:

(有时)MySQL 在使用 JOIN 优化 GROUP BY 语句时遇到了很大的麻烦。为了克服这个问题,您可以将连接的结果存储到一个临时表中,这样您就可以对一个表使用 GROUP BY。

drop temporary table if exists tmp;
create temporary table tmp (id int unsigned not null)
engine=myisam
    select i.id
    from input i
    straight_join dates B 
        on  B.datevalue >= i.`start`
        and B.datevalue <  i.`end`
    where (
        (WEEKDAY(B.datevalue ) = 0) AND i.day_7 OR
        (WEEKDAY(B.datevalue ) = 1) AND i.day_1 OR
        (WEEKDAY(B.datevalue ) = 2) AND i.day_2 OR
        (WEEKDAY(B.datevalue ) = 3) AND i.day_3 OR
        (WEEKDAY(B.datevalue ) = 4) AND i.day_4 OR
        (WEEKDAY(B.datevalue ) = 5) AND i.day_5 OR
        (WEEKDAY(B.datevalue ) = 6) AND i.day_6
    )
    -- and i.id >  000000
    -- and i.id <= 100000
;

drop temporary table if exists tmp1;
create temporary table tmp1 (id int unsigned not null, cnt int unsigned not null)
engine=myisam
    select id, count(1) as cnt
    from tmp
    group by id
;

update input i
join tmp1 using(id)
set i.numdays = tmp1.cnt
where 1=1;

我的测试数据包含 1M 行,其中包含随机日期位 (round(rand())),平均日期范围为 50 天。所以tmp 表包含大约 25M 行。

在我的系统上,10K 行大约需要 500 毫秒,100K 行需要 5 秒,1M 行需要 2 分钟。因此,如果您将更新拆分为 100K 行的块(使用第一条语句中注释的 id 范围条件),您应该在大约 30 分钟内准备好。

【讨论】:

  • 感谢您的回答,但我最终使用的答案与针对此问题给出的其他答案(this answer here)更相似。
【解决方案2】:

评论太长了,但是结合我最初建议的工作日的预先计算,这对你来说有多少(使用单个SUM 和完整的CASE)?

SUM(CASE WHEN B.weekdayval = 1 AND day_1 THEN 1
        WHEN B.weekdayval = 2 AND day_2 THEN 1
        WHEN B.weekdayval = 3 AND day_3 THEN 1
        WHEN B.weekdayval = 4 AND day_4 THEN 1
        WHEN B.weekdayval = 5 AND day_5 THEN 1
        WHEN B.weekdayval = 6 AND day_6 THEN 1
        WHEN B.weekdayval = 7 AND day_7 THEN 1 
        ELSE 0 END) AS adj_total_frequency

其实这样会更好; 可能理论上意味着 B.weekdayval 每行只比较一次(我说 理论上因为 MySQL 不保证不相关的 THEN 子句不会被评估,只是不会“返回"来自CASE)。

SUM(CASE WHEN day_1 THEN B.weekdayval = 1
        WHEN day_2 THEN B.weekdayval = 2
        WHEN day_3 THEN B.weekdayval = 3
        WHEN day_4 THEN B.weekdayval = 4
        WHEN day_5 THEN B.weekdayval = 5
        WHEN day_6 THEN B.weekdayval = 6
        WHEN day_7 THEN B.weekdayval = 7 
        ELSE 0 END) AS adj_total_frequency

编辑:就 datesub 方法而言,我没有时间编写完整的解决方案,但要让您(或其他潜在的回答者)开始...

  • 我的意思是 DATEDIFF
  • 您可以使用DATEDIFF(end, start) DIV 7 获取开始和结束之间的整周数
  • 将其乘以一周中适用于获得近似值的天数
  • 然后(最难的部分)计算出div 未涵盖的小数周要添加的天数。

【讨论】:

  • 难道DATEDIFF 方法全年都不会准确吗?当月份的天数不同时,它会很困难。
  • DATEDIFF 给出了天数之间的天数,月份的变化不应该考虑到这个计算中;如果几个月过早地重置一周,那会是这样,但这不是我们日历的工作方式。为了清楚起见,月份开始于上个月结束的一周中的某一天之后的一周中的某一天。
  • 我接受这个答案是因为它更接近我最终使用的解决方案,即使用来自this answer here 的函数。
猜你喜欢
  • 1970-01-01
  • 2017-04-22
  • 1970-01-01
  • 2013-02-05
  • 1970-01-01
  • 1970-01-01
  • 2018-08-28
  • 2017-07-05
  • 1970-01-01
相关资源
最近更新 更多