MySQL get NOT overlapping date ranges - mysql

I have seen so many questions similar to this, but they all seem to be tailored to highlighting when date ranges are overlapping, I need the opposite.
Lets say I have a table like so
id| start_date | end_date | room_id
1 | 15/05/2018 | 30/06/2020 | 1
2 | 01/11/2018 | 31/10/2019 | 2
3 | 01/08/2020 | 31/07/2022 | 1
4 | 01/12/2019 | 30/11/2021 | 2
5 | 01/08/2020 | 31/07/2022 | 3
As you can see there are multiple bookings for each room. I need to be able to specify either a single start/end date or both, and get back what DOESN'T overlap (i.e, the available rooms)
For example, if i specified just a start date of 01/05/2018 then every room will return, or if i specify just an end date of 30/07/2020 then every room will return because neither of those dates are between the start and end date of each booking. Even though id 1 has a booking that ends on 30/06/2020 and a new one that starts on 01/08/2020, it would still be available because someone could book between those 2 dates.
If I specified both start and end dates, it searches through and returns only the rooms that have no bookings between the 2 dates at all.
I have read plenty of questions online and the logic seems to be
SELECT *
FROM bookings
WHERE $start_date < expiry_date AND $end_date > start_date
which i understand, but if I ran this query above with the following dates
SELECT *
FROM bookings
WHERE '2018-10-01' < expiry_date AND '2019-10-01' > start_date
it returns
id| start_date | end_date | room_id
1 | 15/05/2018 | 30/06/2020 | 1
2 | 01/11/2018 | 31/10/2019 | 2
How do I get it so that when I pass either a start date, end date or BOTH it returns the rooms that are available?

By De Morgan's Laws, we can negate the overlapping range query you gave as follows:
SELECT *
FROM bookings
WHERE $start_date >= expiry_date OR $end_date <= start_date;
The expression ~(P ^ Q) is equivalent to ~P V ~Q.

Related

Show only available time slots in select field

