【问题标题】:MySQL: Matching records that have x consecutive dates available between two datesMySQL:匹配两个日期之间有 x 个连续日期的记录
【发布时间】:2011-06-29 05:14:47
【问题描述】:

背景/应用

我有一个 MySQL 数据库,其中包含一个可出租物业表和一个这些物业的预订表。还有一个搜索功能可以在两个提供的日期之间查找可用的属性。搜索时,用户可以输入开始日期、他们希望停留的天数以及最多 +/- 7 天的日期灵活性。一个预订可以在另一个预订结束的同一天开始(第 1 方在早上离开,第 2 方在晚上到达)。

我很难有效地实现灵活性功能。

架构

CREATE TABLE IF NOT EXISTS `property` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `name` varchar(60) COLLATE utf8_unicode_ci DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS `property_booking` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `property_id` bigint(20) DEFAULT NULL,
    `name` varchar(60) COLLATE utf8_unicode_ci DEFAULT NULL,
    `date_start` date DEFAULT NULL,
    `date_end` date DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

样本数据

INSERT INTO `property` (`name`) 
VALUES ('Property 1'), ('Property 2'), ('Property 3');

INSERT INTO `property_booking` (`property_id`,`name`,`date_start`,`date_end`) 
VALUES (1, 'Steve', '2011-03-01', '2011-03-08'), 
(2, 'Bob', '2011-03-13', '2011-03-20'), 
(3, 'Jim', '2011-03-16', '2011-03-23');

示例场景

用户选择他们想在 2011 年 3 月 10 日开始逗留,他们想逗留 7 天,并且他们有 +/- 2 天的灵活性。我已经编译了一个图像,可以将下面的数据和参数可视化。 (红色:预订 1,绿色:预订 2,条纹:预订 3,蓝色:日期范围(2011 年 3 月 10 日,+ 7 天和 +/- 2 天的灵活性))

预期结果

物业 1 (在整个日期范围内均可预订)
物业 3 (可从 2011 年 3 月 8 日或 2011 年 3 月 9 日开始预订)

当前方法

我当前的查询检查整个可搜索日期范围内所有 7 天的日期范围是否重叠,如下所示:

SELECT p.`id`, p.`name` 
FROM `property` p 
WHERE (NOT (EXISTS (SELECT p2.`name` FROM `property_booking` p2 WHERE (p2.`property_id` = p.`id` AND '2011-03-10' < DATE_SUB(p2.`date_end`, INTERVAL 1 DAY) AND '2011-03-17' > DATE_ADD(p2.`date_start`, INTERVAL 1 DAY))))) 
OR (NOT (EXISTS (SELECT p3.`name` FROM `property_booking` p3 WHERE (p3.`property_id` = p.`id` AND '2011-03-11' < DATE_SUB(p3.`date_end`, INTERVAL 1 DAY) AND '2011-03-18' > DATE_ADD(p3.`date_start`, INTERVAL 1 DAY))))) 
OR (NOT (EXISTS (SELECT p4.`name` FROM `property_booking` p4 WHERE (p4.`property_id` = p.`id` AND '2011-03-09' < DATE_SUB(p4.`date_end`, INTERVAL 1 DAY) AND '2011-03-16' > DATE_ADD(p4.`date_start`, INTERVAL 1 DAY))))) 
OR (NOT (EXISTS (SELECT p5.`name` FROM `property_booking` p5 WHERE (p5.`property_id` = p.`id` AND '2011-03-12' < DATE_SUB(p5.`date_end`, INTERVAL 1 DAY) AND '2011-03-19' > DATE_ADD(p5.`date_start`, INTERVAL 1 DAY)))))
OR (NOT (EXISTS (SELECT p6.`name` FROM `property_booking` p6 WHERE (p6.`property_id` = p.`id` AND '2011-03-08' < DATE_SUB(p6.`date_end`, INTERVAL 1 DAY) AND '2011-03-15' > DATE_ADD(p6.`date_start`, INTERVAL 1 DAY)))));

在示例数据集上,它相当快,但在更大的数据集上,它会变得相当缓慢,当您构建完整的 +/- 7 天灵活性时更是如此。

有人对如何更好地编写此查询有任何建议吗?

【问题讨论】:

  • 如果您可以随意更改数据库的模型,这个其他 SO question 对这个问题有一个很好的答案stackoverflow.com/questions/781221/…
  • +1 表示我见过的最好的问题。

标签: mysql date date-range


【解决方案1】:

