Select all stores open at current time - mysql

I have a table named opening_hours that look like this:
id int(11), weekday int(1), start_hour time, end_hour time
I use this query to select all stores that are open now:
SELECT * FROM shops s
INNER JOIN opening_hours o
ON ( s.id = o.id )
AND ( o.weekday = WEEKDAY(CURDATE()) + 1 )
AND ( ( CURTIME() >= o.start_hour ) AND ( CURTIME() <= o.end_hour ) )
My problem is that this query is giving the wrong result when stores are open after midnight. That's because time after midnight is earlier than the time before midnight. How to handle this?

The correct logic is more complicated. When the opening hours span two dates, then in the wee hours of the morning, you have to look at the previous days opening hours -- unless you assume that the hours are the same for each day (but then why have table?).
The condition is more like this:
SELECT *
FROM shops s INNER JOIN
opening_hours o
ON s.id = o.id
WHERE ((o.weekday = weekday(curdate()) + 1) and
((o.end_hour > o.start_hour and curtime() >= o.start_hour and curtime <= o.end_hour) or
(o.end_hour < o.start_hour and curtime() >= o.start_hour)
) or
(o.weekday = weekday(date_add(curdate(), interval 1 day)) + 1 and
o.end_hour < o.start_hour and
curtime() <= o.end_Hour
);
The three conditions are:
Opening hours all in one day
Opening hours span two days, and the hour not earlier than the opening
Opening hours span two days, and the hour is earlier than the closing hour

When the end_hour is less than start_hour then you have to modify the end_hour so that it becomes greater than start_hour.
One way to achieve this is to deduct end_hour from 24:00:00 when end_hour is less than start_hour otherwise end_hour prevails.
SELECT * FROM shops s
INNER JOIN opening_hours o
ON ( s.id = o.id )
AND ( o.weekday = WEEKDAY(CURDATE()) + 1 )
AND ( ( CURTIME() >= o.start_hour ) AND ( CURTIME() <= IF(o.end_hour < o.start_hour,TIMEDIFF(TIME('24:00:00'),o.end_hour),o.end_hour ) ) ) ;
EDIT:
The query above performs great when the end_hour is 00:00:00.
But it might give wrong output for this case
start_hour = 07:00:00 & end_hour = 02:00:00.
So, here how you can recover (use this condition in your main query):
AND
(
IF(o.end_hour < o.start_hour,
( CURTIME() >= o.start_hour ) OR ( CURTIME() <= o.end_hour ),
( CURTIME() >= o.start_hour ) AND ( CURTIME() <= o.end_hour )
)
Note that if the range lies in the same date then this condition should prevail:
( CURTIME() >= o.start_hour ) AND ( CURTIME() <= o.end_hour ).
And if the range wraps over midnight then this condition should be in action:
( CURTIME() >= o.start_hour ) OR ( CURTIME() <= o.end_hour )

Related

How to test if there are overlapping periods with a particular period?

In my table there are two columns of type date and two columns of type time :
Here are some records of the table :
Now , in my web app I want to insert a new row in that table :
When submitting the form I want to count the number of rows where the entered period overlaps to others that are already in the database table ; by period I mean a ( beginning_date , beginning_time ) and a ( ending_date , ending_time ) together , for example ( 2016-03-15 , 12:00:00 ) and ( 2016-03-17 , 10:00:00 ).
I tried this query but it does not give the right results :
select count(identifiant) from reservation_table where ( (date_debut <= '2016-03-14' and heure_debut <= '01:00:00') and (date_fin <= '2016-03-14' and heure_fin <= '03:00:00') and (date_fin > '2016-03-14' and heure_fin > '01:00:00') ) or
( (date_debut >= '2016-03-14' and heure_debut >= '01:00:00') and (date_fin >= '2016-03-14' and heure_fin >= '03:00:00') and (date_debut < '2016-03-14' and heure_debut < '03:00:00') ) or
( (date_debut >= '2016-03-14' and heure_debut >= '01:00:00') and (date_fin <= '2016-03-14' and heure_fin <= '03:00:00') ) or
( (date_debut <= '2016-03-14' and heure_debut <= '01:00:00') and (date_fin >= '2016-03-14' and heure_fin >= '03:00:00') );
To have a better understanding about the period overlapping here is an image :
So in this image the red period is the period entered from the web app , and the black periods are those already in the database. So how to get all periods that overlap to a particular period ?
The way to test if two elements overlap is to check if the one starts before the second ends, while the second starts before the first ends, as mentioned in the overlap tag wiki.
I don't have much experience with MySql but did find this method to create a datetime value from date and time:
STR_TO_DATE(CONCAT(date, ' ', time), '%Y-%m-%d %H:%i:%s')
Once you have datetime values you can do this:
select count(identifiant)
from reservation_table
where #YourStartDatetime <= STR_TO_DATE(CONCAT(date_fin,' ', heure_fin), '%Y-%m-%d %H:%i:%s')
and #YourEndDateTime >= STR_TO_DATE(CONCAT(date_debut ,' ', heure_debut), '%Y-%m-%d %H:%i:%s')
if the count returns 0, then you have no records overlapping the period specified by #YourStartDatetime and #YourEndDateTime

Convert NOT IN query to better performance

I'm using MySQL 5.0, and I need to fine tune this query. Can anyone please tell me what tuning I can do in this?
SELECT DISTINCT(alert_master_id) FROM alert_appln_header
WHERE created_date < DATE_SUB(CURDATE(), INTERVAL (SELECT parameters FROM schedule_config WHERE schedule_name = "Purging_Config") DAY)
AND alert_master_id NOT IN (
SELECT DISTINCT(alert_master_id) FROM alert_details
WHERE end_date IS NULL AND created_date < DATE_SUB(CURDATE(), INTERVAL (SELECT parameters FROM schedule_config WHERE schedule_name = "Purging_Config") DAY)
UNION
SELECT DISTINCT(alert_master_id) FROM alert_sara_header
WHERE sara_master_id IN
(SELECT alert_sara_master_id FROM alert_sara_lines
WHERE end_date IS NULL) AND created_date < DATE_SUB(CURDATE(), INTERVAL (SELECT parameters FROM schedule_config WHERE schedule_name = "Purging_Config") DAY)
) LIMIT 5000;
The first thing that I'd do is rewrite the subqueries as joins:
SELECT h.alert_master_id
FROM alert_appln_header h
JOIN schedule_config c
ON c.schedule_name = 'Purging_Config'
LEFT JOIN alert_details d
ON d.alert_master_id = h.alert_master_id
AND d.end_date IS NULL
AND d.created_date < CURRENT_DATE - INTERVAL c.parameters DAY
LEFT JOIN (
alert_sara_header s
JOIN alert_sara_lines l
ON l.alert_sara_master_id = s.sara_master_id
)
ON s.alert_master_id = h.alert_master_id
AND s.end_date IS NULL
AND s.created_date < CURRENT_DATE - INTERVAL c.parameters DAY
WHERE h.created_date < CURRENT_DATE - INTERVAL c.parameters DAY
AND d.alert_master_id IS NULL
AND s.alert_master_id IS NULL
GROUP BY h.alert_master_id
LIMIT 5000
If it's still slow after that, re-examine your indexing strategy. I'd suggest indexes over:
alert_appln_header(alert_master_id,created_date)
schedule_config(schedule_name)
alert_details(alert_master_id,end_date,created_date)
alert_sara_header(sara_master_id,alert_master_id,end_date,created_date)
alert_sara_lines(alert_sara_master_id)
OK, this may be just a shot in the dark, but I think you don't need as many DISTINCT here.
SELECT DISTINCT(alert_master_id) FROM alert_appln_header
WHERE created_date < DATE_SUB(CURDATE(), INTERVAL (SELECT parameters FROM schedule_config WHERE schedule_name = "Purging_Config") DAY)
AND alert_master_id NOT IN (
-- removed distinct here --
SELECT alert_master_id FROM alert_details
WHERE end_date IS NULL AND created_date < DATE_SUB(CURDATE(), INTERVAL (SELECT parameters FROM schedule_config WHERE schedule_name = "Purging_Config") DAY)
UNION
-- removed distinct here --
SELECT alert_master_id FROM alert_sara_header
WHERE sara_master_id IN
(SELECT alert_sara_master_id FROM alert_sara_lines
WHERE end_date IS NULL)
AND created_date < DATE_SUB(CURDATE(), INTERVAL (SELECT parameters FROM schedule_config WHERE schedule_name = "Purging_Config") DAY)
) LIMIT 5000;
Since using the DISTINCT is very costly, try to avoid it. In the first WHERE clause you are checking for ids that are NOT within some result, so it shouldn't matter if in that result some ids appear more than once.

MySql sum records daily from 2:00 am instead of midnight.

For a business closing at 2:00 am, daily accounting ends then. How do I write or simplify this query to sum daily traffic where the top of the clock is actually at 2:00.
SELECT date( CAST( Time - INTERVAL 2 HOUR AS DATETIME ) ) AS date,
CAST( (sum( `in_count` ) + sum( `out_count` ) ) /2 AS UNSIGNED) AS count
FROM establishment
LEFT JOIN `device` ON establishment.establishment_ID = device.establishment_ID
LEFT JOIN `device_state` ON device.device_id = device_state.device_ID
WHERE establishment.establishment_ID =1
AND date(CAST( Time - INTERVAL 2 HOUR AS DATETIME ))
BETWEEN '2013-02-01' AND '2013-03-01'
GROUP BY dayofmonth( CAST( Time - INTERVAL 2 HOUR AS DATETIME ))
ORDER BY Time

Correct my MySQL query?

I have table like
CREATE TABLE `survey` (
`id` int(11) NOT NULL auto_increment,
`submitdate` datetime default NULL,
`answer` varchar(5) collate utf8_unicode_ci default NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=499 ;
now to get values like
c t Clicks
2012-10-29 2012-10-22 10
2012-11-04 2012-10-30 20
2012-11-11 2012-11-05 30
2012-11-19 2012-11-12 34
I am using this query
SELECT uq.timespan, COALESCE(tsq.TotalClicks, 0) as Clicks FROM (
SELECT DATE( DATE_ADD( NOW( ) , INTERVAL -21
DAY ) ) c, DATE( DATE_ADD( NOW( ) , INTERVAL -28
DAY ) ) l
union SELECT DATE( DATE_ADD( NOW( ) , INTERVAL -15
DAY ) ) c, DATE( DATE_ADD( NOW( ) , INTERVAL -20
DAY ) ) l
union SELECT DATE( DATE_ADD( NOW( ) , INTERVAL -8
DAY ) ) c, DATE( DATE_ADD( NOW( ) , INTERVAL -14
DAY ) ) l
union SELECT curdate() c,DATE( DATE_ADD( NOW( ) , INTERVAL -7
DAY ) ) l
)uq LEFT JOIN (
SELECT CASE
WHEN submitdate >= NOW() - INTERVAL 4 WEEK
AND submitdate < NOW() - INTERVAL 3 WEEK THEN c 'to' l
DAY ) )
WHEN submitdate >= NOW() - INTERVAL 3 WEEK
AND submitdate < NOW() - INTERVAL 2 WEEK THEN c 'to' l
WHEN submitdate >= NOW() - INTERVAL 2 WEEK
AND submitdate < NOW() - INTERVAL 1 WEEK THEN c 'to' l
DAY ) )
WHEN submitdate >= NOW() - INTERVAL 1 WEEK THEN c 'to' l
END Weeksubmitdate,
count(id) TotalClicks
FROM survey
WHERE submitdate >= NOW() - INTERVAL 4 WEEK
GROUP BY Weeksubmitdate
)tsq ON uq.timespan = tsq.Weeksubmitdate";
problem is with 16th line c to l.
I am getting the following error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''to' l
DAY ) )
WHEN submitdate >= NOW() - INTERVAL 3 WEEK
' at line 16
CASE is supposed to evaluate to a scalar expression. That means its THEN clauses must evaluate to scalar expressions too. Now, what does this c 'to' l thing stand for? Is it a scalar expression? It doesn't seem one to me, however I may be unaware of some things in MySQL, so it's more important whether MySQL itself recognises that as a scalar expression. And apparently it doesn't.
There is another issue. You are trying to reference a derived table's columns inside another derived table. More specifically, you seem to be trying to reference the columns c and l of uq inside the tsq subselect, and that is illegal. If uq was a normal table, it would be fine, but since it is a virtual table, the query doesn't know about its existence at that point, i.e. at the time of parsing the tsq subquery.
Anyway, what you seem to be doing with your query could probably be rewritten more simply, like this, for instance:
SELECT
MIN(submitdate) AS startdate,
MAX(submitdate) AS enddate,
COUNT(*) AS clicks
FROM (
SELECT
CASE
WHEN submitdate >= NOW() - INTERVAL 1 WEEK THEN 1
WHEN submitdate >= NOW() - INTERVAL 2 WEEK THEN 2
WHEN submitdate >= NOW() - INTERVAL 3 WEEK THEN 3
WHEN submitdate >= NOW() - INTERVAL 4 WEEK THEN 4
END AS weekid,
*
FROM survey
) s
GROUP BY
weekid
ORDER BY
startdate
;
The subquery assigns surrogate week IDs to every row of survey. The main query groups the results by those IDs and produces the counts as well as starting & ending dates for every group.

