SQL/Mysql Query available dates in database - mysql

I need some help querying my calendar/dates table
Scenario:
I have a "calendar" table with dates, user will set his available dates, usually day by day. So my table looks like this:
+------+------------+---------------------+---------------------+
| ID | user_id | start_date | end_date |
+------+------------+---------------------+---------------------+
| 1 | 1 | 2016-09-01 08:00:00 | 2016-09-01 16:00:00 |
| 2 | 1 | 2016-09-03 08:00:00 | 2016-09-03 16:00:00 |
| 3 | 1 | 2016-09-04 08:00:00 | 2016-09-04 16:00:00 |
| 3 | 1 | 2016-09-05 08:00:00 | 2016-09-05 16:00:00 |
+------+------------+---------------------+---------------------+
This means user 1 is available on the 1st, 3rd, 4th and 5th.
Lets say I want to query the table and find if user is available from date 2016-09-01 08:00:00 to 2016-09-05 16:00:00, this query must return zero rows since the user is not available on the 2nd of September. But if query from date 2016-09-03 08:00:00 to 2016-09-05 16:00: 00 then it will return these 3 rows.
Hope someone can help me with this

This could be one way (for a single user).
Note #endDate and #startDate are the supplied date fields to search.
SELECT
*
FROM your_table
WHERE EXISTS (
SELECT
user_id
FROM your_table
WHERE start_date >= #startDate
AND start_date <= #endDate
AND user_id = 1
GROUP BY user_id
HAVING SUM((DATEDIFF(end_date,start_date)+1)) = DATEDIFF(#endDate,#startDate)+1
)
AND start_date >= #startDate
AND start_date <= #endDate
AND user_id = 1
Note:
If the supplied date range falls within any range bounded by start_date and end_date (exclusive) then it won't work.
Since SUM((DATEDIFF(end_date,start_date)+1)) = DATEDIFF(#endDate,#startDate)+1 won't be equal in this case. Condition
In this case, you need to stay within the required boundary. Here the boundary is demarcated by the smaller value of end_date and #endDate and the larger value of start_date and #startDate.
Suppose, you have the following record (only one)
start_date = 2016-09-01 and end_date=2016-09-05.
And #startDate=2016-09-02 , #endDate=2016-09-04
Now check the above condition will fail for this set of data.
In this case you need to adopt the following query:
SELECT
*
FROM your_table
WHERE EXISTS (
SELECT
user_id
FROM your_table
WHERE end_date >= #startDate
AND start_date <= #endDate
AND user_id = 1
GROUP BY user_id
HAVING SUM((DATEDIFF(LEAST(end_date,#endDate),GREATEST(start_date,#startDate))+1)) = DATEDIFF(#endDate,#startDate)+1
)
AND end_date >= #startDate
AND start_date <= #endDate
AND user_id = 1

Assuming the periods in the table are not overlapping, you can count the number of days. The days in the period are then:
select sum(datediff(least($period_end, end_date),
greatest($period_start, start_date)
) + 1
)
from t
where $period_start <= end_date and
$period_end >= start_date;
You can then get a flag by comparing to the number of days:
select (case when sum(datediff(least($period_end, end_date),
greatest($period_start, start_date)
) + 1
) =
datediff($period_end, $period_start) + 1
then 1 else 0
end) as IsAvailableForAllDays
from t
where $period_start <= end_date and
$period_end >= start_date;

Related

With MySQL, why is this date query showing incorrect results?

Sample table:
id | foreign_key_id | timestamp | amt |
-------------------------------------------------
1 | 223344 | 2018-06-01 09:22:31 | 3
2 | 233445 | 2018-06-15 23:22:31 | 2
2 | 233445 | 2018-06-30 23:22:31 | 5
3 | 334455 | 2018-07-01 12:22:31 | 1
3 | 334455 | 2018-07-15 12:22:31 | 1
4 | 344556 | 2018-07-31 20:22:31 | 2
And what I want is a grouped result of the total of amt per month, something like,
year | month | total_amt
------------------------
2018 | 6 | 10
2018 | 7 | 4
Which I thought would be easily enough achieved with a query like,
SELECT YEAR(timestamp) year, MONTH(timestamp) month, SUM(amt) total_amt
FROM sample_table
WHERE timestamp >= '2018-06-01'
AND timestamp <= '2018-07-31'
GROUP BY YEAR(timestamp), MONTH(timestamp)
Unfortunately, the result of this query is incorrect,
year | month | total_amt
------------------------
2018 | 6 | 10
2018 | 7 | 2
The amount for June is correct, but July is wrong.
This is a misunderstanding of what is timestamp here, and then what is the compared string.
In the timestamp is a date-time object, which has both a date part, and a time part. The string used in the query is just the date part, without the time part, so when MySQL is doing its thing, it basically "zeroes out" the rest of the date.
So when the query is
...
AND timestamp <= '2018-07-31'
It basically turns into,
...
AND timestamp <= '2018-07-31 00:00:00'
And when you dump the row that isn't matched with the query,
...
AND '2018-07-31 20:22:31' <= '2018-07-31 00:00:00'
Whether it's date comparison, or even simple string comparison, the missing row's timestamp is not less or equal to the date passed in, it is definitely more.
You've a few options to fix this, create a full date-time object in the comparison, with the "fullest" of times,
...
AND timestamp <= '2018-07-31 23:59:59'
Change the operator to less-than the next date,
...
AND timestamp < '2018-08-01'
Or convert the timestamp from a date-time object to a date one,
...
AND DATE(timestamp) <= '2018-07-31'
All work, though I'm not sure about which is the best one performance / speed wise.
Try this:
SELECT YEAR(timestamp) year, MONTH(timestamp) month, SUM(amt) total_amt
FROM sample_table
WHERE timestamp >= '2018-06-01'
AND timestamp < '2018-08-01'
GROUP BY YEAR(timestamp), MONTH(timestamp)
This will work:
SELECT YEAR(timestamp) year, MONTH(timestamp) month, SUM(amt) total_amt
FROM sample_table
WHERE DATE(timestamp) >= '2018-06-01'
AND DATE(timestamp) <= '2018-08-01'
GROUP BY YEAR(timestamp), MONTH(timestamp);
OR
SELECT YEAR(timestamp) year, MONTH(timestamp) month, SUM(amt) total_amt
FROM sample_table
WHERE DATE(timestamp) BETWEEN '2018-06-01' AND '2018-08-01'
GROUP BY YEAR(timestamp), MONTH(timestamp);

Group data by Custom Period Ranges Using a Reference Date and a Set Time Period

If for example, I have a table that looks like this:
+----+--------+---------------------+
| id | volume | createdAt |
+----+--------+---------------------+
| 1 | 0.11 | 2018-01-26 13:56:01 |
| 2 | 0.34 | 2018-01-28 14:22:12 |
| 3 | 0.22 | 2018-03-11 11:01:12 |
| 4 | 0.19 | 2018-04-13 12:12:12 |
| 5 | 0.12 | 2014-04-21 19:12:11 |
+----+--------+---------------------+
I want to perform a query that can accept starting point and then loop through a given number of days, and then group by that date range.
For instance, I'd like the result to look like this:
+------------+------------+--------+
| enddate | startdate | volume |
+------------+------------+--------+
| 2018-04-25 | 2018-04-12 | 0.31 |
| 2018-04-11 | 2018-03-29 | 0.00 |
| 2018-03-28 | 2018-03-15 | 0.00 |
| 2018-03-14 | 2018-03-01 | 0.22 |
| 2018-02-28 | 2018-02-15 | 0.00 |
| 2018-02-14 | 2018-02-01 | 0.00 |
| 2018-01-31 | 2018-01-18 | 0.45 |
| ... | ... | ... |
+------------+------------+--------+
In essence, I want to be able to input a start_date e.g 2018-04-25, a time_interval e.g. 14, like in the illustration above and then the query will sum the volumes in that time range.
I know how to use INTERVAL with the DATE_SUB() and the DATE_ADD() functions but I cannot figure out how to perform the loop I think is necessary.
Please help.
For the given data you can determine time based groupings using the datediff and floor functions:
floor(datediff(createdat, date '2018-04-25')/14) grp
From the group number you can determine the periods stardate and enddate:
date_add(date '2018-04-25', interval (grp*14) day) startdate
date_add(date '2018-04-25', interval ((grp+1)*14) day) enddate
Which represent a half open range with startdate being inclusive and enddate being exclusive.
Putting these together in a usable query:
select startdate, enddate, sum(volume)
from (select t1.*
, date_add(date '2018-04-25', interval (grp*14) day) startdate
, date_add(date '2018-04-25', interval ((grp+1)*14) day) enddate
from (select t.*
, datediff(t.createdat, date '2018-04-25') diff
, floor(datediff(t.createdat, date '2018-04-25')/14) grp
from table1 t) t1) t2
group by startdate, enddate
order by startdate desc;
Unfortunately this does not get the empty periods. To get the empty periods you need a way to generate rows. However, MySQL doesn't have a simple way to generate rows (at least not until MySQL 8 where common table expressions and recursive SQL are added), but there are database objects that already have a large number of rows, such as the information_schema.columns view which likely has sufficient rows for your needs, and if it doesn't, a cross join or two will easily multiply the number of records generated. That paired with a variable that increments for each row returned will provide the needed groups:
select #rn:=#rn+1 rn
, stop
, date_add(date '2018-04-25', interval (#rn*14) day) startdate
, date_add(date '2018-04-25', interval ((#rn+1)*14) day) enddate
from information_schema.columns c
, (select #rn:=min(floor(datediff(createdat, date '2018-04-25')/14))-1
, max(floor(datediff(createdat, date '2018-04-25')/14)) stop
from table1) limits
where #rn < stop;
Outer joining this with the original data and grouping by the period dates yields:
select startdate
, enddate
, sum(volume) volume
from table1
right join (
select #rn:=#rn+1 rn
, stop
, date_add(date '2018-04-25', interval (#rn*14) day) startdate
, date_add(date '2018-04-25', interval ((#rn+1)*14) day) enddate
from information_schema.columns c
-- , information_schema.columns d -- if needed add another cartesian join
, (select #rn:=min(floor(datediff(createdat, date '2018-04-25')/14))-1
, max(floor(datediff(createdat, date '2018-04-25')/14)) stop
from table1) limits
where #rn < stop) periods
on startdate <= createdat
and createdat < enddate
group by startdate, enddate
order by startdate desc;
Take a look at the SQL Fiddle to see this in action
All you need to do it determine start_date(which is the parameter you pass) and end_date from your entire table and loop through them by adding time interval.
Have a look at below stored routine:
CREATE DEFINER=`root`#`localhost` PROCEDURE `getTotalVolumeByDateRange`(start_time timestamp, time_interval int)
BEGIN
DECLARE max_date date;
DECLARE min_date date;
DECLARE temp_end_date date;
SET min_date = DATE(start_time);
SELECT DATE(MAX(createdAt)) FROM VolumeData INTO max_date;
-- SELECT max_date, min_date;
CREATE TEMPORARY TABLE tempRangedVolumeData(
start_date date,
end_date date,
Volume decimal(5,2)
);
while min_date <= max_date DO
SET temp_end_date = DATE_ADD(min_date, Interval time_interval DAY);
INSERT INTO tempRangedVolumeData(start_date, end_date, Volume)
SELECT min_date, temp_end_date, SUM(Volume)
FROM VolumeData
WHERE DATE(CreatedAt) BETWEEN min_date and temp_end_date;
SET min_date = DATE_ADD(min_date, Interval time_interval+1 DAY);
end while;
select
start_date,
end_date,
coalesce(Volume,0) as Volume
from tempRangedVolumeData;
drop table tempRangedVolumeData;
END
I hope this helps. Please comment if i am missing any edge case.

SQL Select data between date and hour in two columns

I have a SQL column like this:
| id | name | date | hour |
I want to select all names between specified start_date, start_hour and end_date, end_hour.
For data like this:
| 1 | one | 2014-12-29 | 11:00 |
| 2 | two | 2014-12-30 | 09:00 |
| 3 | three | 2014-12-30 | 11:00 |
Values:
start_date = 2014-12-29
start_hour = 11:00
end_date = 2014-12-30
end_hour = 10:00`
It should return: one, two.
This looks like a basic sql query. try the following:
select a.name
from (select * from data_table
where date >= start_date
and date <= end_date) as a
where a.hour >= start_hour
and a.hour <= end_hour
the basic idea is to first form a data set of all valid days for all times and then from there pull out the valid times. The other possibly more direct method would be to create a datetime field and then pull on that key.
Let me know if this works for you.
SELECT *
FROM table
WHERE date > '2014-12-28'
AND hour > '10:59'
AND date < '2014-12-31'
AND hour < '10:01'

Retrieve all records that occur within specific date range in MySQL

I have a table that contains several contracts, and each contract has a start date and an end date, like this:
| ID | Contract Name | Start Date | End Date |
|-------|-------------------|--------------|------------|
| 1 | Joe Bloggs | 2012-01-01 | 2012-02-05 |
| 2 | John Smiths | 2012-02-01 | 2012-02-20 |
| 3 | Johnny Briggs | 2012-03-01 | 2012-03-20 |
What I am trying to do is build a query that will retrieve contracts that were active between a specific time period. So if I had the start date of 2012-02-10 and an end date of 2012-03-21 I should have the following contracts displayed:
| ID | Contract Name | Start Date | End Date |
|-------|-------------------|--------------|------------|
| 2 | John Smiths | 2012-02-01 | 2012-02-20 |
| 3 | Johnny Briggs | 2012-03-01 | 2012-03-20 |
My problem though is that I don't know how to build the query to do this. This is what I've got so far:
SELECT *
FROM contracts c
WHERE c.startdate BETWEEN '2012-02-10'
AND '2012-03-21'
AND c.enddate BETWEEN '2012-02-10'
AND '2012-03-21'
This doesn't work though, no records are retrieved. What am I doing wrong?
SELECT * FROM contracts
WHERE (START_DATE between '2012-03-01' AND '2013-03-21')
OR (END_DATE between '2012-03-01' AND '2013-03-21')
OR (START_DATE<= '2012-03-01' AND END_DATE >='2013-03-21');
Check the SQL fiddle
Er, time is linear right?
SELECT *
FROM contracts
WHERE end_date >= '2012-02-10'
AND start_date <= '2012-03-21';
Let me illustrate...
A-------------B
<------->
<------>
<----------->
<---------------------->
In all cases above, the start date is less than B. The end date is greater than A.
For me, a good request will be
SELECT * FROM contracts c WHERE c.startdate >'2012-02-10' AND c.enddate < '2012-03-21'
It should have been like this
SELECT *
FROM contracts c
WHERE c.startdate >= '2012-02-10'
AND c.enddate <= '2012-03-21'
SELECT * FROM contracts
WHERE
(START_DATE between '2012-03-01' AND '2013-03-21')
OR (END_DATE between '2012-03-01' AND '2013-03-21')
OR (START_DATE<= '2012-03-01' AND END_DATE >='2013-03-21');
explanation:
(START_DATE between '2012-03-01' AND '2013-03-21')
: intervals records that start between the input dates. First part or all of interval might be included.
(END_DATE between '2012-03-01' AND '2013-03-21')
: intervals that end between the input dates. Last part or all of interval might be included.
(START_DATE<= '2012-03-01' AND END_DATE >='2013-03-21')
: input dates are included within one interval only

Select date intersection

i have this table
+------+--------+-------------------------+
| id | amount | start_date | end_date |
| 10 | 10 | 2012-01-15 | 2012-01-20 |
| 10 | 12 | 2012-01-14 | 2012-01-15 |
| 10 | 22 | 2012-01-15 | 2012-01-16 |
+------+--------+-------------------------+
i'd wish to find the sum between a given interval.
Example:
start date: 2012-01-13
end date: 2012-01-18
sum(amount) = 44.
start date: 2012-01-18
end date: 2012-01-21
sum(amount) = 10
is that possible with a single query?
is that possible at all?
EDIT
the logic behind is that if the intevarls (given one and inside the table) overlaps, i should get the row.
SELECT SUM(amount)
FROM TableX
WHERE start_date <= #EndDate
AND #StartDate <= end_date
where #StartDate and #EndDate are the dates you want to check between.
SELECT SUM(AMOUNT) FROM table WHERE start_date > '2012-01-13' AND end_date < '2012-01-18'
Sure, you can do:
select sum(amount) from my_dates where start_date >= '2012-01-13' and end_date <= '2012-01-18'
Gives you 34. If you want to get 44 you'd do:
select sum(amount) from my_dates where start_date >= '2012-01-13' and end_date <= '2012-01-20'
Hope this helps
SELECT SUM(`amount`) as TotalAmount
FROM `tableName`
WHERE (start_date >= '2012-01-13 00:00:00'
AND
end_date <= '2012-01-18 23:59:59')