I have a db table called serverstatus that stores time and status as below. At any given time how should i find the total downtime?
Time Status
10:00 UP
11:30 DOWN
12:00 UP
14:00 DOWN
15:00 UP
Fo example at 16:00 the total downtime is 1 hr 30 minutes. I tried to do a self join and substract between two consecutive rows but couldn't get correct answer
Select A.time-min(B.time) from serverstatus A, serverstatus B where A.status="UP" and B.status="DOWN" and B.time < A.time
You can do this with row number simulation if row_number function is not available to you in your mysql version
select #s.*,t.*,timediff(endtime,starttime) duration
sec_to_time(sum(time_to_sec(timediff(endtime,starttime))) / 60) Downduration
from
(
select t.time starttime,t.status,#rn:=#rn+1 rn,
#p:=status p
from t,(select #rn:=0,#p:='') r
order by time
) s
join
(
select t.time endtime,t.status,#rn1:=#rn1+1 rn1,
#p1:=status p1
from t,(select #rn1:=0,#p1:='') r1
order by time
) t on t.rn1 = s.rn + 1
where s.status = 'Down' and t.status = 'Up'
+---------------+
| Downduration |
+---------------+
| 00:01:30.0000 |
+---------------+
1 row in set (0.00 sec)
Related
Let's say I have the following table:
date | name | value
----------------------------
2020-09-01 | name1 | 10
2020-09-02 | name1 | 9
2020-09-03 | name1 | 12
2020-09-04 | name1 | 11
2020-09-05 | name1 | 11
I would like to identify names where the latest value >= 10 AND where over the last 5 days it has ever dropped below 10. In the example table above, name1 would be returned because the latest date has a value of 11 (which is > 10), and over the last 5 days it has dropped below 10 at least once.
Here is my SELECT statement, but it always returns zero rows:
SELECT
name,
count(value) as count
FROM table_name
WHERE
(date = #date AND value >= 10) AND
date BETWEEN date_sub(#date, interval 5 day) AND #date AND value < 10
GROUP BY name
HAVING count < 5
ORDER BY name
I understand why it's failing, but I don't know what to change.
In MySQL 8.0, you could use window functions and aggregation:
select name
from (
select t.*, row_number() over(partition by name order by date desc) rn
from mytable t
where date >= #date - interval 5 day and date <= #date
) t
having max(case when rn = 1 then value end) >= 10 and min(value) <= 10
How about something like this:
SELECT Name, COUNT(*) AS Ct FROM
(SELECT A.*,B.mdate,
CASE WHEN A.date=B.mdate AND A.value >= 10 THEN 1
WHEN A.date >= B.mdate - INTERVAL 5 DAY AND A.date <> B.mdate AND A.value < 10 THEN 1
ELSE 0 END AS Chk
FROM table_name A
JOIN (SELECT Name,MAX(DATE) AS mdate FROM table_name GROUP BY Name) B ON A.Name=B.Name
HAVING Chk <> 0) V
GROUP BY Name
HAVING Ct >= 2
Here's a fiddle for reference: https://www.db-fiddle.com/f/jX4GktCdTrUbqHBf7ZQwdr/0
And here's a breakdown of what the query above is doing.
Joining table_name with a sub-query of the same table but with MAX(DATE) value for comparison.
Using CASE function to check for your conditions; if matches with the conditions, it will return 1, if not, return 0. Added HAVING to exclude any 0 value from the CASE function.
Turn the query to become a sub-query (assigned as V) and do a COUNT(*) over how many occurrence happen on the name then using HAVING again to get any name that have 2 or more occurrence.
I have a table like below
session stepId starttime
------ ----------- -----
1 1 10:00
1 1 10:10
1 2 10:40
1 3 10:50
1 4 11:00
And what I am aiming to calculate is the average time between each step Id, if the stepID is the same , like the first two rows, the most recent one is used.
For example, for the above query, the result should be ((10:40 - 10:10) + (10:50-10:40) + (11:00 - 10:50))/3.
I am using MySQL.
SELECT
TIME(AVG(M.timediff))
FROM
(SELECT
TIME(b.starttime - a.starttime) AS timediff
FROM
(SELECT
stepId, MAX(starttime) starttime
FROM
test.test
GROUP BY stepId) a
LEFT JOIN test.test b ON a.stepId = b.stepId - 1
WHERE
a.starttime IS NOT NULL
AND b.starttime IS NOT NULL) AS M
I'm tracking number of steps/day. I want to get the average steps/day using the 5 best days out of a 7 day period. My end goal is going to be to get an average for the best 5 out of 7 days for a total of 16 weeks.
Here's my sqlfiddle - http://sqlfiddle.com/#!9/5e69bdf/2
Here is the query I'm currently using but I've discovered the result is not correct. It's taking the average of 7 days instead of selecting the 5 days that had the most steps. It's outputting 14,122 as an average instead of 11,606 based on my data as posted in the sqlfiddle.
SELECT SUM(a.steps) as StepsTotal, AVG(a.steps) AS AVGSteps
FROM (SELECT * FROM activities
JOIN Courses
WHERE activities.encodedid=? AND activities.activitydate BETWEEN
DATE_ADD(Courses.Startsemester, INTERVAL $y DAY) AND
DATE_ADD(Courses.Startsemester, INTERVAL $x DAY)
ORDER BY activities.steps DESC LIMIT 5
) a
GROUP BY a.encodedid
Here's the same query with the values filled in for testing:
SELECT SUM(a.steps) as StepsTotal, AVG(a.steps) AS AVGSteps
FROM (SELECT * FROM activities
JOIN Courses
WHERE activities.encodedid='42XPC3' AND activities.activitydate BETWEEN
DATE_ADD(Courses.Startsemester, INTERVAL 0 DAY) AND
DATE_ADD(Courses.Startsemester, INTERVAL 6 DAY)
ORDER BY activities.steps DESC LIMIT 5
) a
GROUP BY a.encodedid
As #SloanThrasher pointed out, the reason the query is not working is because you have multiple rows for the same course in the Courses database which end up being joined to the activities database. Thus the output for the subquery gives the top value (16058) 3 times plus the second highest value (11218) twice for a total of 70610 and an average of 14122. You can work around this by modifying the query as follows:
SELECT SUM(a.steps) as StepsTotal, AVG(a.steps) AS AVGSteps
FROM (SELECT * FROM activities
JOIN (SELECT DISTINCT Startsemester FROM Courses) c
WHERE activities.encodedid='42XPC3' AND activities.activitydate BETWEEN
DATE_ADD(c.Startsemester, INTERVAL 0 DAY) AND
DATE_ADD(c.Startsemester, INTERVAL 6 DAY)
ORDER BY CAST(activities.steps AS UNSIGNED) DESC LIMIT 5
) a
GROUP BY a.encodedid
Now since there are actually only 3 days with activity (2018-07-16, 2018-07-17 and 2018-07-18) between the start of semester and 6 days later (2018-07-12 and 2018-07-18) this gives a total of 37533 (16058+11218+10277) and an average of 12517.7.
StepsTotal AVGSteps
37553 12517.666666666666
Ideally, you probably also want to add a constraint on the Course chosen from Courses e.g. change
(SELECT DISTINCT Startsemester FROM Courses)
to
(SELECT DISTINCT Startsemester FROM Courses WHERE CourseNumber='PHED1164')
Try this query:
SELECT #rn := 1, #weekAndYear := 0;
SELECT weekDayAndYear,
SUM(steps),
AVG(steps)
FROM (
SELECT #weekAndYear weekAndYearLag,
CASE WHEN #weekAndYear = YEAR(activitydate) * 100 + WEEK(activitydate)
THEN #rn := #rn + 1 ELSE #rn := 1 END rn,
#weekAndYear := YEAR(activitydate) * 100 + WEEK(activitydate) weekDayAndYear,
steps,
lightly_act_min,
fairly_act_min,
sed_act_min,
vact_min,
encodedid,
activitydate,
username
FROM activities
ORDER BY YEAR(activitydate) * 100 + WEEK(activitydate), CAST(steps AS UNSIGNED) DESC
) a WHERE rn <= 5
GROUP BY weekDayAndYear
Demo
With additional variables, I imitate SQL Server ROW_NUMBER function, to number from 1 to 7 days partitioned by weeks. This way I can filter best 5 days and easily get a average grouping by column weekAndDate, which is in the same format as variable: yyyyww (i used integer to avoid casting to varchar).
Consider the following:
DROP TABLE IF EXISTS my_table;
CREATE TABLE `my_table`
(id SERIAL PRIMARY KEY
,steps INT NOT NULL
);
insert into my_table (steps) values
(9),(5),(7),(7),(7),(8),(4);
select prev
, sum(steps) total
from (
select steps
, case when #prev = grp
then #j:=#j+1 else #j:=1 end j
, #prev:=grp prev
from (SELECT steps
, case when mod(#i,3)=0
then #grp := #grp+1 else #grp:=#grp end grp -- a 3 day week
, #i:=#i+1 i
from my_table
, (select #i:=0,#grp:=0) vars
order
by id) x
, (select #prev:= null, #j:=0) vars
order by grp,steps desc,i) a
where j <=2 -- top 2 (out of 3)
group by prev;
+------+-------+
| prev | total |
+------+-------+
| 1 | 16 |
| 2 | 15 |
| 3 | 4 |
+------+-------+
http://sqlfiddle.com/#!9/ee46d7/11
I am working with MySQL. I am trying to get the nights of a booking that belong to each interval in a group of intervals of dates. But there are some intervals that are preferred over others and therefore I will take as many nights for the preferred intervals as possible and fill the gaps with the **not preferred interval **. To illustrate this I will show it here:
Given the dates:
check in => 2016-01-16
check out => 2016-02-08
total nights => 24
Preferred | date_from | date_to | Nights
----------------------------------------------------
1 | 2016-01-15 | 2016-01-17 | 2
1 | 2016-02-03 | 2016-02-10 | 6
1 | 2016-01-20 | 2016-01-25 | 6
0 | 2016-01-20 | 2016-01-31 | 2 (2016-01-26 and 2016-01-31 because the other nights are covered by a preferred period)
1 | 2016-01-27 | 2016-01-30 | 4
0 | 2016-01-15 | 2016-01-17 | 0 (these dates are covered by a the first interval which is a preferred interval )
0 | 2016-02-01 | 2016-02-10 | 2 (just 2016-02-01 and 2016-02-02 because 03 - 08 are covered by the second interval which is a preferred interval)
0 | 2016-01-18 | 2016-01-19 | 2
How can I achieve this in MySQL?
assuming you have a table with columns Preferred,date_from,date_to and you're just trying to calculate # of nights.
You can try this query.
SET #checkin = '2016-01-16';
SET #checkout = '2016-02-08';
SELECT T0.preferred,T0.date_from,T0.date_to,IFNULL(NIGHTS.nights,0) as Nights
FROM YourTable T0
LEFT JOIN
(SELECT T1.preferred,T1.date_from,T1.date_to,COUNT(*) AS Nights
FROM YourTable AS T1
INNER JOIN
(SELECT (#checkin + INTERVAL n DAY) as singleday
FROM numbers
WHERE (#checkin + INTERVAL n DAY) <= #checkout)DAYS1
ON DAYS1.singleday BETWEEN T1.date_from AND T1.date_to
WHERE T1.preferred = 1
OR NOT EXISTS
(SELECT 1
FROM YourTable AS T
WHERE T.preferred = 1
AND DAYS1.singleday BETWEEN T.date_from AND T.date_to
)
GROUP BY T1.preferred,T1.date_from,T1.date_to
)NIGHTS
ON T0.preferred = NIGHTS.preferred
AND T0.date_from = NIGHTS.date_from
AND T0.date_to = NIGHTS.date_to
WHERE
T0.date_from <= #checkout
AND T0.date_to >= #checkin
;
http://sqlfiddle.com/#!9/d64344/10
you can replace #checkout and #checkin occurrences with your actual checkin and check out times.
and you can replace YourTable occurrences with your actual table name
Oh yeah in the sqlfiddle i have included a table called Numbers with column n that contains numbers from 0 counting upward to whatever maximum number of possible days of stay. You need to create this table as well.
to create table numbers use the below
CREATE TABLE numbers AS
SELECT a.n+b.n+c.n+d.n+e.n+f.n+g.n+h.n+i.n as n
FROM
(SELECT 0 as n UNION SELECT 1)a,
(SELECT 0 as n UNION SELECT 2)b,
(SELECT 0 as n UNION SELECT 4)c,
(SELECT 0 as n UNION SELECT 8)d,
(SELECT 0 as n UNION SELECT 16)e,
(SELECT 0 as n UNION SELECT 32)f,
(SELECT 0 as n UNION SELECT 64)g,
(SELECT 0 as n UNION SELECT 128)h,
(SELECT 0 as n UNION SELECT 256)i;
explaination of the query
1) subquery DAYS1 returns all single dates
from #checkin to #checkout range
2) T1 is Joined with DAYS1 WHERE
preferred is 1 OR that there doesnt exist a preferred row that covers
the DAYS1's dates
3) then we do a COUNT(*) GROUP BY
preferred,date_from,date_to to get count of single days
4) Then we call our result NIGHTS
5) Then T0 is LEFT JOINED with NIGHTS to get even rows that have 0 nights
6) And only return T0 rows that intercept out #checkin/#checkout range.
UPDATE If you table is too large you can try and narrow down your subqueries with only rows you're interested in like this
SET #checkin = '2016-01-16';
SET #checkout = '2016-02-08';
SELECT T0.preferred,T0.date_from,T0.date_to,IFNULL(NIGHTS.nights,0) as Nights
FROM (SELECT * FROM YourTable WHERE date_from <= #checkout AND date_to >= #checkin) T0
LEFT JOIN
(SELECT T1.preferred,T1.date_from,T1.date_to,COUNT(*) AS Nights
FROM (SELECT * FROM YourTable WHERE date_from <= #checkout AND date_to >= #checkin) AS T1
INNER JOIN
(SELECT (#checkin + INTERVAL n DAY) as singleday
FROM numbers
WHERE (#checkin + INTERVAL n DAY) <= #checkout)DAYS1
ON DAYS1.singleday BETWEEN T1.date_from AND T1.date_to
WHERE T1.preferred = 1
OR NOT EXISTS
(SELECT 1
FROM (SELECT * FROM YourTable WHERE date_from <= #checkout AND date_to >= #checkin) AS T
WHERE T.preferred = 1
AND DAYS1.singleday BETWEEN T.date_from AND T.date_to
)
GROUP BY T1.preferred,T1.date_from,T1.date_to
)NIGHTS
ON T0.preferred = NIGHTS.preferred
AND T0.date_from = NIGHTS.date_from
AND T0.date_to = NIGHTS.date_to
;
A rental system uses a booking table to store all bookings and reservations:
booking | item | startdate | enddate
1 | 42 | 2013-10-25 16:00 | 2013-10-27 12:00
2 | 42 | 2013-10-27 14:00 | 2013-10-28 18:00
3 | 42 | 2013-10-30 09:00 | 2013-11-01 09:00
…
Let’s say a user wants to rent item 42 from 2013-10-27 12:00 until 2013-10-28 12:00 which is a period of one day. The system will tell him, that the item is not available in the given time frame, since booking no. 2 collides.
Now I want to suggest the earliest rental date and time when the selected item is available again. Of course considering the user’s requested period (1 day) beginning with the user’s desired date and time.
So in the case above, I’m looking for an SQL query that returns 2013-10-28 18:00, since the earliest date since 2013-10-27 12:00 at which item 42 will be available for 1 day, is from 2013-10-28 18:00 until 2013-10-29 18:00.
So I need to to find a gap between bookings, that is big enough to hold the user’s reservation and that is as close a possible to the desired start date.
Or in other words: I need to find the first booking for a given item, after which there’s enough free time to place the user’s booking.
Is this possible in plain SQL without having to iterate over every booking and its successor?
If you can't redesign your database to use something more efficient, this will get the answer. You'll obviously want to parameterize it. It says find either the desired date, or the earliest end date where the hire interval doesn't overlap an existing booking:
Select
min(startdate)
From (
select
cast('2013-10-27 12:00' as datetime) startdate
from
dual
union all
select
enddate
from
booking
where
enddate > cast('2013-10-27 12:00' as datetime) and
item = 42
) b1
Where
not exists (
select
'x'
from
booking b2
where
item = 42 and
b1.startdate < b2.enddate and
b2.startdate < date_add(b1.startdate, interval 24 hour)
);
Example Fiddle
SELECT startfree,secondsfree FROM (
SELECT
#lastenddate AS startfree,
UNIX_TIMESTAMP(startdate)-UNIX_TIMESTAMP(#lastenddate) AS secondsfree,
#lastenddate:=enddate AS ignoreme
FROM
(SELECT startdate,enddate FROM bookings WHERE item=42) AS schedule,
(SELECT #lastenddate:=NOW()) AS init
ORDER BY startdate
) AS baseview
WHERE startfree>='2013-10-27 12:00:00'
AND secondsfree>=86400
ORDER BY startfree
LIMIT 1
;
Some explanation: The inner query uses a variable to move the iteration into SQL, the outer query finds the needed row.
That said, I would not do this in SQL, if the DB structure is like the given. You could reduce the iteration count by using some smort WHERE in the inner query to a sane timespan, but chances are, this won't perform well.
EDIT
A caveat: I did not check, but I assume, this won't work, if there are no prior reservations in the list - this should not be a problem, as in this case your first reservation attempt (original time) will work.
EDIT
SQLfiddle
Searching for overlapping date ranges generally yields poor performance in SQL. For that reason having a "Calendar" of available slots often makes things a lot more efficient.
For example, the booking 2013-10-25 16:00 => 2013-10-27 12:00 would actually be represented by 44 records, each one hour long.
The "gap" until the next booking at 2013-10-27 14:00 would then be represented by 2 records, each one hours long.
Then, each record could also have the duration (in time, or number of slots) until the next change.
slot_start_time | booking | item | remaining_duration
------------------+---------+------+--------------------
2013-10-27 10:00 | 1 | 42 | 2
2013-10-27 11:00 | 1 | 42 | 1
2013-10-27 12:00 | NULL | 42 | 2
2013-10-27 13:00 | NULL | 42 | 1
2013-10-27 14:00 | 2 | 42 | 28
2013-10-27 15:00 | 2 | 42 | 27
... | ... | ... | ...
2013-10-28 17:00 | 2 | 42 | 1
2013-10-28 18:00 | NULL | 42 | 39
2013-10-28 19:00 | NULL | 42 | 38
Then your query just becomes:
SELECT
*
FROM
slots
WHERE
slot_start_time >= '2013-10-27 12:00'
AND remaining_duration >= 24
AND booking IS NULL
ORDER BY
slot_start_time ASC
LIMIT
1
OK this isn't pretty in MySQL. That's because we have to fake rownum values in subqueries.
The basic approach is to join the appropriate subset of the booking table to itself offset by one.
Here's the basic list of reservations for item 42, ordered by reservation time. We can't order by booking_id, because those aren't guaranteed to be in order of reservation time. (You're trying to insert a new reservation between two existing ones, eh?) http://sqlfiddle.com/#!2/62383/9/0
SELECT #aserial := #aserial+1 AS rownum,
booking.*
FROM booking,
(SELECT #aserial:= 0) AS q
WHERE item = 42
ORDER BY startdate, enddate
Here is that subset joined to itself. The trick is the a.rownum+1 = b.rownum, which joins each row to the one that comes right after it in the booking table subset. http://sqlfiddle.com/#!2/62383/8/0
SELECT a.booking_id, a.startdate asta, a.enddate aend,
b.startdate bsta, b.enddate bend
FROM (
SELECT #aserial := #aserial+1 AS rownum,
booking.*
FROM booking,
(SELECT #aserial:= 0) AS q
WHERE item = 42
ORDER BY startdate, enddate
) AS a
JOIN (
SELECT #bserial := #bserial+1 AS rownum,
booking.*
FROM booking,
(SELECT #bserial:= 0) AS q
WHERE item = 42
ORDER BY startdate, enddate
) AS b ON a.rownum+1 = b.rownum
Here it is again, showing each reservation (except the last one) and the number of hours following it. http://sqlfiddle.com/#!2/62383/15/0
SELECT a.booking_id, a.startdate, a.enddate,
TIMESTAMPDIFF(HOUR, a.enddate, b.startdate) gaphours
FROM (
SELECT #aserial := #aserial+1 AS rownum,
booking.*
FROM booking,
(SELECT #aserial:= 0) AS q
WHERE item = 42
ORDER BY startdate, enddate
) AS a
JOIN (
SELECT #bserial := #bserial+1 AS rownum,
booking.*
FROM booking,
(SELECT #bserial:= 0) AS q
WHERE item = 42
ORDER BY startdate, enddate
) AS b ON a.rownum+1 = b.rownum
So, if you're looking for the starting time and ending time of the earliest twelve-hour slot you can use that result set to do this: http://sqlfiddle.com/#!2/62383/18/0
SELECT MIN(enddate) startdate, MIN(enddate) + INTERVAL 12 HOUR as enddate
FROM (
SELECT a.booking_id, a.startdate, a.enddate,
TIMESTAMPDIFF(HOUR, a.enddate, b.startdate) gaphours
FROM (
SELECT #aserial := #aserial+1 AS rownum,
booking.*
FROM booking,
(SELECT #aserial:= 0) AS q
WHERE item = 42
ORDER BY startdate, enddate
) AS a
JOIN (
SELECT #bserial := #bserial+1 AS rownum,
booking.*
FROM booking,
(SELECT #bserial:= 0) AS q
WHERE item = 42
ORDER BY startdate, enddate
) AS b ON a.rownum+1 = b.rownum
) AS gaps
WHERE gaphours >= 12
here is the query, it will return needed date, obvious condition - there should be some bookings in table, but as I see from question - you do this check:
SELECT min(enddate)
FROM
(
select a.enddate from table4 as a
where
a.item=42
and
DATE_ADD(a.enddate, INTERVAL 1 day) <= ifnull(
(select min(b.startdate)
from table4 as b where b.startdate>=a.enddate and a.item=b.item),
a.enddate)
and
a.enddate>=now()
union all
select greatest(ifnull(max(enddate), now()),now()) from table4
) as q
you change change INTERVAL 1 day to INTERVAL ### hour
If I have understood your requirements correctly, you could try self-JOINing book with itself, to get the "empty" spaces, and then fit. This is MySQL only (I believe it can be adapted to others - certainly PostgreSQL):
SELECT book.*, TIMESTAMPDIFF(MINUTE, book.enddate, book.best) AS width FROM
(
SELECT book.*, MIN(book1.startdate) AS best
FROM book
JOIN book AS book1 USING (item)
WHERE item = 42 AND book1.startdate >= book.enddate
GROUP BY book.booking
) AS book HAVING width > 110 ORDER BY startdate LIMIT 1;
In the above example, "110" is the looked-for minimum width in minutes.
Same thing, a bit less readable (for me), a SELECT removed (very fast SELECT, so little advantage):
SELECT book.*, MIN(book1.startdate) AS best
FROM book
JOIN book AS book1 ON (book.item = book1.item AND book.item = 42)
WHERE book1.startdate >= book.enddate
GROUP BY book.booking
HAVING TIMESTAMPDIFF(MINUTE, book.enddate, best) > 110
ORDER BY startdate LIMIT 1;
In your case, one day is 1440 minutes and
SELECT book.*, MIN(book1.startdate) AS best FROM book JOIN book AS book1 ON (book.item = book1.item AND book.item = 42) WHERE book1.startdate >= book.enddate GROUP BY book.booking HAVING TIMESTAMPDIFF(MINUTE, book.enddate, best) >= 1440 ORDER BY startdate LIMIT 1;
+---------+------+---------------------+---------------------+---------------------+
| booking | item | startdate | enddate | best |
+---------+------+---------------------+---------------------+---------------------+
| 2 | 42 | 2013-10-27 14:00:00 | 2013-10-28 18:00:00 | 2013-10-30 09:00:00 |
+---------+------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)
...the period returned is 2, i.e., at the end of booking 2, and until "best" which is booking 3, a period of at least 1440 minutes is available.
An issue could be that if no periods are available, the query returns nothing -- then you need another query to fetch the farthest enddate. You can do this with an UNION and LIMIT 1 of course, but I think it would be best to only run the 'recovery' query on demand, programmatically (i.e. if empty(query) then new_query...).
Also, in the inner WHERE you should add a check for NOW() to avoid dates in the past. If expired bookings are moved to inactive storage, this could be unnecessary.