如何知道一组日期范围是否涵盖较大的日期范围?

时间:2019-07-12 00:19:40

标签: mysql sql bit-manipulation

我必须拥有实体:postesbookingspostebooking之间存在oneToMany关系:一个邮件可能有很多预订(在不同的日期)。

bookings 由4列定义:

  • booking_id:id
  • poste_id:联合postes
  • start_datetime:开始日期预订
  • number_day:天数(整数)

postes 由4列定义:

  • poste_id:发布ID
  • pattern(字符串):定义了允许的日期(允许1天,不允许0天)。第八天定义为模式的第一天(模7)
  • start:发布开始日期(预订中的所有日期都包括在开始和结束之间)
  • end:发布结束日期

目标:我想定义一个查询,该查询选择未完全保留的所有postes(例如,可能会有一些新的保留)。我被困住了,因为我无法选择免费日期范围内的任何数据,因为仅存储预订信息。

示例

Booking table
| booking_id | poste_id |       start_datetime | number_day |
|------------|----------|----------------------|------------|
|          1 |        1 | 2019-07-10T00:00:00Z |          4 |
|          4 |        1 | 2019-07-14T00:00:00Z |          1 |
|          7 |        1 | 2019-07-16T00:00:00Z |          4 |
|          2 |        2 | 2019-07-10T00:00:00Z |          2 |
|          9 |        2 | 2019-07-13T00:00:00Z |          2 |
|          5 |        3 | 2019-07-15T00:00:00Z |          2 |
|          8 |        3 | 2019-07-21T00:00:00Z |          3 |
|         11 |        3 | 2019-07-28T00:00:00Z |          1 |
|         12 |        3 | 2019-07-29T00:00:00Z |          1 |
|          3 |        4 | 2019-07-15T00:00:00Z |          1 |
|         13 |        4 | 2019-07-21T00:00:00Z |          2 |
Postes table:
| poste_id | pattern |                start |                  end |
|----------|---------|----------------------|----------------------|
|        1 | 1111101 | 2019-07-10T00:00:00Z | 2019-07-20T00:00:00Z |
|        2 | 1101101 | 2019-07-10T00:00:00Z | 2019-07-20T00:00:00Z |
|        3 | 1100001 | 2019-07-15T00:00:00Z | 2019-07-30T00:00:00Z |
|        4 | 1011001 | 2019-07-15T00:00:00Z | 2019-07-30T00:00:00Z |

该示例的预期输出为:2,4。 (没有用于邮寄13的预订)。

注意

  • 这是一个简单的示例。实际上,日期范围较大,例如几个月了。
  • 数据库假定预订没有重叠
  • 模式长度可能不同于7。模式的第一天与一周中的天数之间没有任何联系。例如,如果模式为“ 1101”,开始日期为“ 10-07-2019”,则意味着第10、11、13、14、15、17等天可用,而其他天不可用。
  • 预订日期始终在poste的开始日期和结束日期之间。

可复制性:

// Build the tables:
CREATE TABLE bookings
    (`booking_id` int, `poste_id` int, `start_datetime` datetime, `number_day` int)
;

INSERT INTO bookings
    (`booking_id`, `poste_id`, `start_datetime`, `number_day`)
VALUES
    (1, 1, '2019-07-10', '4'),
    (4, 1, '2019-07-14', '1'),
    (7, 1, '2019-07-16', '4'),
    (2, 2, '2019-07-10', '2'),
    (9, 2, '2019-07-13', '2'),
    (5, 3, '2019-07-15', '2'),
    (8, 3, '2019-07-21', '3'),
    (11, 3, '2019-07-28', '1'),
    (12, 3, '2019-07-29', '1'),
    (3, 4, '2019-07-15', '1'),
    (13, 4, '2019-07-21', '2')
;

CREATE TABLE postes
    (`poste_id` int, `pattern` VARCHAR(7), `start` datetime, `end` datetime);

