从一个间隔中获取其他间隔中缺少的所有日期

Юли*_*бак 5 mysql mysql-5.6

我正在使用如下所示的数据库模式,并且正在努力编写对该模式的查询。

酒店签订有有效期的合同。它也有许多季节,每个季节都有许多时期。任务是返回合同有效期中季节性期间不可用的所有日期。

这是我的架构:

数据库架构

理想情况下,我想获得不包括酒店期间的所有可能的时间间隔。但如果不可能,那么至少有一个日期列表。

示例(所有日期均采用 DD.MM.YYYY 格式):

  • 合约期:1.01.2018 – 31.12.2018
  • 酒店期间:1.01.2018 – 30.06.2018;1.08.2018 – 31.12.2018

在这种情况下,我损失了一个月 1.07.2018 – 31.07.2018。我想得到这个时期,或者几个时期,如果碰巧有几个。所以,像这样:

开始日期 结束日期
--------- ----------
1.07.2018 31.07.2018

如果无法获得期间列表,那么我想获取所有缺失的日期,并将它们分组到服务器端的期间。

And*_*y M 9

如果酒店永远不会有重叠的季节性时段,则可以通过使用以下方法直接匹配间隔来获得作为间隔列表的结果。

首先,您需要反转季节性周期列表,这意味着获取列表项目之间的差距列表,并用另外两个间隔表示第一个项目之前的周期和最后一个项目之后的周期。换句话说,像这样的列表:

date_from date_to
---------- ---------
date_from 1    date_to 1 
date_from 2    date_to 2
.
.
.
date_from N    date_to N

将转换为这样的列表:

   date_from date_to
----------------- ------------------
   0001-01-01         date_from 1 - 1 天
date_to 1    + 1 天 date_from 2 - 1 天
date_to 2    + 1 天 date_from 3 - 1 天
.
.
.
date_to N-1 + 1 天 date_from N - 1 天
date_to N    + 1 天 9999-12-31

第二个列表中的和指的是第一个列表中的相应值。一天的调整是为了说明输入间隔和输出间隔都包含在内的事实。date_fromidate_toi

这是根据您的架构生成 SQL 中的差距列表的方式:

SELECT
  IFNULL(
    (
      SELECT
        hs_last.date_to + INTERVAL 1 DAY
      FROM
        hotel_season AS hs_last
        INNER JOIN hotel_season_period AS hsp_last
          ON hs_last.id = hsp_last.hotel_season_id
      WHERE
        hs_last.hotel_id = hs_this.hotel_id
        AND hsp_last.date_from < hsp_this.date_from
      ORDER BY
        hsp_last.date_from DESC
      LIMIT
        0, 1
    ),
    CAST('0001-01-01' AS date)
  )                                      AS date_from,
  hs_this.date_from - INTERVAL 1 DAY     AS date_to
FROM
  hotel_season AS hs_this
  INNER JOIN hotel_season_period AS hsp_this
    ON hs_this.id = hsp_this.hotel_season_id
WHERE
  hs_this.hotel_id = @param_hotel_id

UNION ALL

SELECT
  MAX(hs_this.date_to) + INTERVAL 1 DAY  AS date_from,
  CAST('9999-12-31' AS date)             AS date_to
FROM
  hotel_season AS hs_this
  INNER JOIN hotel_season_period AS hsp_this
    ON hs_this.id = hsp_this.hotel_season_id
WHERE
  hs_this.hotel_id = @param_hotel_id
;
Run Code Online (Sandbox Code Playgroud)

请注意,相邻的季节性周期将产生“间隙”,其中date_from > date_to. 当然,这些范围是无效的,您需要在进一步处理之前将它们过滤掉。

进一步处理将涉及找到与酒店合同期相交的差距。与它相交的那些实际上将是您想要返回的间隔,除非您可能需要调整第一个匹配间隙的开始以及最后一个匹配间隙的结束,因为它们可能超出合同期限范围.

这是匹配条件,它考虑到合同期和缺口都是包含区间:

gap.date_to >= contract.date_from AND gap.date_from <= contract.date_to
Run Code Online (Sandbox Code Playgroud)

也就是说,当其结束在合同开始之后或完全匹配而同时间隙的开始在合同结束之前或完全匹配时,将间隙视为与合同期相交。

正如我已经说过的,第一个和最后一个匹配的差距可能会部分超出合同期限,即像这样:

合同: [ ]
第一个差距:[ ]
最后一个差距:[ ]

对于输出,您需要像这样修改它们:

合同: [ ]
第一个差距:[ ]
最后一个差距:[ ]

这意味着你将需要同时计算开始和每个间隙结束:采取的最新gap.date_fromcontract.date_from和起点之间的最早gap.date_tocontract.date_to作为结束:

GREATEST(gap.date_from, contract.date_from)   AS date_from,
LEAST   (gap.date_to  , contract.date_to  )   AS date_to
Run Code Online (Sandbox Code Playgroud)

把所有东西放在一起,这就是你得到的:

SELECT
  GREATEST(gap.date_from, contract.date_from)   AS date_from,
  LEAST   (gap.date_to  , contract.date_to  )   AS date_to
FROM
  list_of_gaps AS gap
  CROSS JOIN contract
WHERE
  contract.company_id = @param_hotel_id
  AND gap.date_to >= contract.date_from
  AND gap.date_from <= contract.date_to
;
Run Code Online (Sandbox Code Playgroud)

对于list_of_gaps您可以直接使用第一个查询作为派生表:

...
FROM
  (
    SELECT
      IFNULL(
    .
    .
    .
  ) AS gap
...
Run Code Online (Sandbox Code Playgroud)

或者您可以先将该查询的结果存储在临时表中:

CREATE TEMPORARY TABLE tmp_gap_list
AS
SELECT
  IFNULL(
.
.
.
Run Code Online (Sandbox Code Playgroud)

并使用临时表作为list_of_gaps替代:

SELECT
  GREATEST(gap.date_from, contract.date_from)   AS date_from,
  LEAST   (gap.date_to  , contract.date_to  )   AS date_to
FROM
  tmp_gap_list AS gap
.
.
.
Run Code Online (Sandbox Code Playgroud)

后一种选择——分两个不同的步骤完成工作——可能更快,这取决于 MySQL 是否会发现单查询解决方案太复杂而无法提出有效的计划,还取决于表中的数据量。为自己测试这两种选择,以选择最适合您的一种。