I am working on a booking system where users can book certain services online. I am stuck on finding and displaying available time slots within a specific day. I know the length of the needed time slot is 1 hour and the business hours.
Is there a way to show time slots that has not yet been booked on a certain day and display only the available time slots that is available to be booked in a dropdown select form?
If a customer selects a specific day and clicks "Select Day" then it needs to query the DB and return the results.
My SQL structure is as follows
|id | title | start_time | end_time | booking_date |
| 1 | Name1 | 2022-05-12 08:00:00 | 2022-05-12 09:00:00 | 2022-05-12 |
| 2 | Name2 | 2022-05-12 10:00:00 | 2022-05-12 11:00:00 | 2022-05-12 |
| 3 | Name3 | 2022-05-12 13:00:00 | 2022-05-12 14:00:00 | 2022-05-12 |
| 4 | Name4 | 2022-05-12 14:00:00 | 2022-05-12 15:00:00 | 2022-05-12 |
as per above the select form should display the timeslots that is not already taken.
09:00 - 10:00
12:00 - 13:00
15:00 - 16:00
It would be something like:
select
id, title
from
<table>
where
start_time between '2022-05-12 00:00:00' and '2022-05-12 11:59:59'
and
booking_date is null
I don't know the name of your table, so you would need to replace <table> with that. I'm also assuming that "booking_date" will have a value to indicate that time slot has been reserved, that it's a date field, and it will be null if that slot hasn't been selected. However, booking_date could have a different purpose.
This is a lazy answer
(because I think just use SQL will do it, use subSelect and other function, but I don't know how to do, sorry.)
get today occupy time:
SELECT id, TIME(start_time) AS s_time FROM tablename
WHERE start_time >= '2022-05-12 00:00:00'
AND start_time < '2022-05-13 00:00:00'
diff time in php:
$sqlResult = []; // sql result
$timeAll = [
'00:00:00',
'01:00:00',
'02:00:00',
'03:00:00',
... // TODO: we need fill it
'23:00:00',
];
foreach ($sqlResult as $item) {
if (isset($timeAll[$item['s_time']])) {
unset($timeAll[$item['s_time']]);
}
}
return $timeAll;
// TODO: javascript or other client code can use it.
ref knowledge link:
MySQL SELECT WHERE datetime matches day (and not necessarily time)
If you choose 2022-05-26, and Peter is occupying room F25 from 2022-05-16 until 2022-05-29, it means the date you select must be out of this range.
So, the query below will only return rooms that were not booked on that day.
SELECT b.id, b.room_id as available_room FROM booking as b
WHERE(
unix_timestamp('$mydate')
NOT BETWEEN unix_timestamp(b.start_date)
AND unix_timestamp(b.end_time)
)
AND unix_timestamp(b.end_time) < unix_timestamp('$mydate');
Assuming $mydate is the variable that contains the date selected by the user, the above query will return rooms that will be available in the future on that particular day.

Getting records by date is multiples of 30 days

I have the following query to get appointments that need remind once a month if they are not done yet. I want to get records with 30, 60, 90, 120,etc... in the past from the current date.
SELECT
a.*
FROM
appointments a
WHERE
DATEDIFF(CURDATE(), a.appointment_date) % 30 = 0
is there another way not to use DATEDIFF to achieve this? I want to increase the performance of this query.
Ok, lets all put the dates and date-diff aside for a moment. Looking at the question, the person is trying to look for all appointments in the past that dont necessarily have another in the future. Such as doing a FOLLOW-UP appointment with a Dr. "Come back in a month to see where things change". This points me to thinking there is probably some patient ID in the table of appointments. So this probably turns the question to looking at the past 30, 60 or 90 days ago to see if there was a corresponding appointment scheduled in the future. If already scheduled, the patient does not need a call reminder to get into the office.
That said, I would start a bit differently, get all patients that DID have an appointment within the last 90 days, and see if they already have (or not) a follow-up appointment already on the schedule for the follow-up. This way, the office person can make contacts with said patients to get on the calendar.
start by getting all maximum appointments for any given patient within the last 90 days. If someone had an appointment 90 days ago, and had a follow-up at 59 days, then they probably only care about the most recent appointment to make sure THAT has the follow-up.
select
a1.patient_id,
max( a1.appointment_date ) MostRecentApnt
from
appointments a1
WHERE
a1.appointment_date > date_sub( a1.appointment_date, interval 90 day )
group by
a1.patient_id
Now, from this fixed list and beginning date, all we care is, how many days to current is there last appointment. IS it X number of days? Just use datediff and sort. You can visually see the how many days. By trying to break them into buckets of 30, 60 or 90 days, just knowing how many days since the last appointment is probably just as easy as sorting in DESCENDING order with the oldest appointments getting called on first, vs those that just happened. Maybe even cutting off the calling list at say 20 days and still has not made an appointment and getting CLOSE to the expected 30 days in question.
SELECT
p.LastName,
p.FirstName,
p.Phone,
Last90.Patient_ID,
Last90.MostRecentApnt,
DATEDIFF(CURDATE(), Last90.appointment_date) LastAppointmentDays
FROM
( select
a1.patient_id,
max( a1.appointment_date ) MostRecentApnt
from
appointments a1
WHERE
a1.appointment_date > date_sub( a1.appointment_date, interval 90 day )
group by
a1.patient_id ) Last90
-- Guessing you might want patient data to do phone calling
JOIN Patients p
on Last90.Patient_id = p.patient_id
order by
Last90.MostRecentApnt DESC,
p.LastName,
p.FirstName
Sometimes, having an answer just for the direct question doesnt get the correct need. Hopefully I am more on-target with the desired ultimate outcome needs. Again, the above implies joining to the patient table for follow-up call purposes to schedule an appointment.
You could use the following query which compares the day of the month of the appointement to the day of the month of today.
We also test whether we are the last day of the month so as to get appointements due at the end of the month. For example if we are the 28th February (not a leap year) we will accept days of the month >= 28, ie 29, 30 & 31, which would otherwise be missed.
This method has the same problem as your current system, that appointements falling during the weekend will be missed.
select a.*
from appointements a,
(select
day(now()) today,
case when day(now())= last_day(now()) then day(now()) else 99 end lastDay
) days
where d = today or d >= lastDay;
You just want the appointments for 30 days in the future? Are they stored as DATE? Or DATETIME? Well, this works in either case:
SELECT ...
WHERE appt_date >= CURDATE() + INTERVAL 30 DAY
AND appt_date < CURDATE() + INTERVAL 31 DAY
If you have INDEX(appt_date) (or any index starting with appt_date), the query will be efficient.
Things like DATE() are not "sargable", and prevent the use of an index.
If your goal is to nag customers, I see nothing in your query to prevent nagging everyone over and over. This might need a separate "nag" table, where customers who have satisfied the nag can be removed. Then performance won't be a problem, since the table will be small.
If your primary concern is to speed up this query we can add a column int for comparing the number of days and index it. We then add triggers to calculate the modulus of the datediff between the start of the Unix period: 01/01/1970 (or any other date if you prefer) and store the result in this column.
This will take a small amount of storage space, and slow down insert and update operations. This will not be noticable when we add or modify one appointment at the time, which I suspect to be the general case.
When we query our table we calculate the day value of today, which will take very little time as it will only be done once, and compare it with the days column which will be very quick because it is indexed and there are no calculations involved.
Finally we run your current query and look at it using explain to see that, even though we have indexed the column date_ , the index cannot be used for this query.
CREATE TABLE appointments (
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
date_ date,
days int
);
CREATE INDEX ix_apps_days ON appointments (days);
✓
✓
CREATE PROCEDURE apps_day()
BEGIN
UPDATE appointments SET days = day(date_);
END
✓
CREATE TRIGGER t_apps_insert BEFORE INSERT ON appointments
FOR EACH ROW
BEGIN
SET NEW.days = DATEDIFF(NEW.date_, '1970-01-01') % 30 ;
END;
✓
CREATE TRIGGER t_apps_update BEFORE UPDATE ON appointments
FOR EACH ROW
BEGIN
SET NEW.days = DATEDIFF(NEW.date_, '1970-01-01') % 30 ;
END;
✓
insert into appointments (date_) values ('2022-01-01'),('2022-01-01'),('2022-04-15'),(now());
✓
update appointments set date_ = '2022-01-12' where id = 1;
✓
select * from appointments
id | date_ | days
-: | :--------- | ---:
1 | 2022-01-12 | 14
2 | 2022-01-01 | 3
3 | 2022-04-15 | 17
4 | 2022-04-22 | 24
select
*
from appointments
where DATEDIFF(CURDATE() , '1970-01-01') % 30 = days;
id | date_ | days
-: | :--------- | ---:
4 | 2022-04-22 | 24
explain
select DATEDIFF(CURDATE() , '1970-01-01')
from appointments
where DATEDIFF(CURDATE() , '1970-01-01') = days;
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra
-: | :---------- | :----------- | :--------- | :--- | :------------ | :----------- | :------ | :---- | ---: | -------: | :----------
1 | SIMPLE | appointments | null | ref | ix_apps_days | ix_apps_days | 5 | const | 1 | 100.00 | Using index
CREATE INDEX ix_apps_date_ ON appointments (date_);
✓
SELECT
a.*
FROM
appointments a
WHERE
DATEDIFF(CURDATE(), a.date_) % 30 = 0
id | date_ | days
-: | :--------- | ---:
4 | 2022-04-22 | 24
explain
SELECT
a.*
FROM
appointments a
WHERE
DATEDIFF(CURDATE(), a.date_) % 30 = 0
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra
-: | :---------- | :---- | :--------- | :--- | :------------ | :--- | :------ | :--- | ---: | -------: | :----------
1 | SIMPLE | a | null | ALL | null | null | null | null | 4 | 100.00 | Using where
db<>fiddle here

MariaDB SELECT all records for only accounts expiring within 30 days

How do I only SELECT records using MariaDB whose accounts are set to expire in 30 days based off of the max() expiration date?
What not to SELECT: if today is December 7, 2017 and below the account "a" has a maximum expiration date of June 1st of 2018 then absolutely no records should be returned for account "a" with the desired SELECT query regardless of the fact that this table also contains rows for account "a" that are older than today's date.
What should be selected: account "b" has two records and it's max() record is less than 30 days; the row id 4 is what row should be returned from the desired SELECT query.
+----+---------+------------+
| id | account | expiration |
+----+---------+------------+
| 1 | a | 2017-01-01 |
| 2 | a | 2018-06-01 |
| 3 | b | 2017-01-01 |
| 4 | b | 2018-01-01 |
+----+---------+------------+
I've spent a bit of time on this in various aspects though my most rigorous testing ultimately fails. Here is one bit that I've got working:
SELECT DATEDIFF(STR_TO_DATE(expiration, '%Y-%m-%d'),CURDATE()) AS days_left FROM table;
You already notice you want MAX.
You want MAX per account, which translates to GROUP BY account in SQL.
You only want to show certain accounts, namely those with MAX within a particular date range. Criteria on aggregations belong in the HAVING clause.
Hence:
select account
from mytable
group by account
having max(expiration) between current_date and current_date + interval 30 day;

Use Max date to create a date range

I need to create a date range in a table that houses transaction information. The table updates sporadically throughout the week from a manual process. Each time the table is updated transactions are added up to the previous Sunday. For instance, the upload took place yesterday and so transactions were loaded through last Sunday (Feb 26th). If it had been loaded on Wednesday it would still be dated for Sunday. The point is that I have a moving target with my transactions and also when the data is loaded to the table. I am trying to fix my look back period to the date of the latest transaction then go three weeks back. Here is the query that I came up with:
SELECT distinct TransactionDate
FROM TransactionTABLE TB
inner join (
SELECT distinct top 21 TransactionDate FROM TrasactionTABLE ORDER BY TransactionDate desc
) A on TB.TransactionDate = A.TransactionDate
ORDER BY TB.TransactionDate desc
Technically this code works. The problem that I am running into now is when there were no transactions on a given date, such as bank holidays (in this case Martin Luther King Day), then the query looks back one day too far.
I have tried a few different options including MAX(TransactionDate) but if I use that in a sub-query or CTE then use the new value in a WHERE statement as a reference I only get the max value or the value I subtract that statement by. For instance if I say WHERE TransactionDate >= MAX(TransactionDate)-21 and the max date is Feb 26th then the result is Feb 2nd instead of the range of dates from Feb 2nd through Feb 26th.
IN SUMMARY, what I need is a date range looking three weeks back from the date of the latest transaction date. This is for a daily report so I cannot hardcode the date in. Since I am also using Excel Connections the use of Declare statements is prohibited.
Thank you StackOverflow gurus in advance!
You could use something like this:
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, dates as (
select top (21)
[Date]=convert(date,dateadd(day, row_number() over (order by (select 1))-1
, dateadd(day,-20,(select max(TransactionDate) from t) ) ) )
from n as deka
cross join n as hecto
order by [Date]
)
select Date=convert(varchar(10),dates.date,120) from dates
rextester demo: http://rextester.com/ZFYV25543
returns:
+------------+
| Date |
+------------+
| 2017-02-06 |
| 2017-02-07 |
| 2017-02-08 |
| 2017-02-09 |
| 2017-02-10 |
| 2017-02-11 |
| 2017-02-12 |
| 2017-02-13 |
| 2017-02-14 |
| 2017-02-15 |
| 2017-02-16 |
| 2017-02-17 |
| 2017-02-18 |
| 2017-02-19 |
| 2017-02-20 |
| 2017-02-21 |
| 2017-02-22 |
| 2017-02-23 |
| 2017-02-24 |
| 2017-02-25 |
| 2017-02-26 |
+------------+
I just found this for looking up dates that fall within a given week. The code can be manipulated to change the week start date.
select convert(datetime,dateadd(dd,-datepart(dw,convert(datetime,convert(varchar(10),DateAdd(dd,-1/*this # changes the week start day*/,getdate()),101)))+1/*this # is used to change the week start date*/,
convert(datetime,convert(varchar(10),getdate(),21))))/*also can enter # here to change the week start date*/
I've included a screenshot of the results if you were to include this with a full query. This way you can see how it looks with a range of dates. I did a little manipulation so that the week starts on Monday and references Monday's date.
Since I am only looking back three weeks a simple GETDATE()-21 is sufficient because as the query moves forward through the week it will look back 21 days and pick the Monday at the beginning of the week as my start date.

Issue with Group by Mysql query

I have been stumped by this MySQL query for the past few day,and seeking some expert help before i throw my PC out of the window, lol.
I have a table that consists of invoices; invoice number column name is Trans_ref$$ (primary key).
each invoice is linked to a job , column name is OPSREF$$_ORIGINAL (foreign key).
Both columns are in the same table.
a job can have multiple invoices.
Now the issue is i'm trying to create a query that will identify the FIRST invoice in sequence (Trans_ref$$) in a job group and if that FIRST invoice is with a date range then output the job (OPSREF$$_ORIGINAL).
The invoice number is sequential and a unique number.
I have tried using a case / min group by statement to identify the first invoice in a job group within a data range, however this query is not isolating the first invoice within a date range, instead it is just outputting 1st or 2nd or 3rd positioned invoice in job group that matches the date range….I hope this makes sense .
I have included a screen shot of the table and the desired output, and would appreciate if someone
Hello
I have been stumped by this query for the past day,and seeking some expert help before i throw my PC out of the window, lol.
I have a table that consists of invoices; invoice number column name is Trans_ref$$ (primary key).
each invoice is linked to a job , column name is OPSREF$$_ORIGINAL (foreign key).
Both columns are in the same table.
a job can have multiple invoice.
Now the issue is i'm trying to create a query that will identify the FIRST invoice (Trans_ref$$) in a job group and if that FIRST invoice is with a date range then output the job (OPSREF$$_ORIGINAL).
The invoice number in a sequential / unique number.
I have tried using a case / min group by statement to identify the first invoice in a job group within a data range, however this query is not isolating the first invoice within a date range, instead it is just outputting 1st or 2nd or 3rd positioned invoice in job group that matches the date range….I hope this makes sense .
Here the code I have used
SET #START_DATE := "2014-08-27";
SET #END_DATE:= "2014-09-02";
select
count(TRANS_REF$$),
Case
when min(TRANS_REF$$) and INVOICE_DATE_D8>=#START_DATE and INVOICE_DATE_D8<=#END_DATE
then OPSREF$$_ORIGINAL
end as OPSREF$$_ORIGINAL
from INVOICE_HEADER_UNS
where
TRANSACTION_STATUS = 9
and Tmode$$="06"
group by OPSREF$$_ORIGINAL
So to breakdown the process in stages.....
1. grab each OPSREF$$_ORIGINAL group within the INVOICE_HEADER_UNS table
Date Range - Start 27/08/2014 - End 02/09/2014
TABLE - INVOICE_HEADER_UNS
TRANS_REF$$ INVOICE_DATE_D8 OPSREF$$_ORIGINAL
---------------------------------------------
| 1 | 26/08/2014 | ABC |
| 2 | 02/08/2014 | ABC |
---------------------------------------------
| 3 | 28/08/2014 | DDD |
| 4 | 09/09/2014 | DDD |
---------------------------------------------
| 5 | 01/01/2013 | JJJ |
| 6 | 21/12/2013 | JJJ |
---------------------------------------------
| 7 | 01/09/2014 | LLL |
---------------------------------------------
2. within each OPSREF$$_ORIGINAL group, look at TRANS_REF$$ sequence and find the first TRANS_REF$$ in that OPSREF$$_ORIGINAL group.
Date Range - Start 27/08/2014 - End 02/09/2014
TABLE - INVOICE_HEADER_UNS
TRANS_REF$$ INVOICE_DATE_D8 OPSREF$$_ORIGINAL Is First inv?
--------------------------------------------------------
| 1 | 26/08/2014 | ABC | yes
| 2 | 02/08/2014 | ABC | no
--------------------------------------------------------
| 3 | 28/08/2014 | DDD | yes
| 4 | 09/09/2014 | DDD | no
---------------------------------------------------------
| 5 | 01/01/2013 | JJJ | yes
| 6 | 21/12/2013 | JJJ | no
---------------------------------------------------------
| 7 | 01/09/2014 | LLL | yes
---------------------------------------------------------
3. Look at the the FIRST INVOICE in the ordered sequence for the group and check the INVOICE_DATE_D8 for that row and see if it is within date range ( 27/08/2014 - 02/09/2014 )
Date Range - Start 27/08/2014 - End 02/09/2014
TABLE - INVOICE_HEADER_UNS
TRANS_REF$$ INVOICE_DATE_D8 OPSREF$$_ORIGINAL Is First inv? In date range?
--------------------------------------------------------
| 1 | 26/08/2014 | ABC | yes no
--------------------------------------------------------
| 3 | 28/08/2014 | DDD | yes yes
---------------------------------------------------------
| 5 | 01/01/2013 | JJJ | yes no
---------------------------------------------------------
| 7 | 01/09/2014 | LLL | yes yes
4. Does each row meet the criteria (i.e. first invoice and in date - YES + YES) ? if correct then output the distinct OPSREF$$_ORIGINAL for that row
Here is the desired output from the example above
Date Range - Start 27/08/2014 - End 02/09/2014
TABLE - INVOICE_HEADER_UNS
OPSREF$$_ORIGINAL Is First inv? In date range?
------------------------
| DDD | yes yes
------------------------
| LLL | yes yes
------------------------
Based on this, can you suggest how I can do this? thanks
Here is the Table definition;
tablename - INVOICE_HEADER_UNS
**TRANS_REF$$** char(7) - Primary key
INVOICE_DATE_D8 datatime
OPSREF$$_ORIGINAL char(11)
16/09/2014 - Here is the query that appears to be returning the correct results using DRapp syntax, however the query needs to be optimised somehow as when I bolt this onto the bigger query as a sub query it causes my MYSQL workbench to crash. After doing an explain on the query , it appears to be indexing through 28,309 rows. How can i optimise this query without impacting the results?
I have tried adding the below to the where clause however it adds more records to the overall query output, but indexes 979 rows instead/
set #START_DATE := '2014-08-27';
set #END_DATE := '2014-09-02';
SELECT
if (min(ihu.INVOICE_DATE_D8)>= #START_DATE and min(ihu.INVOICE_DATE_D8)<= #END_DATE ihu.opsref$$_original, null) as firstinvoiceindaterange
from
INVOICE_HEADER_UNS ihu
join CONSIGNMENT_ALL_HEADER_UNS on ihu.OUR_REF = CONSIGNMENT_ALL_HEADER_UNS.OPSREF$$
where ihu.TRANSACTION_STATUS = 9 and ihu.Tmode$$="06"
AND CONSIGNMENT_ALL_HEADER_UNS.CONS_TYPE$$ <>'7'
AND CONSIGNMENT_ALL_HEADER_UNS.CONS_TYPE$$ in ('3' , '5', '9')
#and ihu.INVOICE_DATE_D8>= #START_DATE and ihu.INVOICE_DATE_D8<= #END_DATE
group by
ihu.opsref$$_original
I think this should help you out, and you can tweak it as needed.
SELECT
ihu.opsref$$_original,
MIN( ihu.TRANS_REF$$ ) MinRefForEntireJob,
MIN( ihu.INVOICE_DATE_D8 ) MinDateForEntireJob,
MIN( IF( ihu.INVOICE_DATE_D8 >= #START_DATE
AND ihu.INVOICE_DATE_D8 <= #END_DATE, ihu.TRANS_REF$$, null )) as MinRefWithinDateRange,
MIN( IF( ihu.INVOICE_DATE_D8 >= #START_DATE
AND ihu.INVOICE_DATE_D8 <= #END_DATE, ihu.INVOICE_DATE_D8, null )) as MinDateWithinDateRange
from
INVOICE_HEADER_UNS ihu,
( select #START_DATE := '2014-08-27',
#END_DATE := '2014-09-02' ) sqlvars
group by
ihu.opsref$$_original
Once you see these results, then you SHOULD be able to just add a HAVING clause something like
HAVING
MinDateWithinDateRange IS NOT NULL