Mysql Select Query to find items also partially free between two given dates

Viewed 110

I am in need of listing free items between two given dates. I have in table A items and in table B their occupancies. When I search for the free items between two dates, I need to list also items partially free.

Tabel A:
| ID       | ITEM_NAME      |
| -------- | -------------- |
| 1        | Item1          |
| 2        | Item2          |
| 3        | Item3          |

Table B:
|id_item     |occupancy_start_date     |occupancy_end_date|
|---------------------------------------------------------|
|1           |2021-07-24               |2021-08-06        |
|2           |2021-07-24               |2021-07-31        |
|3           |2021-07-29               |2021-08-03        |

While I search for free items between 2021-07-24 and 2021-08-06, I must get Item2 and Item3.

Item2 is free from 2021-08-01 till 2021-08-06
Item3 is free from 2021-07-24 till 2021-07-29
Item3 is free from 2021-08-04 till 2021-08-06

(Practically I must find free slots of dates between two given dates by the user)

Can you guys help me? Thank you.

2 Answers

Without window function :

There is a way to extract by dividing into three parts: "From start date", "Till end date" and "Others", and filter the rows by each query, like this:

SET @sdate = DATE'2021-07-24';
SET @edate = DATE'2021-08-06';

-- From start date
SELECT
    b.id_item id_item,
    @sdate from_date,
    DATE_ADD(MIN(b.occupancy_start_date), INTERVAL -1 DAY) to_date
FROM TableB b
GROUP BY b.id_item
HAVING from_date <= to_date
-- Till end date
UNION ALL
SELECT
    b.id_item id_item,
    DATE_ADD(MAX(b.occupancy_end_date), INTERVAL 1 DAY) from_date,
    @edate to_date
FROM TableB b
GROUP BY b.id_item
HAVING from_date <= to_date
-- Others
UNION ALL
SELECT
    b1.id_item,
    DATE_ADD(b1.occupancy_end_date, INTERVAL 1 DAY),
    DATE_ADD(b2.occupancy_start_date, INTERVAL -1 DAY)
FROM TableB b1 INNER JOIN TableB b2
    ON b1.id_item=b2.id_item AND
    b1.occupancy_end_date < b2.occupancy_start_date AND
    -- Exclude inappropriate rows
    NOT EXISTS (SELECT 1 FROM TableB b WHERE
        b1.id_item = b.id_item AND (
        ( b1.occupancy_end_date < b.occupancy_start_date AND
          b.occupancy_start_date < b2.occupancy_start_date) OR
        ( b1.occupancy_end_date < b.occupancy_end_date AND
          b.occupancy_end_date < b2.occupancy_start_date) ) )
ORDER BY 1,2
;

DB Fiddle

With window function :

If MySQL8, you can use the LAG (or LEAD) function, like this:

SET @sdate = DATE'2021-07-24';
SET @edate = DATE'2021-08-06';

SELECT
    a.ITEM_NAME,
    w.id_item,
    w.from_date,
    w.to_date
FROM (
  -- Non-free period(s) exist
  SELECT * FROM (
    SELECT
      id_item,
      DATE_ADD(LAG(occupancy_end_date, 1)
        OVER (PARTITION BY id_item ORDER BY occupancy_end_date),
        INTERVAL 1 DAY) from_date,
      DATE_ADD(occupancy_start_date, INTERVAL -1 DAY) to_date
    FROM TableB
    WHERE @sdate < occupancy_end_date AND occupancy_start_date < @edate
  ) t
  WHERE from_date <= to_date
  -- From start date
  UNION ALL
  SELECT
      id_item,
      @sdate from_date,
      DATE_ADD(MIN(occupancy_start_date), INTERVAL -1 DAY) to_date
  FROM TableB
  GROUP BY id_item
  HAVING from_date <= to_date
  -- Till end date
  UNION ALL
  SELECT
      id_item,
      DATE_ADD(MAX(occupancy_end_date), INTERVAL 1 DAY) from_date,
      @edate to_date
  FROM TableB
  GROUP BY id_item
  HAVING from_date <= to_date
  -- No occupancy
  UNION ALL
  SELECT
      ID,
      @sdate from_date,
      @edate to_date
  FROM TableA a
  WHERE NOT EXISTS (SELECT 1 FROM TableB b WHERE a.ID=b.id_item)
) w
INNER JOIN TableA a ON w.id_item=a.ID
ORDER BY
    w.id_item,
    w.from_date
;

DB Fiddle

Here's another starting point (using the fiddle generously provided by others - pushed up to a more recent version) ... I'll post a more complete answer next week, if no one's beaten me to it...

WITH RECURSIVE cte AS (
   SELECT '2021-07-24' dt
    UNION ALL
   SELECT dt + INTERVAL  1 DAY
     FROM cte
    WHERE dt < '2021-08-06')
   SELECT x.dt
        , a.*
     FROM cte x 
     JOIN TableA a
     LEFT
     JOIN TableB b
       ON b.id_item = a.id
      AND x.dt BETWEEN b.occupancy_start_date AND b.occupancy_end_date
    WHERE b.id_item IS NULL
Related