Query for duration between two times within 1 day

Suppose I have a table that contain information on streaming media connections. In this table, I have a start time and end time for when the connection was initiated and then later closed.
Table: logs
id (INT, PK, AUTO_INCREMENT)
StartTime (DATETIME)
EndTime (DATETIME)
I want to be able to run a query that will add up the total time connections were established for a day. This is obvious for connections within a day:
SELECT
SUM(
TIME_TO_SEC(
TIMEDIFF(`EndTime`, `StartTime`)
)
)
WHERE (`StartTime` BETWEEN '2010-01-01' AND '2010-01-02);
However, suppose a StartTime begins one day, say around 11:00PM, and EndTime is some time the next day, maybe 3:00AM. In these situations, I want to allocate only the amount of time that occurred during the day, to that day. So, 1 hour would go towards the first day, and 3 hours would go to the next.
SUM(
TIME_TO_SEC(
TIMEDIFF(
IF(`EndTime`>DATE_ADD('2010-01-01', INTERVAL 1 DAY), DATE_ADD('2010-01-01', INTERVAL 1 DAY), `EndTime`),
IF(`StartTime`<'2010-01-01', '2010-01-01', `StartTime`)
)
)/60/60
)
The thinking with this is that if the EndTime is more than the end of the day, then we'll just use the end of the day instead. If the StartTime is less than the beginning of the day, then we'll just use the beginning of the day instead.
So, I then need to wrap this all up into something that will generate a table that looks like this:
date, total
2010-01-01, 0
2010-01-02, 1.53
2010-01-03, 5.33
I thought this query would work:
SELECT
`date`,
SUM(
TIME_TO_SEC(
TIMEDIFF(
IF(`EndTime`>DATE_ADD(`date`, INTERVAL 1 DAY), DATE_ADD(`date`, INTERVAL 1 DAY), `EndTime`),
IF(`StartTime`<`date`, `date`, `StartTime`)
)
)/60/60
) AS `total_hours`
FROM
(SELECT * FROM `logs` WHERE `StartTime` BETWEEN '2010-08-01' AND '2010-08-31') AS logs_small,
(SELECT DATE_ADD("2010-08-01", INTERVAL `number` DAY) AS `date` FROM `numbers` WHERE `number` BETWEEN 0 AND 30) AS `dates`
GROUP BY `date`;
Note the numbers table referenced is a table with just one column, number, with a series of integers, 0, 1, 2, 3, etc. I am using it here to generate a series of dates, which works fine.
The problem with this query is that I get inaccurate data. Specifically, rows in the logs table that have an EndDate that goes into the next day don't get any time counted in that next day. For example, if I had a row that started 2010-08-01 23:00:00 and ended 2010-08-02 01:00:00, then the resulting row for 2010-08-02 would add up to 0.
Is there a better way to do this? Ideally, I'd like to get 0 instead of null on days that don't have any records that match up to them as well.
Edit: To clarify, I want to turn this:
id, StartTime, EndTime
0, 2000-01-01 01:00:00, 2000-01-01 04:00:00
1, 2000-01-01 23:00:00, 2000-01-02 05:00:00
2, 2000-01-02 00:00:00, 2000-01-04 01:00:00
... into this:
date, total_hours
2000-01-01, 4
2000-01-02, 29
2000-01-03, 24
2000-01-04, 1
2000-01-05, 0
Solution
Thanks to jim31415 for coming up with the solution! I translated his answer over to the functions usable in MySQL and came up with this:
SELECT `d`.`Date`,
SUM(COALESCE(
(CASE WHEN t.StartTime >= d.Date AND t.EndTime < DATE_ADD(d.Date, INTERVAL 1 DAY) THEN TIME_TO_SEC(TIMEDIFF(t.EndTime, t.StartTime))
WHEN t.StartTime < d.Date AND t.EndTime <= DATE_ADD(d.Date, INTERVAL 1 DAY) THEN TIME_TO_SEC(TIMEDIFF(t.EndTime,d.Date))
WHEN t.StartTime >= d.Date AND t.EndTime > DATE_ADD(d.Date, INTERVAL 1 DAY) THEN TIME_TO_SEC(TIMEDIFF(DATE_ADD(d.Date, INTERVAL 1 DAY),t.StartTime))
WHEN t.StartTime < d.Date AND t.EndTime > DATE_ADD(d.Date, INTERVAL 1 DAY) THEN 24*60*60
END), 0)
)/60/60 ConnectionTime
FROM (SELECT DATE_ADD('2011-03-01', INTERVAL `number` DAY) AS `Date` FROM `numbers` WHERE `number` BETWEEN 0 AND 30) AS d
LEFT JOIN `logs` t ON (t.StartTime >= d.Date AND t.StartTime < DATE_ADD(d.Date, INTERVAL 1 DAY))
OR (t.EndTime >= d.Date AND t.EndTime < DATE_ADD(d.Date, INTERVAL 1 DAY))
OR (t.StartTime < d.Date AND t.EndTime > DATE_ADD(d.Date, INTERVAL 1 DAY))
GROUP BY d.Date
ORDER BY d.Date;
I should also note that the null values for EndTime weren't applicable in my situation, as I am reading from old log files in my application. If you need them though, Jim's post has them outlined quite well.
This is in MS SQL, but I think the logic applies and can be translated into MySQL.
I wasn't sure how you wanted to handle EndTime that are null, so I commented that out.
select d.Date,
sum(coalesce(
(case when t.StartTime >= d.Date and t.EndTime < dateadd(day,1,d.Date) then datediff(minute,t.StartTime,t.EndTime)
when t.StartTime < d.Date and t.EndTime <= dateadd(day,1,d.Date) then datediff(minute,d.Date,t.EndTime)
when t.StartTime >= d.Date and t.EndTime > dateadd(day,1,d.Date) then datediff(minute,t.StartTime,dateadd(day,1,d.Date))
when t.StartTime < d.Date and t.EndTime > dateadd(day,1,d.Date) then 24*60
--when t.StartTime >= d.Date and t.EndTime is null then datediff(minute,t.StartTime,getdate())
--when t.StartTime < d.Date and t.EndTime is null then datediff(minute,d.Date,getdate())
end), 0)
) ConnectionTime
from (select Date=dateadd(day, num, '2011-03-01') from #NUMBERS where num between 0 and 30) d
left join Logs t on (t.StartTime >= d.Date and t.StartTime < dateadd(day,1,d.Date))
or (t.EndTime >= d.Date and t.EndTime < dateadd(day,1,d.Date))
or (t.StartTime < d.Date and t.EndTime > dateadd(day,1,d.Date))
group by d.Date
order by d.Date
Use a union to make it easier for yourself
SELECT
`date`,
SUM(
TIME_TO_SEC(TIMEDIFF(`EndTime`,`StartTime`))/60/60
) AS `total_hours`
FROM
(SELECT id, starttime, if (endtime > date then date else endtime) FROM `logs` WHERE `StartTime` >= date AND `StartTime` < date
union all
SELECT id, date, endtime FROM `logs` WHERE `enddate` >= date AND `enddate` < date and !(`StartTime` >= date AND `StartTime` < date)
union all
SELECT id, date, date_add(date, 1) FROM `logs` WHERE `enddate` > date AND `startdate` < date
) as datedetails inner join
(SELECT DATE_ADD("2010-08-01", INTERVAL `number` DAY) AS `date` FROM `numbers` WHERE `number` BETWEEN 0 AND 30) AS `dates`
GROUP BY `date`;
Hope, I understood your question correctly
Edit: Forgot case when there is a multiday request that starts before the day asked for, and ended after
Use this
select startTime,duration as duration,time,TIME_TO_SEC(TIMEDIFF(time,startTime)) as diff from <idling> limit 25;
select startTime,duration DIV 60 as duration,time,TIMESTAMPDIFF(MINUTE,startTime,time) as diff from <idling> limit 25;