INSERT INTO postes VALUES 
  (1, "1111101", "2019-07-10", "2019-07-20"),
  (2, "1101101", "2019-07-10", "2019-07-20"),
  (3, "1100001", "2019-07-15", "2019-07-30"),
  (4, "1011001", "2019-07-15", "2019-07-30");

我的工作:到目前为止,我设法在给定的一天中找到了可用的帖子:

   SELECT DISTINCT p.* 
     FROM postes p
LEFT JOIN bookings b
       ON b.poste_id = p.poste_id
    WHERE
          /* Ignore date in past */
          MOD(DATEDIFF("2019-07-16", p.start), LENGTH(p.pattern)) >= -1

      AND
          /* Filter poste with pattern = 1 */
          SUBSTRING(p.pattern, MOD(DATEDIFF("2019-07-16", p.start),
                                   LENGTH(p.pattern)) + 1 , 1) = 1
      AND 
          /* Filter those available this day */
          p.poste_id NOT IN (
                SELECT b.poste_id
                  FROM bookings b
                 WHERE b.start_datetime <= "2019-07-16"
                   AND "2019-07-16" < DATE_ADD(b.start_datetime, INTERVAL b.number_day DAY)
                             );

输出:

| poste_id | pattern |                start |                  end |
|----------|---------|----------------------|----------------------|
|        2 | 1101101 | 2019-07-10T00:00:00Z | 2019-07-20T00:00:00Z |

4 个答案:

答案 0 :(得分:1)

由于poste.pattern,我看不到如何直接处理日期范围。可以通过加入一个表来扩展1天解决方案,该表在单独的行中列出了某个范围内的所有日期,可以通过以下方式生成:

  

How to populate a table with a range of dates?

...用该表中的"2019-07-16"列替换_date

注意:对于此任务,使用编程语言可能比仅使用SQL的方法更具性能。

答案 1 :(得分:1)