好的,这是一个棘手问题的棘手答案...

SELECT * FROM property AS p
LEFT JOIN  
(
  SELECT property_id, DATEDIFF(MAX(date_end),20110308) AS startblock, 
      DATEDIFF(20110319,MIN(date_start))-1 AS endblock
  FROM property_booking AS pb
  WHERE date_start < 20110319 || date_end >= 20110308 
  GROUP BY property_id
  HAVING LEAST(startblock,endblock) > 4
) AS p2 ON p.id = p2.property_id 
WHERE p2.property_id IS NULL;

子查询选择所有不符合条件的属性。 IS NULL 的 LEFT JOIN 基本上可以排除(对不合格属性的否定)

  • 20110308 是所需的开始日期 -2 天(因为 +/-2 天的灵活性)
  • 20110319 是期望的结束日期 +2 天
  • HAVING LEAST(startblock,endblock) &gt; 4 中的数字 4 是 +/- 数字 (2*2) 的两倍

我花了一段时间才解决(但你的问题很有趣,我有时间)

我已经用边缘案例对其进行了测试,并且它适用于我抛出的所有测试用例......)。其背后的逻辑有点奇怪,但一个好的旧笔和纸帮助我解决了这个问题!

编辑

不幸的是,我意识到这适用于大多数情况,但并非全部...(在查找期的开始和结束时进行 2 天的单日预订,即使该物业应该可用,也无法使用)。

这里的问题是您必须查找数据库中不“存在”的信息,并根据您拥有的数据重建它。查看我对您问题的评论,了解解决问题的更好方法

【讨论】:

  • 有趣的解决方案,谢谢!我将不得不解决它并进一步测试它,但看起来可以完成这项工作。您提到的不适用的情况不会成为问题,因为最短预订期为 7 天。一旦我将其转换为 DQL(用于 Doctrine),我将看看它如何应对更大的数据集并回复您。
【解决方案2】:

我想这就是你要找的东西:

   SELECT MAX( IF( (    b.date_start < '2011-03-08' + INTERVAL 7 DAY
                    AND b.date_end > '2011-03-08'), 1, 0)) AS is_booked,
          p.id,
          p.name
     FROM property p
LEFT JOIN property_booking b ON p.id = b.property_id
 GROUP BY p.id
   HAVING is_booked < 1

如果要包含余地,请展开 MAX() 聚合以包含选项:

   SELECT MAX( IF(    (    b.date_start < '2011-03-08' + INTERVAL 7 DAY
                       AND b.date_end > '2011-03-08')
                  AND (    b.date_start < '2011-03-08' + INTERVAL 7 DAY + INTERVAL 1 DAY
                       AND b.date_end > '2011-03-08' + INTERVAL 1 DAY)
                  AND (    b.date_start < '2011-03-08' + INTERVAL 7 DAY + INTERVAL 2 DAY
                       AND b.date_end > '2011-03-08' + INTERVAL 2 DAY), 1, 0)
             ) AS is_booked,
          p.id,
          p.name
     FROM property p
LEFT JOIN property_booking b ON p.id = b.property_id
 GROUP BY p.id
   HAVING is_booked < 1

如果我对您的问题的理解正确,那么这个 GROUP BY 查询应该比多个子查询更有效地涵盖它。

【讨论】:

  • 逻辑其实很简单。如果预订的 date_start 早于所需的结束日期,并且预订的 date_end 晚于所需的开始日期,则日期范围重叠。由于 LEFT JOIN 包含不匹配的 NULL,因此 is_booked 别名包含是否发生重叠,anc 因为它是一个 MAX 聚合函数,所以当某些范围不重叠而其他范围重叠时,它会自动将每个标记为已预订。
  • 感谢您的建议 - 在初始测试中,它似乎可以满足我的要求,但是我并不完全了解 MAX() 函数在我有限的数据集上的用法,它没有它似乎会产生相同的结果。
  • 由于外部左连接,左行可能会在更完整的数据集中与不匹配的右行连接。如果没有 MAX(),该值将是最后遇到的值,其中 b.date_start 和 b.date_end 可能为空。此外,虽然 MySQL 将允许您在没有聚合的情况下使用 GROUP BY,但大多数 SQL 不会,而且它确实有点不合适,因为它没有意义,并且与仅由一个过滤的 SELECT DISTINCT 执行相同的工作group by 子句中的列。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-02
  • 2021-05-25
  • 1970-01-01
  • 1970-01-01
  • 2019-08-05
相关资源
最近更新 更多