SQL: Calculate average of difference between rows - mysql

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

Related

How to calculate difference between two rows

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)

Selected Intervals of dates in MySQL

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
;

Use a sub query result

I have a table with numbers and dates (1 number each date and dates aren't necessarily at regular intervals).
I would like to get the count of dates when a number isn't in the table.
Where I am :
select *
from
(
select
date from nums
where chiffre=1
order by date desc
limit 2
) as f
I get this :
date
--------------
2014-09-07
--------------
2014-07-26
Basically, I have this query dynamically:
select * from nums where date between "2014-07-26" and "2014-09-07"
And in a second time, browse the whole table (because there I limited to the first 2 rows but I would compare the 2 and 3 and 3 and 4 etc...)
The goal is to get this:
date | actual_number_of_real_dates_between_two_given_dates
2014-09-07 - 2014-07-26 | 20
2014-04-02 - 2014-02-12 | 13
etc...
How can I do this? Thanks.
Edit:
What I have (just an example, dates and "chiffre" are more complex) :
date | chiffre
2014-09-30 | 2
2014-09-29 | 1
2014-09-28 | 2
2014-09-27 | 2
2014-09-26 | 1
2014-09-25 | 2
2014-09-24 | 2
etc...
What I need for the number "1":
actual_number_of_real_dates_between_two_given_dates
1
3
etc...
Edit 2:
My updated query thanks to Gordon Linoff
select count(n.id) as difference
from nums n inner join
(select min(date) as d1, max(date) as d2
from (select date from nums where chiffre=1 order by date desc limit 2) d
) dd
where n.date between dd.d1 and dd.d2
How can I test row 2 with 3? 3 with 4 etc... Not only last 2?
Should I use a loop? Or I can do it without?
Does this do what you want?
select count(distinct n.date) as numDates,
(datediff(dd.d2, dd.d1) + 1) as datesInPeriod,
(datediff(dd.d2, dd.d1) + 1 - count(distinct n.date)) as missingDates
from nums n cross join
(select date('2014-07-26') as d1, date('2014-09-07') as d2) d
where n.date between dd.d1 and dd.d2;
EDIT:
If you just want the last two dates:
select count(distinct n.date) as numDates,
(datediff(dd.d2, dd.d1) + 1) as datesInPeriod,
(datediff(dd.d2, dd.d1) + 1 - count(distinct n.date)) as missingDates
from nums n cross join
(select min(date) as d1, max(date) as d2
from (select date from nums order by date desc limit 2) d
) dd
where n.date between dd.d1 and dd.d2;

Find big enough gaps in booking table

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.

Compute outstanding amounts in MySQL

I am having an issue with a SELECT command in MySQL. I have a database of securities exchanged daily with maturity from 1 to 1000 days (>1 mio rows). I would like to get the outstanding amount per day (and possibly per category). To give an example, suppose this is my initial dataset:
DATE VALUE MATURITY
1 10 3
1 15 2
2 10 1
3 5 1
I would like to get the following output
DATE OUTSTANDING_AMOUNT
1 25
2 35
3 15
Outstanding amount is calculated as the total of securities exchanged still 'alive'. That means, in day 2 there is a new exchange for 10 and two old exchanges (10 and 15) still outstanding as their maturity is longer than one day, for a total outstanding amount of 35 on day 2. In day 3 instead there is a new exchange for 5 and an old exchange from day 1 of 10. That is, 15 of outstanding amount.
Here's a more visual explanation:
Monday Tuesday Wednesday
10 10 10 (Day 1, Value 10, matures in 3 days)
15 15 (Day 1, 15, 2 days)
10 (Day 2, 10, 1 day)
5 (Day 3, 5, 3 days with remainder not shown)
-------------------------------------
25 35 15 (Outstanding amount on each day)
Is there a simple way to get this result?
First of all in the main subquery we find SUM of all Values for current date. Then add to them values from previous dates according their MATURITY (the second subquery).
SQLFiddle demo
select T1.Date,T1.SumValue+
IFNULL((select SUM(VALUE)
from T
where
T1.Date between
T.Date+1 and T.Date+Maturity-1 )
,0)
FROM
(
select Date,
sum(Value) as SumValue
from T
group by Date
) T1
order by DATE
I'm not sure if this is what you are looking for, perhaps if you give more detail
select
DATE
,sum(VALUE) as OUTSTANDING_AMOUNT
from
NameOfYourTable
group by
DATE
Order by
DATE
I hope this helps
Each date considers each row for inclusion in the summation of value
SELECT d.DATE, SUM(m.VALUE) AS OUTSTANDING_AMOUNT
FROM yourTable AS d JOIN yourtable AS m ON d.DATE >= m.MATURITY
GROUP BY d.DATE
ORDER BY d.DATE
A possible solution with a tally (numbers) table
SELECT date, SUM(value) outstanding_amount
FROM
(
SELECT date + maturity - n.n date, value, maturity
FROM table1 t JOIN
(
SELECT 1 n UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5
) n ON n.n <= maturity
) q
GROUP BY date
Output:
| DATE | OUTSTANDING_AMOUNT |
-----------------------------
| 1 | 25 |
| 2 | 35 |
| 3 | 15 |
Here is SQLFiddle demo