(尚不完整的答案,但至少有一些提示...)

  • bookings poste_idroom_id的第二列吗? (我猜“房间”对于英语来说是更好的词?)
  • 使用DATE数据类型代替DATETIME。 (此外,MySQL会在日期时间文字中使TZ窒息。)
  • 使用VARCHAR(7)代替TINYINT UNSIGNED。这将使您可以使用布尔运算,移位运算和BIT_COUNT()函数。 (请参阅http://api.mongodb.com/python/current/api/pymongo/database.html#pymongo.database.Database。)这些应该有助于所需的计算。
  • 您的位串长为7,好像与星期几有关。但是吗?也就是说,第一位是否与星期日相关?还是与poste.start绑定?
  • 您正在使用哪个版本的MySQL?在8.0之前,位操作限制为64,因此将位操作解决方案的时间限制为大约2个月。有了8.0,操作的大小几乎是无限的。

所以,对于8.0,我可能会

  1. 将位(不是char)模式复制足够的次数。 (Hmmm ... REPEAT可以很方便地用于字符,但不能用于位。也许对字符字符串进行重复,然后转换为位。)
  2. end日期之后砍掉位。
  3. BIT_COUNT(),以查看该范围内有多少天。
  4. SUM(number_day)获取保留天数。
  5. 减去以查看不保留多少天。 (注意:这是假设数据为“有效”,即bookings中没有任何“重叠”。

(按照Aprillion的建议,我可能会用“真实的”编程语言编写代码。在上面的步骤在那里可能会有用。)

对于旧版本的MySQL和/或使用VARCHAR(7)而不是TINYINT,上述步骤可能会起作用,但有一些替代。例如,BIT_COUNT可以替换为LENGTH(s) - LENGTH(REPLACE(s, '1', ''))

答案 2 :(得分:1)

这是SQL问题中一个非常常见的问题-如何匹配数据库中不存在的数据? SQL更好地匹配 存在的数据。

SQL的另一种特性是,它对行集更好地工作,而不是由开始和结束定义的虚范围。因此,我的策略是将范围转换为实际行集。

首先,创建一个包含所有日期的表格:

CREATE TABLE dates (date DATE PRIMARY KEY);
INSERT INTO dates SET date = '2019-07-01';
... 
INSERT INTO dates SET date = '2019-07-30';

创建一个表格,列出任何帖子使用的所有日期:

CREATE TABLE poste_dates (
  poste_id INT, 
  date DATE, 
  booking_id INT,
  PRIMARY KEY (poste_id, date)
);

使用每个日期的所有日期填充该日期,介于日期范围的开始和结束之间。使用联接条件进行过滤,以使与您的模式相对应的星期几为“ 1”。

INSERT INTO poste_dates (poste_id, date) 
SELECT poste_id, d.date FROM postes p JOIN dates d 
  ON SUBSTR(p.pattern, MOD(DATEDIFF(d.date, p.start), LENGTH(p.pattern))+1, 1) 
WHERE d.date BETWEEN p.start AND p.end;

Query OK, 34 rows affected (0.01 sec)

现在您拥有所有帖子的所有日期:

+----------+------------+------------+
| poste_id | date       | booking_id |
+----------+------------+------------+
|        1 | 2019-07-10 |       NULL |
|        1 | 2019-07-11 |       NULL |
|        1 | 2019-07-12 |       NULL |
|        1 | 2019-07-13 |       NULL |
|        1 | 2019-07-14 |       NULL |
|        1 | 2019-07-17 |       NULL |
|        1 | 2019-07-18 |       NULL |
|        1 | 2019-07-19 |       NULL |
|        1 | 2019-07-20 |       NULL |
|        2 | 2019-07-10 |       NULL |
|        2 | 2019-07-11 |       NULL |
|        2 | 2019-07-13 |       NULL |
|        2 | 2019-07-14 |       NULL |
|        2 | 2019-07-17 |       NULL |
|        2 | 2019-07-18 |       NULL |
|        2 | 2019-07-20 |       NULL |
|        3 | 2019-07-15 |       NULL |
|        3 | 2019-07-16 |       NULL |
|        3 | 2019-07-22 |       NULL |
|        3 | 2019-07-23 |       NULL |
|        3 | 2019-07-29 |       NULL |
|        3 | 2019-07-30 |       NULL |
|        4 | 2019-07-15 |       NULL |
|        4 | 2019-07-17 |       NULL |
|        4 | 2019-07-18 |       NULL |
|        4 | 2019-07-22 |       NULL |
|        4 | 2019-07-24 |       NULL |
|        4 | 2019-07-25 |       NULL |
|        4 | 2019-07-29 |       NULL |
+----------+------------+------------+

对于每个预订,请使用UPDATEposte_dates表中设置预订ID。在预订时长内使用LIMIT。我们必须一次执行一次,因为在MySQL中,当LIMIT拥有UPDATEJOIN不起作用。

UPDATE poste_dates SET booking_id =  1 WHERE poste_id = 1 AND date >= '2019-07-10' ORDER BY date LIMIT 4;
UPDATE poste_dates SET booking_id =  4 WHERE poste_id = 1 AND date >= '2019-07-14' ORDER BY date LIMIT 1;
UPDATE poste_dates SET booking_id =  7 WHERE poste_id = 1 AND date >= '2019-07-16' ORDER BY date LIMIT 4;
UPDATE poste_dates SET booking_id =  2 WHERE poste_id = 2 AND date >= '2019-07-10' ORDER BY date LIMIT 2;
UPDATE poste_dates SET booking_id =  9 WHERE poste_id = 2 AND date >= '2019-07-13' ORDER BY date LIMIT 2;
UPDATE poste_dates SET booking_id =  5 WHERE poste_id = 3 AND date >= '2019-07-15' ORDER BY date LIMIT 2;
UPDATE poste_dates SET booking_id =  8 WHERE poste_id = 3 AND date >= '2019-07-21' ORDER BY date LIMIT 3;
UPDATE poste_dates SET booking_id = 11 WHERE poste_id = 3 AND date >= '2019-07-28' ORDER BY date LIMIT 1;
UPDATE poste_dates SET booking_id = 12 WHERE poste_id = 3 AND date >= '2019-07-29' ORDER BY date LIMIT 1;
UPDATE poste_dates SET booking_id =  3 WHERE poste_id = 4 AND date >= '2019-07-15' ORDER BY date LIMIT 1;
UPDATE poste_dates SET booking_id = 13 WHERE poste_id = 4 AND date >= '2019-07-21' ORDER BY date LIMIT 2;

现在日期如下:

+----------+------------+------------+
| poste_id | date       | booking_id |
+----------+------------+------------+
|        1 | 2019-07-10 |          1 |
|        1 | 2019-07-11 |          1 |
|        1 | 2019-07-12 |          1 |
|        1 | 2019-07-13 |          1 |
|        1 | 2019-07-14 |          4 |
|        1 | 2019-07-16 |          7 |
|        1 | 2019-07-17 |          7 |
|        1 | 2019-07-18 |          7 |
|        1 | 2019-07-19 |          7 |
|        1 | 2019-07-20 |       NULL |
|        2 | 2019-07-10 |          2 |
|        2 | 2019-07-11 |          2 |
|        2 | 2019-07-13 |          9 |
|        2 | 2019-07-14 |          9 |
|        2 | 2019-07-16 |       NULL |
|        2 | 2019-07-17 |       NULL |
|        2 | 2019-07-18 |       NULL |
|        2 | 2019-07-20 |       NULL |
|        3 | 2019-07-15 |          5 |
|        3 | 2019-07-16 |          5 |
|        3 | 2019-07-21 |          8 |
|        3 | 2019-07-22 |          8 |
|        3 | 2019-07-23 |          8 |
|        3 | 2019-07-28 |         11 |
|        3 | 2019-07-29 |         12 |
|        3 | 2019-07-30 |       NULL |
|        4 | 2019-07-15 |          3 |
|        4 | 2019-07-17 |       NULL |
|        4 | 2019-07-18 |       NULL |
|        4 | 2019-07-21 |         13 |
|        4 | 2019-07-22 |         13 |
|        4 | 2019-07-24 |       NULL |
|        4 | 2019-07-25 |       NULL |
|        4 | 2019-07-28 |       NULL |
|        4 | 2019-07-29 |       NULL |
+----------+------------+------------+

现在,使用该表中的booking_id为NULL的表格搜索具有日期的所有帖子非常简单。

SELECT DISTINCT poste_id FROM poste_dates WHERE booking_id IS NULL;

这仍然与您发布2和4的预期结果不同。

  • poste 1包含日期2019-07-20,因为模式为1111101-1111,该日期在20号放置了1,但没有预订1号张贴在20号。因此1尚未完全预订。
  • 邮局3包含日期2019-07-30,因为模式为1100001-1100001-11,该日期在30号上放置了1,但没有邮局3的预订涵盖了30号。因此,3位客人没有被预订满。

答案 3 :(得分:1)

从8.0开始,您可以使用即时生成的数字表和LATERAL的少许帮助来完成此操作。您可能希望创建一个持久性数字表。

with e1(n) as (
        select 1 union all select 1 union all select 1 union all
        select 1 union all select 1 union all select 1 union all
        select 1 union all select 1 union all select 1 union all select 1
), e2(n) as (select 1 from e1 a, e1 b), -- 100 rows
   e4(n) as (select 1 from e2 a, e2 b), -- 10,000 rows
numbers(n) as (
   select row_number() over(order by n) N from e4
)
select distinct poste_id, pattern, start, `end` 
from postes p 
join numbers n on adddate(start, n.N-1) <= `end`
   --  compute the date and respective position in the pattern for further usage
   , lateral (select adddate(start, n.N-1) dt, (n.N-1) % length(pattern) + 1 pos) x
where substring(pattern, x.pos, 1)
and not exists (
     select 1 
     from bookings b
     where b.poste_id = p.poste_id and x.dt >= b.start_datetime and x.dt <= adddate(b.start_datetime, b.number_day))
order by p.poste_id;

Fiddle