【问题标题】:Find availability in multiple calendars with relation using MySQL使用 MySQL 在具有关系的多个日历中查找可用性
【发布时间】:2021-06-25 21:04:02
【问题描述】:

我拥有经常出租的东西。它由几个部分组成。它可以部分租用,也可以整体租用。如果一个部分被出租,您将无法将它作为一个整体出租

例如,我租了一辆车。这辆车有轮胎,也是出租的。您可以选择租用轮胎(«整体»)的汽车,或者只租一个轮胎。但是,如果租用一个或多个轮胎,您将无法租用汽车(“整车”)。

我把它想象成一个层次结构。

         Whole
    _______|_______
   |               |
 Part 1           Part 2

我使用一个 Google 日历来处理“整个”事情,并为每个包含部分使用单独的日历。这行得通,但是很烦人,我希望能够将链接发送给感兴趣的人-他们可以在其中查看可用的内容。

所以我创建了一个非常简单的数据库(mariadb 10.4),有两个表:

# tbl: part
| id | parent_id | name   |

parent_id 列仅引用同一张表中的另一行,这是数据示例。

| 1  | NULL      | Car    |
| 2  | 1         | Tire 1 |
| 3  | 1         | Tire 2 |

那么下面的表格来存储每个部分是booked时的日期(+示例数据)。

# tbl: booking
| id | part_id | booked_from | booked_until |
--------------------------------------------
| 1  | 1       | 2021-07-31  | 2021-08-03   |
| 2  | 2       | 2021-08-03  | 2021-08-07   |
| 3  | 3       | 2021-08-04  | 2021-08-06   |
| 4  | 3       | 2021-08-09  | 2021-08-10   |

据此我们知道汽车本身是从2021-07-31 - 2021-08-03 预订的,但它只能从2021-08-06 预订,因为在此期间有两个轮胎出租(但是它们可以在同时它们不是严格相关的)。但是直到2021-08-09,因为一个轮胎又被预订了。

我正在寻找的是一个查询以获取可用日期列表。从pars-table 我能够找出哪些部分是相关的,这不是我最大的问题 - 我想,因为在查询可用性时可以使用这样的东西:

  • 汽车:part_id IN(1,2,3)
  • 轮胎 1:part_id IN(1,2)
  • 轮胎 3:part_id IN(1,3)

我的问题是(很简单?)我如何编排一个只返回可用日期的查询,尤其是对于日期可以重叠的汽车。

例如汽车的结果

SELECT 
  `booked_until` AS `available-from`, 
  `booked_from` as `available-until`
FROM 
  booking
