Searching for availability with MySQL (and PHP)?

前端 未结 2 1345
别跟我提以往
别跟我提以往 2020-12-30 17:23

I have two MySQL (MyIsAm) tables that represent letting units and bookings:

  • LettingUnits (ID, Name, etc...)
  • LettingUnitBooki
相关标签:
2条回答
  • 2020-12-30 17:48

    It's not pretty.

    • Join LettingUnitBookings to itself
    • Find the start and end of gaps between bookings for each F_LU_ID
    • Get the size of the gaps - the available 'slots'
    • Consider the case where there are no existing bookings that 'bracket' a suitable slot, add in outlier dates for this
    • Join that projection to the LettingUnits table and apply WHERE criteria (start, end, duration)

    I've neglected to include BookingUnits that have no bookings at all.

    Ends up looking like this:

    SELECT @StartOfWindow := '2009-01-01',
           @EndOfWindow   := '2009-02-01',
           @WindowSize    := 5
    ;
    
    SELECT
        lu.Name,
        Slots.*
    FROM (
        SELECT
            lub1.F_LU_ID,
            DATE_ADD( MAX( lub2.date_time ), INTERVAL 1 DAY )     AS StartOfSlot,
            DATE_SUB( lub1.date_time, INTERVAL 1 DAY )            AS EndOfSlot,
            DATEDIFF( lub1.date_time, MAX( lub2.date_time ) ) - 1 AS AvailableDays
        FROM
        ( SELECT F_LU_ID, Start AS date_time FROM LettingUnitBookings
          UNION
          SELECT F_LU_ID, CAST( '9999-12-31' AS DATE ) FROM LettingUnitBookings
        ) AS lub1,
        ( SELECT F_LU_ID, End   AS date_time FROM LettingUnitBookings
          UNION
          SELECT F_LU_ID, CAST( '1000-01-01' AS DATE ) FROM LettingUnitBookings
        ) AS lub2
        WHERE
            lub2.date_time <= lub1.date_time
        AND lub2.F_LU_ID = lub1.F_LU_ID
        GROUP BY
            lub1.F_LU_ID,
            lub1.date_time
        ) Slots
    JOIN LettingUnits lu
    ON   lu.ID = Slots.F_LU_ID
    WHERE
        Slots.AvailableDays >= @WindowSize
    AND (
       (     DATEDIFF( Slots.EndOfSlot, @EndOfWindow )     >= @WindowSize
         AND DATEDIFF( @StartOfWindow, Slots.StartOfSlot ) >= @WindowSize
       )
       OR
       (     DATEDIFF( @EndOfWindow, Slots.StartOfSlot ) >= @WindowSize
         AND DATEDIFF( Slots.EndOfSlot, @StartOfWindow ) >= @WindowSize
       )
    )
    

    Gives

    Name        F_LU_ID StartOfSlot EndOfSlot  AvailableDays
    Foo Cottage 123     2009-01-06  2009-01-09 5
    Foo Cottage 123     2009-01-21  2009-01-24 5
    

    Hopefully that can be adapted to suit your needs.

    Alternatively, if a booking can start on the same day that the previous booking ends, you can adapt slightly...

    SELECT
        lu.Name,
        Slots.*
    FROM (
        SELECT
            lub1.F_LU_ID,
            MAX( lub2.date_time ) AS StartOfSlot,
            lub1.date_time        AS EndOfSlot,
            DATEDIFF( lub1.date_time, MAX( lub2.date_time )) AS AvailableDays
        FROM
        ( SELECT F_LU_ID, Start AS date_time FROM LettingUnitBookings
          UNION
          SELECT F_LU_ID, CAST( '9999-12-31' AS DATE ) FROM LettingUnitBookings
        ) AS lub1,
        ( SELECT F_LU_ID, End   AS date_time FROM LettingUnitBookings
          UNION
          SELECT F_LU_ID, CAST( '1000-01-01' AS DATE ) FROM LettingUnitBookings
        ) AS lub2
        WHERE
            lub2.date_time <= lub1.date_time
        AND lub2.F_LU_ID = lub1.F_LU_ID
        GROUP BY
            lub1.F_LU_ID,
            lub1.date_time
        ) Slots
    JOIN LettingUnits lu
    ON   lu.ID = Slots.F_LU_ID
    WHERE
        Slots.AvailableDays >= @WindowSize
    AND
       (     DATEDIFF( Slots.EndOfSlot, @EndOfWindow )     >= @WindowSize
         AND DATEDIFF( @StartOfWindow, Slots.StartOfSlot ) >= @WindowSize
       )
       OR
       (     DATEDIFF( @EndOfWindow, Slots.StartOfSlot ) >= @WindowSize
         AND DATEDIFF( Slots.EndOfSlot, @StartOfWindow ) >= @WindowSize
       )
    
    0 讨论(0)
  • 2020-12-30 18:09

    Here's a solution that seems to work:

    SELECT t.*, DATEDIFF(t.LatestAvailable, t.EarliestAvailable) AS LengthAvailable
    FROM 
       (SELECT u.*,
          COALESCE(b1.End, @StartOfWindow) AS EarliestAvailable,
          COALESCE(b2.Start, @EndOfWindow) AS LatestAvailable
        FROM LettingUnits u
        LEFT OUTER JOIN LettingUnitBookings b1
          ON (u.ID = b1.F_LU_ID AND b1.End BETWEEN @StartOfWindow AND @EndOfWindow)
        LEFT OUTER JOIN LettingUnitBookings b2
          ON (u.ID = b2.F_LU_ID AND b2.Start BETWEEN @StartOfWindow AND @EndOfWindow
              AND b2.Start >= b1.End) -- edit: new term
        ) AS t
    LEFT OUTER JOIN LettingUnitBookings x
      ON (t.ID = x.F_LU_ID AND x.Start < t.LatestAvailable AND x.End > t.EarliestAvailable)
    WHERE x.ID IS NULL AND DATEDIFF(t.LatestAvailable, t.EarliestAvailable) >= @WindowSize;
    

    The output is:

    +-----+-------------+-------------------+-----------------+-----------------+
    | ID  | Name        | EarliestAvailable | LatestAvailable | LengthAvailable |
    +-----+-------------+-------------------+-----------------+-----------------+
    | 123 | Foo Cottage | 2009-01-05        | 2009-01-10      |               5 |
    | 123 | Foo Cottage | 2009-01-20        | 2009-01-25      |               5 |
    | 456 | Bar Cottage | 2009-01-20        | 2009-01-31      |              11 |
    +-----+-------------+-------------------+-----------------+-----------------+
    

    Analyzing this with EXPLAIN shows that it employs indexes pretty well:

    +----+-------------+------------+--------+---------------+---------+---------+-------+------+-------------------------+
    | id | select_type | table      | type   | possible_keys | key     | key_len | ref   | rows | Extra                   |
    +----+-------------+------------+--------+---------------+---------+---------+-------+------+-------------------------+
    |  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL    | NULL    | NULL  |    9 | Using where             |
    |  1 | PRIMARY     | x          | ref    | F_LU_ID       | F_LU_ID | 8       | t.ID  |    2 | Using where; Not exists |
    |  2 | DERIVED     | u          | system | NULL          | NULL    | NULL    | NULL  |    1 |                         |
    |  2 | DERIVED     | b1         | ref    | F_LU_ID       | F_LU_ID | 8       | const |    0 |                         |
    |  2 | DERIVED     | b2         | ref    | F_LU_ID       | F_LU_ID | 8       | const |    0 |                         |
    +----+-------------+------------+--------+---------------+---------+---------+-------+------+-------------------------+
    

    Compare with the EXPLAIN report for the solution given by @martin clayton:

    +----+--------------+---------------------+--------+---------------+---------+---------+------+------+---------------------------------+
    | id | select_type  | table               | type   | possible_keys | key     | key_len | ref  | rows | Extra                           |
    +----+--------------+---------------------+--------+---------------+---------+---------+------+------+---------------------------------+
    |  1 | PRIMARY      | lu                  | system | PRIMARY,ID    | NULL    | NULL    | NULL |    1 |                                 |
    |  1 | PRIMARY      | <derived2>          | ALL    | NULL          | NULL    | NULL    | NULL |    4 | Using where                     |
    |  2 | DERIVED      | <derived3>          | ALL    | NULL          | NULL    | NULL    | NULL |    4 | Using temporary; Using filesort |
    |  2 | DERIVED      | <derived5>          | ALL    | NULL          | NULL    | NULL    | NULL |    4 | Using where; Using join buffer  |
    |  5 | DERIVED      | LettingUnitBookings | ALL    | NULL          | NULL    | NULL    | NULL |    3 |                                 |
    |  6 | UNION        | LettingUnitBookings | index  | NULL          | F_LU_ID | 8       | NULL |    3 | Using index                     |
    | NULL | UNION RESULT | <union5,6>          | ALL    | NULL          | NULL    | NULL    | NULL | NULL |                                 |
    |  3 | DERIVED      | LettingUnitBookings | ALL    | NULL          | NULL    | NULL    | NULL |    3 |                                 |
    |  4 | UNION        | LettingUnitBookings | index  | NULL          | F_LU_ID | 8       | NULL |    3 | Using index                     |
    | NULL | UNION RESULT | <union3,4>          | ALL    | NULL          | NULL    | NULL    | NULL | NULL |                                 |
    +----+--------------+---------------------+--------+---------------+---------+---------+------+------+---------------------------------+
    

    In general, you want to avoid optimization plans that force Using filesort or Using temporary because these are performance killers. A query using GROUP BY is almost certain to cause this kind of optimization, at least in MySQL.

    0 讨论(0)
提交回复
热议问题