Get next available date from start date and end date - mysql

I want available dates from database table.
Room number Start_date End_date
room 1 2018-12-01 2018-12-05
room 1 2018-12-08 2018-12-15
room 1 2018-12-20 2018-12-31
Here is I added table from database.
When user select start date and end date than check in database. If already in between date of start date and end date then result want available date.
User start date = 2018-12-05
End date = 2018-12-20
Need result:
2018-12-06
2018-12-07
2018-12-16
2018-12-17
2018-12-18
2018-12-19

To really solve this problem, you need a Calendar table with one row per day.
With such a table, you can do:
select c.date
from calendar c
where c.date >= '2018-12-05' and
c.date <= '2018-12-20' and
not exists (select 1
from t
where c.date >= start_date and
c.date <= end_date
);
I note that this does not take room_number into account at all -- because your question does not mention that at all. If you have a question that involves room_number, ask another question, with appropriate sample data, desired results, and explanation of what you want to accomplish.

Related

How to find datetimes where some conditions hold in MySQL?

We have a MySQL database containing bookings on different courts. Table properties (shortened):
CREATE TABLE `booking` (
`startDate` datetime NOT NULL,
`endDate` datetime NOT NULL,
`courtId` varchar(36),
FOREIGN KEY (`courtId`) REFERENCES `court` (`id`) ON DELETE CASCADE
)
Usually, bookings are paid, but under certain conditions (which I can check in the WHERE-part of a query), bookings can be free.
Given a court and booking duration, I want to query the next datetime at which the booking can be created for free. The conditions are not the problem, the problem is how to query not for entities but for datetime values.
How to realize this efficiently in MySQL?
EDIT: Maybe it helps to outline the conditions under which bookings are free:
The conditions under which bookings are free are dependent on how many courts are offered at the startDate by someone (courts are always offered except if there are special "not-offered"-bookings on that court) and how many other bookings overlapping the startDate are already free. This means bookings can be (and probably are) free even if there are no bookings at all in the database.
Solution
Finding available slot before the last booking :
Find the difference between each booking with it's following one. If the difference is greater than the number of days of the new booking, you can use that slot.
Finding available slot after the last booking :
If there is no such slot, you can assign a day after the end date of the last booking.
If this query returns null, it means there is no booking for the court. You can handle that in the client side.
Code
SET #c := 1; # Court id
SET #n := 2; # Number of days
/*
Previous booking
*/
SET #i := 0;
CREATE TEMPORARY TABLE bp AS
SELECT #i := #i + 1 AS id, startDate, endDate FROM booking
WHERE courtId = #c
ORDER BY startDate;
/*
Next booking
*/
SET #i := -1;
CREATE TEMPORARY TABLE bn AS
SELECT #i := #i + 1 AS id, startDate, endDate FROM booking
WHERE courtId = #c
ORDER BY startDate;
/*
Finding available slot before the last booking (Intermediate slot).
*/
SELECT DATE_ADD(MIN(bp.endDate), INTERVAL 1 DAY) INTO #si FROM
bp
JOIN
bn
ON bn.id = bp.id
WHERE DATEDIFF(bn.startDate, bp.endDate) > #n;
/*
Finding available slot after the last booking
*/
SELECT DATE_ADD(MAX(endDate), INTERVAL 1 DAY) INTO #sa FROM bn;
SELECT IFNULL(#si, #sa);
Using the code
Just replace the values of the variables #c and #n.
An idea to solve this is to rephrase it as: for the given :court_id parameter, give me the smallest future end_time for which no other booking starts within the given :duration parameter.
This can be expressed in different ways in SQL.
With a not exists condition and a correlated subquery that ensures that no further booking on the same court starts within :duration minutes.
select min(b.end_date) next_possible_start_date
from bookings b
where
b.court_id = :court_id
and b.end_date > now()
and not exists (
select 1
from bookings b1
where
b.court_id = :court_id
and b1.start_date > b.end_date
and b1.start_date < DATE_ADD(b.end_date, interval :duration minute)
)
Note: if you have additional conditions, they must be repeated in the where clause of the query and of the subquery.
The same logic as not exists can be impemented with a left join antipattern
select min(b.end_date) next_possible_start_date
from bookings b
left join booking b1
on b1.court_id = b.court_id
and bi1.start_date > b.end_date
and b1.start < DATE_ADD(b.end_date, interval :duration minute)
where
b.court_id = :court_id
and b.end_date > now()
and b1.court_id is null
In MySQL 8.0, it is also possible to use window functions: lag() retrieves the start_date of the next booking, which can then be compared with the end_date of the current booking.
select min(end_date) next_possible_start_date
from (
select
end_date,
lead(start_date) over(partition by court_id order by start_date) next_start_date
from booking b
where court_id = :court_id
)
where
next_start_date is null
or next_start_date >= DATE_ADD(end_date, interval :duration minute)
Edit
Here is a new version of the query that adresses the use case when the court is immediatly free at the time when the search is performed:
select
court_id,
greatest(min(b.end_date), now()) next_possible_start_date
from bookings b
where
-- b.court_id = :court_id and
not exists (
select 1
from bookings b1
where
b1.court_id = b.court_id
and b1.start_date > b.end_date
and b1.start_date < date_add(greatest(b.end_date, now()), interval ::duration minute)
)
group by court_id
Note: this searches for all available courts at once; you can uncomment the where clause to filter on a specific court.
Given this sample data:
court_id | start_date | end_date
-------: | :------------------ | :------------------
1 | 2019-10-29 13:00:00 | 2019-10-29 13:30:00
1 | 2019-10-29 14:00:00 | 2019-10-29 15:00:00
2 | 2019-10-29 23:14:05 | 2019-10-30 00:14:05
2 | 2019-10-30 01:14:05 | 2019-10-30 02:14:05
Court 1 is immedialty free. Court 2 is booked for next hour, then there is a 60 minutes vacancy before the next booking.
If we run the query for a duration of 60 minutes, we get:
court_id | next_possible_start_date
-------: | :-----------------------
1 | 2019-10-29 23:14:05 -- available right now
2 | 2019-10-30 00:14:05 -- available in 1 hour
While for 90 minutes, we get:
court_id | next_possible_start_date
-------: | :-----------------------
1 | 2019-10-29 23:14:05 -- available right now
2 | 2019-10-30 02:14:05 -- available in 3 hours
Demo on DB Fiddle

need active customer list by day wise between selected dates which have start and end date(null also)

we need result of no.active customers between day 1 and day2
the customer will have start and end date some time end date as null i want the active customer between two dates day wise results
cust strdate end
1 2018-01-01 null
2 2018-01-01 2018-01-03
3 2018-01-02 2018-01-02
4 2018-01-04 null
my result for external passed dates 2018-01-01 to 2018-01-04
day count
2018-01-01 2
2018-01-02 3
2018-01-03 2
2018-01-04 2
One option uses a calendar table to represent all the dates which you want to see. For the purpose of your sample data, we can just hard code the dates in a CTE. With actual data, you would want to generate a date range. This means that the subquery below aliased as d would be replaced with an actual table/view containing the dates whose range you want to include in the query.
SELECT
d.date,
COUNT(*) AS count
FROM
(
SELECT '2018-01-01' AS date UNION ALL
SELECT '2018-01-02' UNION ALL
SELECT '2018-01-03' UNION ALL
SELECT '2018-01-04'
) d
LEFT JOIN yourTable t
ON d.date >= t.strdate AND (d.date <= t.enddate OR t.enddate IS NULL)
GROUP BY
d.date
ORDER BY
d.date;
Demo
This answer assumes that if the end date be NULL, then it means that the right side of the range is completely open, i.e. all dates would match, provided their start dates are also within range.

SQL query to find free rooms in hotel

I use a query to find free rooms from hotel DB. I wrote a query which select rooms that not in booking table:
SELECT * FROM room WHERE roomId NOT IN
(SELECT roomId FROM booking b WHERE STR_TO_DATE('${endDate}', '%m-%d-%Y') <= b.endDate AND
STR_TO_DATE('${startDate}', '%m-%d-%Y') >= b.startDate);
My booking table looks like:
+-----------+------------+------------+--------+---------+
| bookingId | startDate | endDate | roomId | guestId |
+-----------+------------+------------+--------+---------+
| 1 | 2016-03-12 | 2016-03-22 | 1 | 1 |
+-----------+------------+------------+--------+---------+
But if my startDate is 2016-03-10 and endDate is 2016-03-25 - I've got already booked room from 2016-03-12 to 2016-03-22. How I can fix it? I don't need to show room that booked between my dates.
General approach for the problem of finding free rooms in range ($BOOKING_BEGIN <=> $BOOKING_END) would be like:
SELECT
rooms.room_id
FROM
rooms
LEFT JOIN
bookings
ON (
bookings.room_id = rooms.room_id AND
NOT (
(bookings.begin < $BOOKING_BEGIN and bookings.end < $BOOKING_BEGIN)
OR
(bookings.begin > $BOOKING_END and bookings.end > $BOOKING_END)
)
)
WHERE
bookings.room_id IS NULL;
Which simply means 'take all the rooms in the hotel, and join them with ones which are already booked. If there's null, that means room is free in a given range (Join didn't find existing booking).
Here is the query that works, and has been tested for all combinations of vacancy before any other. vacancy after anything. Start date before, on, after existing start. End date before, on, after existing end date. Totally straddling outside another booking. And totally within another booking.
select
r.RoomID
from
Room r
LEFT JOIN
( select
b.RoomID
from
booking b,
( select #parmStartDate := '2016-01-21',
#parmEndDate := '2016-01-23' ) sqlvars
where
b.EndDate >= #parmStartDate
AND b.StartDate <= #parmEndDate
AND ( timestampdiff( day, b.StartDate, #parmEndDate )
* timestampdiff( day, #parmStartDate, b.EndDate )) > 0 ) Occupied
ON r.RoomID = Occupied.RoomID
where
Occupied.RoomID IS NULL;
The sample booking data I created included
BookID RoomID StartDate EndDate
1 1 2016-02-03 2016-02-04
2 1 2016-02-04 2016-02-08
3 1 2016-02-12 2016-02-16
4 1 2016-02-20 2016-02-28
I then tested with the following booking dates and came up with the following valid vacancy vs conflict and already occupied. This test is just for a single room, but obviously applicable for any room in the hotel.
Both dates before anything on file... Room available
2016-01-10 - 2016-01-15
Both dates after anything on file... Room available
2016-03-10 - 2016-03-15
Occupied ID 1 -- Same start date
2016-02-03 - 2016-02-04
Occupied ID 2 -- Same start date, but less than existing occupied end date
2016-02-04 - 2016-02-05
Occupied ID 2 -- Same start, Exceeds end occupancy date
2016-02-04 - 2016-02-09
Occupied ID 3 -- Start before, but end date WITHIN existing booking
2016-02-09 - 2016-02-13
Available. The END Date is the START Date of the existing booking
(Between 2 & 3 booking)
2016-02-09 - 2016-02-12
Occupied ID 3 -- Started within date, but end outside existing booking
2016-02-15 - 2016-02-17
Available. End of existing booking and nothing booked on 2/17
2016-02-16 - 2016-02-17
Occupied ID 3 -- Completely encompasses booking entry
2016-02-11 - 2016-02-17
Occupied ID 4 -- totally WITHIN another entry
2016-02-21 - 2016-02-23
Now, to explain what is going on. I did with a LEFT-JOIN and looking for NULL (ie: no conflict of another booking) which is quite similar to your NOT IN subselect. So I will skip that part.
First, the FROM clause. So I dont have to "declare" variables like a stored procedure, I am doing them IN-LINE via the #parmStartDate, #parmEndDate and assigning the alias sqlvars just for declaration purposes. Since this returns one row, having a Cartesian applied to the booking table is no problem.
from
booking b,
( select #parmStartDate := '2016-01-21',
#parmEndDate := '2016-01-23' ) sqlvars
Now, the WHERE clause. If your table has years worth of bookings after time, and 100's of rooms, this could get quite large quickly, so I want to pre-start with only those dates where existing bookings would come into place this is the
where
b.EndDate >= #parmStartDate
AND b.StartDate <= #parmEndDate
At a minimum, I only care about those bookings where an existing checkout date is AT LEAST the date you are trying to find availability. Ex: You are looking for a check-in date of July 4th. Why would you even care if someone checked out in Feb, Mar, Apr, etc... So now, how far out do you go... You also only care for those records where the next existing booking has a START Date UP TO the day you would be checking out. So, if checking out July 6th, you don't care about any bookings for July 7th or after. So far, so good.
Now, comes the how do I know if a room is occupied or not. I was having difficulties comparing existing Start Date to looking for dates and was getting false answers, so I had to resort to date math and comparing start to end and end to start, and if the multiplier result was positive, there is a conflict.
AND ( timestampdiff( day, b.StartDate, #parmEndDate )
* timestampdiff( day, #parmStartDate, b.EndDate )) > 0 )
Since we already know we have records within the POSSIBLE date range, this is doing a conflict check in either direction for full outside, inside, conflict left or conflict right. it just works.
You would have to see it to understand it better and this is the query that I ran so you could look at the results for yourself. Just plug in the respective start / end dates you are looking for.
select
b.BookID,
b.RoomID,
b.StartDate,
b.EndDate,
#parmStartDate as pStart,
#parmEndDate as pEnd,
( timestampdiff( day, b.StartDate, #parmEndDate )
* timestampdiff( day, #parmStartDate, b.EndDate )) <= 0 as Available,
( timestampdiff( day, b.StartDate, #parmEndDate )
* timestampdiff( day, #parmStartDate, b.EndDate )) > 0 as Occupied
from
booking b,
( select #parmStartDate := '2016-01-21',
#parmEndDate := '2016-01-23' ) sqlvars
Good Luck...

Mysql Unique records, where multiple records exist

I am struggling with a Mysql call and was hoping to borrow your expertise.
I believe that what I want may only be possible using two selects and I have not yet done one of these and am struggling to wrap my head around this.
I have a table like so:
+------------------+----------------------+-------------------------------+
| username | acctstarttime | acctstoptime |
+------------------+----------------------+-------------------------------+
| bill | 22.04.2014 | 23.04.2014 |
+------------------+----------------------+-------------------------------+
| steve | 16.09.2014 | |
+------------------+----------------------+-------------------------------+
| fred | 12.08.2014 | |
+------------------+----------------------+-------------------------------+
| bill | 24.04.2014 | |
+------------------+----------------------+-------------------------------+
I wish to select only unique records from the username column ie I only want one record for bill and I need the one with most recent start_date, providing they were weren't in the last three months (end_date is not important to me here) else I do not want any data. In summary I just need anyone where there most recent start date is over 3 months old.
The command I am using currently is:
SELECT DISTINCT(username), ra.acctstarttime AS 'Last IP', ra.acctstoptime
FROM radacct AS ra
WHERE ra.acctstarttime < DATE_SUB(now(), interval 3 month)
GROUP BY ra.username
ORDER BY ra.acctstarttime DESC
However, this simply gives me details about the date_start for that particular customer where they had a start date over 3 months ago.
I have tired a few other combinations of this and have tried a command with a double select but I'm currently hitting brick walls. Any help or a push in the right direction would be much appreciated.
Update
I have created the following:
http://sqlfiddle.com/#!2/f47b2/1
Effectively I should only see 1 row when the query is as it should be. This would be the row for bill. As he is the only one that does not have a start date within the last three months. The result I would expect to see is the following:
24 bill April, 11 2014 12:11:40+0000 (null)
As this is the latest start date for bill, but this start date is not within the last three months. Hopefully this will help clarify. Many thanks for your help thus far.
http://sqlfiddle.com/#!2/f47b2/14
This is another example. If the acctstartdate for bill would show as the April entry, then I could add my where clause for the last three months and this would give me my desired result.
SQLFiddle
http://sqlfiddle.com/#!2/444432/9 (MySQL 5.5)
I am looking at the question in 2 ways based on the current text:
I only want one record for bill and I need the one with most recent start_date, providing they were in the last three months (end_date is not important to me here) else I do not want any data
Structure
create table test
(
username varchar(20),
date_start date
);
Data
Username date_start
--------- -----------
bill 2014-09-25
bill 2014-09-22
bill 2014-05-26
andy 2014-05-26
tim 2014-09-25
tim 2014-05-26
What we want
Username date_start
--------- -----------
bill 2014-09-25
tim 2014-09-25
Query
select *
from test a
inner join
(
select username, max(date_start) as max_date_start
from test
where date_start > date_sub(now(), interval 3 month)
group by username
) b
on
a.username = b.username
and a.date_start = b.max_date_start
where
date_start > date_sub(now(), interval 3 month)
Explanation
For the most recent last 3 months, let's get maximum start date for each user. To limit the records to the latest 3 months we use where date_start > date_sub(now(), interval 3 month) and to find the maximum start date for each user we use group by username.
We, then, join main data with this small subset based on user and max date to get the desired result.
Another angle
If we desire to NOT look at the latest 3 months and instead find the most recent date for each user, we would be looking at this kind of data:
What we want
Username date_start
--------- -----------
bill 2014-05-26
tim 2014-05-26
andy 2014-05-26
Query
select *
from test a
inner join
(
select username, max(date_start) as max_date_start
from test
where date_start < date_sub(now(), interval 3 month)
group by username
) b
on
a.username = b.username
and a.date_start = b.max_date_start
where
date_start < date_sub(now(), interval 3 month)
Hopefully you can change these queries to your liking.
EDIT
Based on your good explanation, here's the query
SQLFiddle: http://sqlfiddle.com/#!2/f47b2/17
select *
from activity a
-- find max dates for users for records with dates after 3 months
inner join
(
select username, max(acctstarttime) as max_date_start
from activity
where acctstarttime < date_sub(now(), interval 3 month)
group by username
) b
on
a.username = b.username
and a.acctstarttime = b.max_date_start
-- find usernames who have data in the recent three months
left join
(
select username, count(*)
from activity
where acctstarttime >= date_sub(now(), interval 3 month)
group by username
) c
on
a.username = c.username
where
acctstarttime < date_sub(now(), interval 3 month)
-- choose users who DONT have data from recent 3 months
and c.username is null
Let me know if you would like me to add explanation
Try this:
select t.*
from radacct t
join (
select ra.username, max(ra.acctstarttime) as acctstarttime
from radacct as ra
WHERE ra.acctstarttime < DATE_SUB(now(), interval 3 month)
) s on t.username = s.username and t.acctstarttime = s.acctstarttime
SQLFiddle

Select most recent monthly anniversary day from a unix timestamp

I have an app that inserts a Unix timestamp on registration. What I'd like to do, is calculate usage details for the month since the last monthly anniversary day. So I would need a unix timestamp of what the most recent anniversary day would be.
For example, if a registration is submitted on January 5, the customer's anniversary day is the 5th. So to check usage on February 15th, I need to retrieve all entries from the logs since Feb 5.
Getting the day of registration is easy:
SELECT DATE_FORMAT(FROM_UNIXTIME(created), '%d') FROM accounts
however, I'm lost finding the unix timestamp of the last anniversary date based on the registration day. How would I do that? To clarify, I'm looking to return all action_id created on or after the most recent anniversary date.
Tables:
accounts
+------------+------------+
| account_id | created |
+------------+------------+
| 1 | 1321838910 |
+------------+------------+
....
logs
+------------+------------+------------+
| account_id | action_id | created |
+------------+------------+------------+
| 1 | 233 | 1348249244 |
+------------+------------+------------+
| 1 | 263 | 1348257653 |
+------------+------------+------------+
....
Note: to keep things simple, I'm going to forgo figuring out what happens if an anniversary day is the 31st for example - that is, unless someone has a super ninja statement that takes those occurrences into account.
Not tested. See what you think. Logic is to:
Get the last day of the current month.
Add the account created day number of days to #1 result.
If current day is greater than created day, subtract 1 month from #2 result. Else subtract 2 months.
SELECT l.*
FROM accounts a
LEFT JOIN logs l
ON a.account_id = l.account_id
AND l.created >= UNIX_TIMESTAMP(
DATE_SUB(DATE_ADD(LAST_DAY(NOW()), INTERVAL DAY(FROM_UNIXTIME(a.created)) DAY),
INTERVAL IF(DAY(NOW()) > DAY(FROM_UNIXTIME(a.created)), 1, 2) MONTH));
Edit
I gave this some more thought and perhaps the query below will work regardless of when the anniversary date is. Assumption I made that if the anniversary day is not in a particular month then last day of the month should be taken. It's ugly but I put in some variables to make it more concise, there must be a nicer way. Anyway, I haven't tested but logic as follows.
If current day > anniversay day then just subtract the difference in days to get date.
else if the last day of the previous month is less than anniversary day then use the last day of previous month.
else subtract the day difference between anniversary day and last day of previous month from last date of previous month.
SELECT l.*
FROM accounts a
JOIN logs l
ON a.account_id = l.account_id
AND l.created >= UNIX_TIMESTAMP(
IF(#dNow := DAY(NOW()) >= #dCreated := DAY(FROM_UNIXTIME(a.created)),
DATE_SUB(NOW(), INTERVAL #dNow - #dCreated DAY),
IF(DAY(#endLastMonth := LAST_DAY(DATE_SUB(NOW(), INTERVAL 1 MONTH))) <= #dCreated,
#endLastMonth,
DATE_SUB(#endLastMonth, INTERVAL DAY(#endLastMonth) - #dCreated DAY))));
Perhaps this using order by desc to get most recent created date?
SELECT DATE_FORMAT(FROM_UNIXTIME(X.created), '%d')
FROM (
SELECT CREATED
FROM mytable
WHERE ACCOUNT_ID = ? -- customer id
AND DATE_DIFF(DATE_FORMAT(FROM_UNIXTIME(CREATED),'%Y-%m-%d'), NOW()) MOD 30 = 0
AND DATE_DIFF(DATE_FORMAT(FROM_UNIXTIME(CREATED),'%Y-%m-%d'), NOW()) / 30 = 1
ORDER BY CREATED DESC LIMIT 1)X;