/** some JOIN magic? **
WHERE part_id IN(1,2,3)

例如 tire 1 将是相同的,但 part_id IN(1,2) 因为 tire 2 (id: 3)tire 1 没有直接关系。

两者都应该分别返回:

# car
| available-from | available-until |
------------------------------------
| NULL           | 2021-07-31      |
| 2021-08-06     | 2021-08-09      |
| 2021-08-10     | NULL            |
# tire 1
| available-from | available-until |
------------------------------------
| NULL           | 2021-07-31      |
| 2021-08-07     | NULL            |

NULL-values 只是表示之前或之前没有任何预订。例如,此轮胎在from now 之前可用,直到2021-07-31 和从2021-08-07 到地球存在的最后一天。

希望这是有道理的 - 并且有人能够提供帮助。

提前谢谢你。

【问题讨论】:

  • 你用的是mysql 8吗?否则这种树的功能有限
  • 嗨,我正在使用 MariaDB mysql Ver 15.1 Distrib 10.4.18-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
  • 然后看看递归 ctes
  • 谢谢。看过了,但无论我怎样称呼WITH 语句(例如cte_availability),它都会以#1146 - Table '<db>.cte_availability' doesn't exists 失败。
  • 我在下面添加了一个没有递归 ctes 的解决方案。

标签: mysql sql database gaps-and-islands mariadb-10.4


【解决方案1】:

好的,这是我的尝试。

因此,如果我理解正确,除了表中明确给出的信息外,这些单位还隐含不可用。所以我首先明确地检索了这个:

select unit_id, entry_start, entry_end 
  from unit_calendar_entry
union
select p.id, u.entry_start, u.entry_end
 from unit p
 join unit_calendar_entry u
   on  p.parent_unit_id = u.unit_id
union
select p.parent_unit_id as unit_id, u.entry_start, u.entry_end 
 from unit p
 join unit_calendar_entry u
   on p.id = u.unit_id
  and p.parent_unit_id is not null
order by unit_id, entry_start;
  • 第一个select 只是获取表中已有的条目
  • 第二个添加了汽车的条目,因为如果它的任何一个轮胎被预订,它就可以被认为是预订
  • 第三个添加了轮胎条目,因为如果汽车被预订,它们可以被视为已预订

结果:

unit_id     entry_start           entry_end
----------------------------------------------------
1          2021-07-31 00:00:00   2021-08-03 00:00:00
1          2021-08-03 00:00:00   2021-08-07 00:00:00
1          2021-08-04 00:00:00   2021-08-06 00:00:00
1          2021-08-09 00:00:00   2021-08-10 00:00:00
2          2021-07-31 00:00:00   2021-08-03 00:00:00
2          2021-08-03 00:00:00   2021-08-07 00:00:00
3          2021-07-31 00:00:00   2021-08-03 00:00:00
3          2021-08-04 00:00:00   2021-08-06 00:00:00
3          2021-08-09 00:00:00   2021-08-10 00:00:00

基于此,为了对相邻/重叠的时间跨度进行分组,需要解决间隙和岛屿问题。您可以使用两个查询来标记属于一起的条目like in this SO answer。如果我们调用上面的查询subtab,则对应的语句是

select c.*, sum(case when prev_end < entry_start then 1 else 0 end) over (order by unit_id, entry_start) as grouping   
         from (
              select subtab.*, max(entry_end) over (partition by unit_id order by entry_start rows between unbounded preceding and 1 preceding) as prev_end 
                from subtab
              ) c
  • 内部查询获取每一行的上一个结尾。
  • 外部分配一个分组 ID(在每个 unit_id 内)标识属于连续块(又名)的所有条目。

结果:

unit_id entry_start          entry_end            prev_end             grouping
-------------------------------------------------------------------------------
1       2021-07-31 00:00:00  2021-08-03 00:00:00  (null)               0
1       2021-08-03 00:00:00  2021-08-07 00:00:00  2021-08-03 00:00:00  0
1       2021-08-04 00:00:00  2021-08-06 00:00:00  2021-08-07 00:00:00  0
1       2021-08-09 00:00:00  2021-08-10 00:00:00  2021-08-07 00:00:00  1
2       2021-07-31 00:00:00  2021-08-03 00:00:00  (null)               1
2       2021-08-03 00:00:00  2021-08-07 00:00:00  2021-08-03 00:00:00  1
3       2021-07-31 00:00:00  2021-08-03 00:00:00  (null)               1
3       2021-08-04 00:00:00  2021-08-06 00:00:00  2021-08-03 00:00:00  2
3       2021-08-09 00:00:00  2021-08-10 00:00:00  2021-08-06 00:00:00  3

由此(我们称之为tab),您可以通过对unit_idgrouping 进行分组来获得不可用的时间跨度(请参阅下面的this SO answer 或dbfiddle)或计算空闲时间如下:

select distinct unit_id
              , NULLIF((min(ifnull(prev_end,'1000-01-01')) over (partition by unit_id, grouping)),'1000-01-01') as available_from
              , min(entry_start) over (partition by unit_id, grouping) as available_til
   from tab
union 
select distinct unit_id
                , max(entry_end) over (partition by unit_id) as available_from
                , null as available_til
 from tab
order by unit_id, available_from
  • 第一个查询将available_from 作为每个unit_id/groupingprev_end 的最小值。为了从MIN() 获取NULL 值,我使用了类似于this SO answer 的解决方法。
  • 第二个查询为每个unit_id 添加一行,最大entry_end 作为开始,NULL 作为结束

结果:

unit_id  available_from         available_til
---------------------------------------------------
1        (null)                 2021-07-31 00:00:00
1        2021-08-07 00:00:00    2021-08-09 00:00:00
1        2021-08-10 00:00:00    (null)
2        (null)                 2021-07-31 00:00:00
2        2021-08-07 00:00:00    (null)
3        (null)                 2021-07-31 00:00:00
3        2021-08-03 00:00:00    2021-08-04 00:00:00
3        2021-08-06 00:00:00    2021-08-09 00:00:00
3        2021-08-10 00:00:00    (null)

将所有内容放在一个查询中:

with tab as (
            select c.*, sum(case when prev_end < entry_start then 1 else 0 end) over (order by unit_id, entry_start) as grouping   
              from (
                   select d.*, max(entry_end) over (partition by unit_id order by entry_start rows between unbounded preceding and 1 preceding) as prev_end 
                     from (
                      select unit_id, entry_start, entry_end 
                        from unit_calendar_entry
                      union
                      select p.id, u.entry_start, u.entry_end
                       from unit p
                       join unit_calendar_entry u
                         on p.parent_unit_id = u.unit_id
                      union
                      select p.parent_unit_id as unit_id, u.entry_start, u.entry_end 
                       from unit p
                       join unit_calendar_entry u
                         on p.id = u.unit_id
                        and p.parent_unit_id is not null
                          ) d
                   ) c
            ) 
 select distinct unit_id, NULLIF((min(ifnull(prev_end,'1000-01-01')) over (partition by unit_id, grouping)),'1000-01-01') as available_from, min(entry_start) over (partition by unit_id, grouping) as available_til
   from tab
   union 
  select distinct unit_id, max(entry_end) over (partition by unit_id) as available_from, null as available_til
   from tab
 order by unit_id, available_from

另见this db<>fiddle

【讨论】:

    【解决方案2】:

    非常感谢您 - 这对您非常有帮助。

    由于这个查询对我来说非常复杂,无法完全理解,所以我想问一下

    1. 如何只选择unit_id = 1 的结果?我尝试在外部查询上使用 where 语句,但没有任何效果。在指定 WHERE unit_id IN(1,2,3) 时返回所有日历相交的单元 1 的结果就足够了(这将是在 dbfiddle 中为单元 1 返回的行)。
    unit_id  available_from         available_til
    ---------------------------------------------------
    1        (null)                 2021-07-31 00:00:00
    1        2021-08-07 00:00:00    2021-08-09 00:00:00
    1        2021-08-10 00:00:00    (null)
    
    1. 如何限制日期范围的结果,比如说在日期 2021-08-01 - 2021-08-30 之间。

    再次感谢您!

    编辑: 如果我将“螺栓”(用于安装轮胎)添加到等式中,将层次结构提升到另一个层次。这个解决方案还能用吗?

                                        Whole
                                   _______|_______
                                  |               |
                                Part 1           Part 2
                            ______|____          ...
                            |          |
                         Bolt 1 .... Bolt N
    

    【讨论】:

      【解决方案3】:

      好的,所以我不得不进一步扩展解决方案。添加了另一个表,该表为每个项目的每个条目保留特定数据。例如,它需要返回多长时间,以及准备«ting»恢复其原始状态所需的时间。这只是一个近似值,并不那么重要,但它应该限制任何人在特定时间之前收集它。 :)

      所以表格现在看起来像这样:

      # tbl: property_unit (former: part)
      | id | parent_id | identifier   |
      
      # tbl: property_unit_calendar (NEW)
      | id | property_unit_id | return_by | preparation_time |
      
      # tbl: property_unit_calendar_entry (former: booking)
      | id | calendar_id | entry_start | entry_end |
      

      这不会对当前查询产生太大影响,因为表property_unit_calendar 中的列return_bypreparation_time 中的时间在预订时应用于property_unit_calendar_entry 中的日期时间字段。

      采用代码并添加此关系。它似乎工作正常 - (再次感谢您)。

      更新后的db<>fiddle

      现在我正在努力找出我应该如何减少结果以匹配我正在检查可用性的实际unit。我应该为每个tab-selects 添加吗?像这样

      FROM tab
      WHERE property_unit = 8
      UNION
      SELECT DISTINCT
        ...
      FROM tab
      WHERE property_unit = 8
      

      我正在努力解决的另一件事是如何将结果减少到某个帧,例如在日期之间、从某个日期或到某个日期。

      主要问题不是如何在日期之间/从/直到日期获得结果,但如果预订在时间范围之前提前,它将在第一行和最后一行给我NULL - 这应该表明它可用于“不可预测的过去/未来”(这可能不正确)。

      所以.... 最好为 NULL 添加一个额外的查询来检查帧之前或之后是否有预订?

      希望我解释得足够好。需要考虑这么多的依赖关系!

      谢谢!

      【讨论】:

      • 意识到我可以在d-selector/alias 上使用WHERE property_unit_id = 8,在tab-with 中只获得该单元的结果。现在只剩下一种方法来确保它不会在设置日期范围、开始或结束时说它对整个未来都可用。
      • 我也可以将日期测试放在ìd-test 之后,但我想它会使用更多的计算机能力而不是在内部查询中做一些魔术 (d) ?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多