MySQL - How to check continuity of data - mysql

I have a database containing information about sick days of employees in the following structure ( example ):
date || login
2018-01-02 || TestLogin1
2018-01-03 || TestLogin2
2018-01-04 || TestLogin5
2018-01-05 || TestLogin1
2018-01-06 || TestLogin2
And I want to check whether someone had 23 Sick Days in a row within previous 60 days.
I know how to do this in PHP, using loops , but was wondering whether there is a possibility to create this app in raw MySQL.
This is the output I want to achieve:
login || NumberOfDaysOnSickLeaveWithinPrevious2Month
TestLogin4 || 32
TestLogin7 || 30
TestLogin12 || 20
TestLogin3 || 15
TestLogin1 || 10
Will be thankful for the support,
Thanks in advance,

Your sample data suggests that you just want aggregation:
select login,
count(*) as NumberOfDaysOnSickLeaveWithinPrevious2Month
from t
where date >= curdate() - interval 2 month
group by login;
That has nothing to do with "consecutive days". But your sample data doesn't even show two days in a row with the same login -- nor even any dates within the past two months.

It's a lot easier to develop this if you shrink the numbers for example 2 or more continuous days absent in the last 5 days.
drop table if exists t;
create table t(employee_id int, dt date);
insert into t values
(1,'2018-07-10'),(1,'2018-07-11'),(1,'2018-07-12'),
(2,'2018-07-10'),(2,'2018-07-15'),
(3,'2018-07-10'),(3,'2018-07-11'),(3,'2018-07-13'),(3,'2018-07-14')
;
select employee_id, bn, count(*)
from
(
select t.*, concat(employee_id,year(dt) * 10000 + month(dt) * 100 + day(dt))
- #p = 1 diff,
if(
concat(employee_id,year(dt) * 10000 + month(dt) * 100 + day(dt))
- #p = 1 ,#bn:=#bn,#bn:=#bn+1) bn,
#p:=concat(employee_id,year(dt) * 10000 + month(dt) * 100 + day(dt)) p
from t
cross join (select #bn:=0,#p:=0) b
where dt >= date_add(date(now()), interval -5 day)
order by employee_id,dt
) s
group by employee_id,bn having count(*) >= 2 ;
+-------------+------+----------+
| employee_id | bn | count(*) |
+-------------+------+----------+
| 1 | 1 | 3 |
| 3 | 4 | 2 |
| 3 | 5 | 2 |
+-------------+------+----------+
3 rows in set (0.06 sec)
Note the use of variables to work out a block number ,and the having clause. Concating employee and date creates a psuedo key and simplifies calculation.

Related

SQL Count events with duration per hour

I have data of an event with duration (say, eating a meal at a restaurant) and I want to know for any given hour how many events were taking place. The data looks like this:
Event | Start Time | End Time
-----------------------------------------
1 | 12:03 | 14:20
2 | 12:30 | 12:50
3 | 13:05 | 14:45
4 | 14:01 | 14:49
I also have "Duration" available as an alternative to "End Time". The result I'm looking for would be like this:
Hour | Count
-----------------------
12 | 2
13 | 2
14 | 3
During hour 12, there were two events happening (1 & 2), hour 13 also had two events (1 & 3) and hour 14 had three events (1, 3, & 4).
I can do this programmatically with a loop. I can count when the events start (or end) in SQL. But I'd really like to bridge the gap and do this in SQL, but I can't think of a way.
One possible solution (works with MySQL v5.6+ and SQLite3):
create table hours(Hour int);
insert into hours values
(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),
(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23);
create table log(Event int,StartTime varchar(5),EndTime varchar(5));
insert into log values
(1,'12:03','14:20'),
(2,'12:30','12:50'),
(3,'13:05','14:45'),
(4,'14:01','14:49');
-- ------------------------------------------------------------------------------
select Hour,count(Event) Count
from log join hours
on Hour between substr(StartTime,1,2) and substr(EndTime,1,2)
group by Hour;
If you are running MySQL 8.0, you could use UNION ALL, window functions and aggregation, like so:
select hr, sum(sum(cnt)) over(order by hr) cnt
from (
select hour(start_time) hr, 1 cnt from mytable
union all select hour(end_time) + 1, -1 from mytable
) t
group by hr
Demo on DB Fiddle:
hr | cnt
-: | --:
12 | 2
13 | 2
14 | 3
15 | 0
If you do not have MySql 8, then create a table hour:
CREATE TABLE hour (
hr INT PRIMARY KEY
);
INSERT INTO hour(hr) VALUES
(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),
(12),(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23);
And then:
select h.hr, count(*) as cnt from hour h
join mytable m on h.hr between hour(m.Start_Time) and hour(m.End_Time)
group by hr
order by hr
;
See Db-Fiddle

How to get all records before any specified month in mysql

I want to select all records before any specified month using mysql. Here is my attempted statement:
SELECT SUM(amount) as allPreviousAmount FROM `fn_table`
WHERE MONTH(transdate) < 1 AND YEAR(transdate) = YEAR(CURRENT_DATE())
transdate is datetime data type.
I have data on December 2018. But this does not select the data. Then I remove the Year part, still no data is selected. The transdate is 2018-12-31 15:59:41.
Please fix it and explain why this is not working.
This will do (assuming there are no future dates):
SELECT SUM(amount) as allPreviousAmount
FROM `fn_table`
WHERE MONTH(transdate) < ? OR YEAR(transdate) < YEAR(CURRENT_DATE())
Replace ? with the month that you want the results for.
Multiply year by 100 add month (on both sides) and compare.
set #dt1 = '2019-10-01';
select #dt1,current_date,
year(#dt1) * 100 + month(#dt1),
case
when year(#dt1) * 100 + month(#dt1) < year(current_date) * 100 + month(current_date) then
'Less than'
else 'other'
end as result;
------------+--------------+--------------------------------+-----------+
| #dt1 | current_date | year(#dt1) * 100 + month(#dt1) | result |
+------------+--------------+--------------------------------+-----------+
| 2019-10-01 | 2019-11-14 | 201910 | Less than |
+------------+--------------+--------------------------------+-----------+
1 row in set (0.00 sec)

MySQL summing verses and n biggest results

I've a table looking more or less like that:
**Day** | **Mileage**
----------------
1 | 13
2 | 2
3 | 25
4 | 15
5 | 20
6 | 8
7 | 17
8 | 12
9 | 16
10 | 5
How to write a SQL query:
Returning total mileage from the firts to the nth day without using php? For example, day 1:13, day 2: 15, day 3: 40.
How to get 5 biggest mileages without using limit?
For the sum up:
select mday, (select SUM(miles) from mm s where s.mday <= mm.mday) tot_miles
from mm;
For the 5 biggest mileage per day
select mday, miles from mm order by miles desc limit 5
Change the column names and table name to match yours.
Since you added the "no use of limit", which is odd, you can try this:
set #r =0;
select * from (
select *, #r :=#r+1 as r from mm order by miles desc) e
where r<=5
Since You do not want to use limit here is the tedious way
1)
select sum(mileage) from xyz where day between 2 and 15;
2)
mysql> select #rownum:=#rownum+1 'rank',mileage
from (select mileage from xyzorder by mileage desc)mil,(select #rownum:=0)r
where #rownum<5;

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.

SQL Server 2008 information per month divided onto days

I have a table that has "periods" and looks like this (added some rows as example):
| StartDate | EndDate | Amount1 | Amount2 | Type
==============================================================
1 | 20110101 | 20110131 | 89 | 2259 | 1
2 | 20110201 | 20110228 | 103 | 50202 | 1
3 | 20110301 | 20110331 | 90 | 98044 | 1
4 | 20110401 | 20110430 | 78 | 352392 | 1
==============================================================
As you can see each "period" is exactly one month. Each month is represented four times (there are 4 types) so this table has 48 rows.
I have a selection that gives me the number of days per period (simply, the day out of the EndDate, since its always the amount of days in that period) and Amount1PerDay and Amount2PerDay (the amount of that period divided by the number of days).
The first problem is:
I need a View on this table that shows me one row for each day in every period with the Amount1 and Amount2 columns that hold the value of that period divided by the number of days in that period.
The second problem is:
Most of these divisions do not give me a whole number. Decimals are not an option, so I need to divide the remaining days (after division) among the first days of that period.
Take January as an example: Amount1 has 89. Divided over 31 days thats almost 2,9. So its 2 per day, and the first 27 days get 3 (,9 * 31 = 27). That way the result has only whole numbers in it.
Assuming StartDate and EndDate are of type date or datetime, and Amount1 & Amount2 are integers:
SELECT
Date,
Amount1 = Amount1Whole + CASE WHEN DayNum < Amount1Rem THEN 1 ELSE 0 END,
Amount2 = Amount2Whole + CASE WHEN DayNum < Amount2Rem THEN 1 ELSE 0 END,
Type
FROM (
SELECT
Date = DATEADD(day, v.number, t.StartDate),
DayNum = v.number,
Amount1Whole = Amount1 / DAY(t.EndDate),
Amount2Whole = Amount2 / DAY(t.EndDate),
Amount1Rem = Amount1 % DAY(t.EndDate),
Amount2Rem = Amount2 % DAY(t.EndDate),
t.Type
FROM atable t
INNER JOIN master..spt_values v ON v.type = 'P'
AND v.number BETWEEN 0 AND DATEDIFF(day, t.StartDate, t.EndDate)
) s
SQL Server performs integer division when both operands are integers, so, for instance, the result of 5 / 2 would be 2, and not 2.5.
master..spt_values is a long-existing system table used for internal purposes. It contains a subset of rows that you can use as a number (tally) table, which is what it serves as in this query.
Q1
How to select a range of dates is explained in this article:
http://social.msdn.microsoft.com/forums/en-US/sqlexpress/thread/916161f2-cf3c-4b6b-9015-9d13ed1af49e/
This should produce a column named THEDATE to be used for Q2
Q2
How to select the distributed amount goes like:
SELECT FLOOR(AMOUNT / NUMDAYS) + IIF( DAY(THEDATE) <= (AMOUNT - NUMDAYS * FLOOR(AMOUNT / NUMDAYS) ), 1, 0)
NUMDAYS = DATEDIFF( d, STARTDATE, ENDDATE ) + 1