Is there a way to select or query the data only on working hours?
id
description
datetime
1
Alarm Activated
2022-01-02 14:00:00
2
Alarm Deactivated
2022-01-02 15:00:00
3
Alarm Activated
2022-01-03 18:00:00
..
Alarm Activated
2022-01-31 11:00:00
I'd like to get the number of the alarm activated on or during working hours from mon-fri 8am to 5pm.
I tried to use the between date but no luck.
SELECT * -- if you need to count them only - use SELECT COUNT(*)
FROM datatable
WHERE WEEKDAY(`datetime`) < 5 -- test weekday
AND TIME(`datetime`) BETWEEN '08:00:00' AND '17:00:00'; -- test hours
Here we use the following tests:
weekday < 6 (Saturday) = Monday to Friday
hour more than 7 (from 08:00:00)
hour less than 17 (to 16:59:59)
create table alarms(
id int,
description varchar(100),
date_time datetime);
insert into alarms values
(1,'Alarm Activated',
'2022-01-02 14:00:00'),
(2,'Alarm Deactivated',
'2022-01-02 15:00:00'),
(3,'Alarm Activated',
'2022-01-03 18:00:00'),
(4,'Alarm Activated',
'2022-01-31 11:00:00');
select *
from alarms
where weekday(date_time) < 6
and 7 < hour(date_time) < 17;
id | description | date_time
-: | :-------------- | :------------------
3 | Alarm Activated | 2022-01-03 18:00:00
4 | Alarm Activated | 2022-01-31 11:00:00
db<>fiddle here
MySQL version 8.0
I want to calculate time difference between two datetime column.
And get rows where duration >= 12:00:00.
which I would normally do:
select id
, start_time
, end_time
, timediff(end_time, start_time) as duration
from table;
which I would get something like this:
id start_time end_time duration
0 1 2020-06-01 01:00:00 2020-06-01 14:00:00 13:00:00
1 2 2020-06-01 01:00:00 2020-06-01 18:00:00 17:00:00
2 3 2020-06-01 19:00:00 2020-06-02 10:00:00 15:00:00
3 4 2020-06-02 04:00:00 2020-06-02 16:00:00 12:00:00
For duration column I don't want times between 00:00:00 ~ 04:00:00 to be added towards the duration. So for the first row duration = 10:00:00 since 01:00:00~14:00:00 = 10:00:00, ignoring times between 00:00:00 ~ 04:00:00
same for second row we substract 3 hours from duration.
so my desired output would be:
id start_time end_time duration
0 1 2020-06-01 01:00:00 2020-06-01 14:00:00 10:00:00
1 2 2020-06-01 01:00:00 2020-06-01 18:00:00 14:00:00
2 3 2020-06-01 19:00:00 2020-06-02 10:00:00 11:00:00
3 4 2020-06-02 04:00:00 2020-06-02 16:00:00 12:00:00
There are lots of rows where times include minutes and seconds too.
Thanks in advance!
I've grabbed all rows where duration >= 12:00:00.
Then separated data into 4 regions depending on their start_time.
a_region = 00~04
b_region = 04~12
c_region = 12~16
d_region = 16~24
For a_region I've subtracted 04:00:00 - start_time which is time we should compensate to duration in a_region.
compensation = 04:00:00 - start_time
compensated_time = duration - compensation.
For b_region it needs no compensation if it has passed 00~04 it means it already passed duration = 12:00:00.
For c_region,
compensation = 16:00:00 - start_time
compensated_time = duration - compensation
For d_region since we've grabbed duration >= 12:00:00
it will pass all of 00~04 therefore
compensated_time = duration - 04:00:00.
I solved it using Python but above is the logic I've used.
One option uses greatest():
select id
, start_time
, end_time
, timediff(
greatest(,
end_time,
date_format(end_time, '%Y-%m-%d 04:00:00')
),
greatest(
start_time,
date_format(start_time, '%Y-%m-%d 04:00:00')
)
) as duration
from table;
For my website, I have a loyalty program where a customer gets some goodies if they've spent $100 within the last 30 days. A query like below:
SELECT u.username, SUM(total-shipcost) as tot
FROM orders o
LEFT JOIN users u
ON u.userident = o.user
WHERE shipped = 1
AND user = :user
AND date >= DATE(NOW() - INTERVAL 30 DAY)
:user being their user ID. Column 2 of this result gives how much a customer has spent in the last 30 days, if it's over 100, then they get the bonus.
I want to display to the user which day they'll leave the loyalty program. Something like "x days until bonus expires", but how do I do this?
Take today's date, March 16th, and a user's order history:
id | tot | date
-----------------------
84 38 2016-03-05
76 21 2016-02-29
74 49 2016-02-20
61 42 2015-12-28
This user is part of the loyalty program now but leaves it on March 20th. What SQL could I do which returns how many days (4) a user has left on the loyalty program?
If the user then placed another order:
id | tot | date
-----------------------
87 12 2016-03-09
They're still in the loyalty program until the 20th, so the days remaining doesn't change in this instance, but if the total were 50 instead, then they instead leave the program on the 29th (so instead of 4 days it's 13 days remaining). For what it's worth, I care only about 30 days prior to the current date. No consideration for months with 28, 29, 31 days is needed.
Some create table code:
create table users (
userident int,
username varchar(100)
);
insert into users values
(1, 'Bob');
create table orders (
id int,
user int,
shipped int,
date date,
total decimal(6,2),
shipcost decimal(3,2)
);
insert into orders values
(84, 1, 1, '2016-03-05', 40.50, 2.50),
(76, 1, 1, '2016-02-29', 22.00, 1.00),
(74, 1, 1, '2016-02-20', 56.31, 7.31),
(61, 1, 1, '2015-12-28', 43.10, 1.10);
An example output of what I'm looking for is:
userident | username | days_left
--------------------------------
1 Bob 4
This is using March 16th as today for use with DATE(NOW()) to remain consistent with the previous bits of the question.
The following is basically how to do what you want. Note that references to "30 days" are rough estimates and what you may be looking for is "29 days" or "31 days" as works to get the exact date that you want.
Retrieve the list of dates and amounts that are still active, i.e., within the last 30 days (as you did in your example), as a table (I'll call it Active) like the one you showed.
Join that new table (Active) with the original table where a row from Active is joined to all of the rows of the original table using the date fields. Compute a total of the amounts from the original table. The new table would have a Date field from Active and a Totol field that is the sum of all the amounts in the joined records from the original table.
Select from the resulting table all records where the Amount is greater than 100.00 and create a new table with Date and the minimum Amount of those records.
Compute 30 days ahead from those dates to find the ending date of their loyalty program.
You would need to take the following steps (per user):
join the orders table with itself to calculate sums for different (bonus) starting dates, for any of the starting dates that are in the last 30 days
select from those records only those starting dates which yield a sum of 100 or more
select from those records only the one with the most recent starting date: this is the start of the bonus period for the selected user.
Here is a query to do that:
SELECT u.userident,
u.username,
MAX(base.date) AS bonus_start,
DATE(MAX(base.date) + INTERVAL 30 DAY) AS bonus_expiry,
30-DATEDIFF(NOW(), MAX(base.date)) AS bonus_days_left
FROM users u
LEFT JOIN (
SELECT o.user,
first.date AS date,
SUM(o.total-o.shipcost) as tot
FROM orders first
INNER JOIN orders o
ON o.user = first.user
AND o.shipped = 1
AND o.date >= first.date
WHERE first.shipped = 1
AND first.date >= DATE(NOW() - INTERVAL 30 DAY)
GROUP BY o.user,
first.date
HAVING SUM(o.total-o.shipcost) >= 100
) AS base
ON base.user = u.userident
GROUP BY u.username,
u.userident
Here is a fiddle.
With this input as orders:
+----+------+---------+------------+-------+----------+
| id | user | shipped | date | total | shipcost |
+----+------+---------+------------+-------+----------+
| 61 | 1 | 1 | 2015-12-28 | 42 | 0 |
| 74 | 1 | 1 | 2016-02-20 | 49 | 0 |
| 76 | 1 | 1 | 2016-02-29 | 21 | 0 |
| 84 | 1 | 1 | 2016-03-05 | 38 | 0 |
| 87 | 1 | 1 | 2016-03-09 | 50 | 0 |
+----+------+---------+------------+-------+----------+
The above query will return this output (when executed on 2016-03-20):
+-----------+----------+-------------+--------------+-----------------+
| userident | username | bonus_start | bonus_expiry | bonus_days_left |
+-----------+----------+-------------+--------------+-----------------+
| 1 | John | 2016-02-29 | 2016-03-30 | 10 |
+-----------+----------+-------------+--------------+-----------------+
Simple solution
Seeing how you do your first query, I guessed that when you are at the point where you look for the "expiration date", you already know that the user meets the 100 points over last 30 days. Then you can do this :
SELECT DATE_ADD(MIN(date),INTERVAL 30 DAY)
FROM orders o
WHERE shipped = 1
AND user = :user
AND date >= (DATE(NOW() - INTERVAL 30 DAY))
It takes the minimum order date of a user over the last 30 days, and add 30 days to the result.
But that really is a poor design to achieve what you want.
You would better to think further and implement what's next.
Advanced solution
In order to reproduce all the following solution, I have used the Fiddle that Trincot kindly built, and expanded it to test on more data : 4 users having 4 orders.
SQL FIddle http://sqlfiddle.com/#!9/668939/1
Step 1 : Design
The following query will return all the users meeting the loyalty program criteria, along with their earlier order date within 30 days and the loyalty program expiration date calculated from the earlier date, and the number of days before it expires.
SELECT O.user, u.username, SUM(total-shipcost) as tot, MIN(date) AS mindate,
DATE_ADD(MIN(date),INTERVAL 30 DAY) AS expirationdate,
DATEDIFF(DATE_ADD(MIN(date),INTERVAL 30 DAY), DATE(NOW())) AS daysleft
FROM orders o
LEFT JOIN users u
ON u.userident = o.user
WHERE shipped = 1
AND date >= DATE(NOW() - INTERVAL 30 DAY)
GROUP BY user
HAVING tot >= 100;
Now, create a VIEW with the above query
CREATE VIEW loyalty_program AS
SELECT O.user, u.username, SUM(total-shipcost) as tot, MIN(date) AS mindate,
DATE_ADD(MIN(date),INTERVAL 30 DAY) AS expirationdate,
DATEDIFF(DATE_ADD(MIN(date),INTERVAL 30 DAY), DATE(NOW())) AS daysleft
FROM orders o
LEFT JOIN users u
ON u.userident = o.user
WHERE shipped = 1
AND date >= DATE(NOW() - INTERVAL 30 DAY)
GROUP BY user
HAVING tot >= 100;
It is important to understand that this is only a one-shot action on your database.
Step 2 : Use your new VIEW
Once you have the view, you can get easily, for all users, the "state" of the loyalty program:
SELECT * FROM loyalty_program
user username tot mindate expirationdate daysleft
1 John 153 February, 28 2016 March, 29 2016 9
2 Joe 112 February, 24 2016 March, 25 2016 5
3 Jack 474 February, 23 2016 March, 24 2016 4
4 Averel 115 February, 22 2016 March, 23 2016 3
For a specific user, you can get the date you are looking for like this:
SELECT expirationdate FROM loyalty_program WHERE username='Joe'
You can also request all the users for which the expiration date is today
SELECT user FROM loyalty_program WHERE expirationdate=DATE(NOW))
But there are other easy possibilities that you'll discover after having played with your VIEW.
Conclusion
Make your life easier: learn to use VIEWS !
I am assuming your table looks like this:
user | id | total | date
-------------------------------
12 84 38 2016-03-05
12 76 21 2016-02-29
23 74 49 2016-02-20
23 61 42 2015-12-28
then try this:
SELECT x.user, x.date, x.id, x.cum_sum, d,date, DATEDIFF(NOW(), x.date) from (SELECT a.user, a.id, a.date, a.total,
(SELECT SUM(b.total) FROM order_table b WHERE b.date <= a.date and a.user=b.user ORDER BY b.user, b.id DESC) AS cum_sum FROM order_table a where a.date>=DATE(NOW() - INTERVAL 30 DAY) ORDER BY a.user, a.id DESC) as x
left join
(SELECT c.user, c.date as start_date, c.id from (SELECT a.user, a.id, a.date, a.total,
(SELECT SUM(b.total) FROM order_table b WHERE b.date <= a.date and a.user=b.user ORDER BY b.user, b.id DESC) AS cum_sum FROM order_table a where a.date>=DATE(NOW() - INTERVAL 30 DAY) ORDER BY a.user, a.id DESC) as c WHERE FLOOR(c.cum_sum/100)=MIN(FLOOR(c.cum_sum/100)) and MOD(c.cum_sum,100)=MAX(MOD(c.cum_sum,100)) group by concat(c.user, "_", c.id)) as d on concat(x.user, "_", x.id)=concat(d.user, "_", d.id) where x.date=d.date;
You will get a table something like this:
user | Date | cum_sum | start_date | Time_left
----------------------------------------------------
12 2016-03-05 423 2016-03-05 24
13 2016-02-29 525 2016-02-29 12
23 2016-02-20 944 2016-02-20 3
29 2015-12-28 154 2015-12-28 4
i have not tested this. But what i am trying to do is to create a table in descending order of id and user, and get a cumulative total column along with it. I have created another table by using this table with cumulative total, with relevant date (i.e. date from which date difference is to be calculated) for each user. I have left joined these two tables, and put in the condition x.date=d.date. I have put start_date and date in the table to check if the query is working.
Also, this is not the most optimum way of writing this code, but i have tried to stay as safe as possible by using sub queries, since i did not have the data to test this. Let me know if you face any error.
I am not very skilled at SQL so hopefully someone here can help me out.
I have a date_of_post column in my table which looks like this (example) 2015-08-31 11:00:00.
I use the INTERVAL 1 DAY to get the last 24 hours. However it returns more than the last 24 hours it seems. This is the query I use to fetch my data
SELECT DATE_ADD(date(t.date_of_post),
INTERVAL hour(t.date_of_post) HOUR) AS dateTime,
count(*) as entries
FROM `soc_stat` t
WHERE `main_tag` = 'morgenmad'
AND t.date_of_post > DATE_SUB(CURDATE(), INTERVAL 1 DAY)
GROUP BY date(t.date_of_post), hour(t.date_of_post)
And it returns the following:
2015-08-31 11:00:00 = 11
2015-08-31 12:00:00 = 2
2015-08-31 13:00:00 = 3
2015-08-31 14:00:00 = 3
2015-08-31 15:00:00 = 1
2015-08-31 16:00:00 = 3
2015-08-31 17:00:00 = 2
2015-08-31 19:00:00 = 1
2015-09-01 04:00:00 = 1
2015-09-01 05:00:00 = 3
2015-09-01 06:00:00 = 9
2015-09-01 07:00:00 = 33
2015-09-01 08:00:00 = 38
2015-09-01 09:00:00 = 29
2015-09-01 10:00:00 = 13
2015-09-01 11:00:00 = 12
2015-09-01 12:00:00 = 6
2015-09-01 13:00:00 = 5
I don't understand why 11:00:00, 12:00:00 and 13:00:00 exist in 2015-08-31 and 2015-09-01. Shouldn't it only return the last 24 hours?
CURDATE() returns the "beginning of today". replace it with NOW()
A visual might help. If you use aliases stick with them throughout. When you use aggregate functions like count, group by all non-aggregate columns.
for me it is 2015-09-01 08:47:00
create table soc_stat
( id int auto_increment primary key,
main_tag varchar(20) not null,
date_of_post datetime not null
);
truncate table soc_stat;
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-09-02 11:00:00');
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-09-01 11:00:00');
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-09-01 09:00:00');
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-09-01 08:00:00');
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-09-01 07:00:00');
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-08-31 09:00:00');
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-08-31 08:00:00');
insert soc_stat (main_tag,date_of_post) values ('morgenmad','2015-08-31 07:00:00');
SELECT date(t.date_of_post) dt, hour(t.date_of_post) hr,count(*) as entries
FROM `soc_stat` t
WHERE t.`main_tag` = 'morgenmad'
AND t.date_of_post between DATE_SUB(now(), INTERVAL 1 DAY) and now()
GROUP BY dt,hr
order by t.date_of_post desc;
+------------+------+---------+
| dt | hr | entries |
+------------+------+---------+
| 2015-09-01 | 8 | 1 |
| 2015-09-01 | 7 | 1 |
| 2015-08-31 | 9 | 1 |
+------------+------+---------+
I got an alert table for users, in which we have to send alerts to users in user defined intervals like 0 ( only once), 3 months, 6 months, 1 year
So I designed a table like this
id | user_id | alert_date | repeat_int
-----+--------------+-------------------------+-------------
12 | 747 | 2013-04-19 00:00:00 | 0
13 | 746 | 2013-03-19 00:00:00 | 1
14 | 745 | 2012-04-19 00:00:00 | 0
15 | 744 | 2013-04-19 00:00:00 | 0
16 | 743 | 2013-05-19 00:00:00 | 0
We are sending alert just a day before "alert_date"
With the following query I can fetch the data
SELECT al.id,
al.user_id,
al.alert_date,
al.repeat_int AS repunit
FROM alerts AS al
WHERE DATE_ADD(alert_date,INTERVAL repeat_int MONTH)=date_add(CURRENT_DATE,INTERVAL 1 DAY)
OR date(al.alert_date)=date_add(CURRENT_DATE,INTERVAL 1 DAY)
Its working file but my real problem is
The repeat will only works once, we need it repeat every interval
ie. If alert date is 2012-03-14 and repeat_int is 0 - Need to work only once
but if alert date is 2012-03-14 and repeat_int is 1 - Need to work in every 14th from 2012-03-14
and if the alert date is 2012-03-14 and repeat_int is 3 - Need to work in every three month's 14. ie alert on 2012-03-14, 2012-06-14, 2012-09-14 etc...
Is there any way to do that?
Update
The OP has changed his schema in response to comments, so the query is essentially:
SELECT *
FROM alerts
WHERE CURRENT_DATE + INTERVAL 1 DAY = COALESCE(next_alert_date, alert_date);
This handles "next_alert_date" being NULL on the very first run.
Original answer
For the original schema:
SELECT *
FROM alerts
JOIN (SELECT CURRENT_DATE + INTERVAL 1 DAY AS tomorrow) d
WHERE -- We want to alert if
-- 1. Tomorrow is the alert_date
tomorrow = alert_date
OR
--
-- 2. Tomorrow is "repeat_int" months removed from alert_date, falling on
-- the same day of the month or on the end of the month if the original
-- alert_date day of month is later in the month than is possible for us
-- now. E.g., 2013-01-31 repeated monthly is adjusted to 2013-02-28.
(
PERIOD_DIFF(DATE_FORMAT(tomorrow, '%Y%m'), DATE_FORMAT(alert_date, '%Y%m'))
MOD repeat_int = 0
AND
-- Make sure we are at the same day of the month
( (DAYOFMONTH(tomorrow) = DAYOFMONTH(alert_date)
OR
-- Or, if the day of the alert is beyond the last day of our month,
-- that we are at the end of our month.
(LAST_DAY(alert_date) > LAST_DAY(tomorrow)
AND
DAYOFMONTH(tomorrow) = LAST_DAY(tomorrow)) )
);