MySQL Use Virtual Columns as Join Points? - mysql

I have the three following tables:
monthly:
| u_id | memb_type | run_day |
------------------------------------
| 1 | 1 | 410 |
| 2 | 1 | 410 |
| 1 | 2 | 510 |
| 2 | 1 | 510 |
| 1 | 2 | 610 |
| 2 | 1 | 610 |
memb_types:
| id | name |
----------------------------
| 1 | member |
| 2 | active_member |
user:
| id | join_date |
----------------------------
| 1 | 2015-03-01 |
| 2 | 2015-04-04 |
The query I'm attempting to write will show what memb_type the user was the first two months of them joining. The tricky part is monthly.run_day is not a valid datetime, and it basically requires creating multiple virtual columns I have something along the lines of this right now and I'm stuck:
SELECT
user.id,
user.join_date,
MONTH(user.join_date + INTERVAL 1 MONTH) AS `first_month`,
"???" AS `first_month_memb_type`,
MONTH(user.join_date + INTERVAL 2 MONTH) AS `second_month`,
"???" AS `second_month_memb_type`
FROM
user
INNER JOIN
monthly
ON
user.id = monthly.u_id
INNER JOIN
memb_types
ON
monthly.memb_type = memb_types.id
I'm not exactly sure how to use the data generated from MONTH(user.join_date + INTERVAL 1 MONTH) and join that back to the monthly table joined on the run_day (SUBSTRING(monthly.run_day, 1, 1) Does what we need to match the value from the month to the run_day) But I don't know how to join that back to the monthly table twice for the first_month_memb_type and the second_month_memb_type.
Based on the needs the desired output would be as follows (Abbreviated for brevity):
| id | join_date | fm | fm_type | sm | sm_type |
-------------------------------------------------------
| 1 | 2015-03-01 | 4 | member | 5 | active_member |
| 2 | 2015-04-04 | 5 | member | 6 | member |
Any ideas how to accomplish this?

It's okay to use those functions as the join predicates, which means we just need to join a couple of extra times to get your first and second months all in a row, and only once - instead of two rows per u_id.
select u.id,
u.join_date,
month(u.join_date + interval 1 month) fm,
mt1.name fm_type,
month(u.join_date + interval 2 month) 2m,
mt2.name sm_type
from user u
inner join monthly m1
on month(u.join_date + interval 1 month) = substring(m1.run_day, 1, 1)
and m1.u_id = u.id
inner join monthly m2
on month(u.join_date + interval 2 month) = substring(m2.run_day, 1, 1)
and m2.u_id = u.id
inner join memb_types mt1
on m1.memb_type = mt1.id
inner join memb_types mt2
on m2.memb_type = mt2.id
demo fiddle
Note: if your table gets particularly large - it's not going to be much fun joining on the results of functions, in which case you may wish to consider making those 'virtual' columns, not so virtual, and then indexing them.
edit
As pointed out in the comments, it may make a slight difference if run_day is a numeric field, to use math rather than substring:
select u.id,
u.join_date,
month(u.join_date + interval 1 month) fm,
mt1.name fm_type,
month(u.join_date + interval 2 month) 2m,
mt2.name sm_type
from user u
inner join monthly m1
on month(u.join_date + interval 1 month) = floor(m1.run_day / 100)
and m1.u_id = u.id
inner join monthly m2
on month(u.join_date + interval 2 month) = floor(m2.run_day / 100)
and m2.u_id = u.id
inner join memb_types mt1
on m1.memb_type = mt1.id
inner join memb_types mt2
on m2.memb_type = mt2.id

Related

Query based on dates

I've got the following table/data (example)
Users
user_id | email
1 | asd#asd.com
2 | asd2#asd.com
3 | asd3#asd.com
4 | asd4#asd.com
5 | asd5#asd.com
Scheduled_Jobs
job_id | user_id | date
1 | 1 | 05/09/2019
2 | 1 | 05/10/2019
3 | 1 | 05/11/2019
4 | 1 | 05/12/2019
5 | 2 | 07/10/2019
6 | 2 | 07/11/2019
7 | 2 | 07/12/2019
8 | 3 | 11/07/2019
9 | 4 | 13/10/2019
10 | 4 | 13/11/2019
11 | 5 | 10/10/2019
12 | 5 | 10/11/2019
13 | 5 | 10/12/2019
Last_Update
update_id | job_id
1 | 1
2 | 2
3 | 3
4 | 5
5 | 9
6 | 11
When a user is created a list of scheduled jobs is created too. When a user completes a job the Last_Update table is getting updated.
I'm trying to show a list of users which got unfinished jobs based on date. For example 1-30 days delay: x users, 31-60 days delay: y users etc
Based on the example above here would be the expected result:
Number of users with no delayed jobs: 2 (users 1 & 4)
1-30 days delay: 2 (users 2 & 5)
31-60 days delay: 0
Over 60 days delay: 1 (user 3)
I'm currently only showing the number of users that got no delayed jobs
SELECT u.user_id
FROM users u
LEFT JOIN (
SELECT j.user_id AS completed
FROM jobs j
LEFT JOIN last_update lu
ON lu.job_id = j.job_id
WHERE j.job_date <= CURDATE()
AND lu.update_id IS NULL
) AS cj
ON u.user_id = cj.completed
WHERE cj.completed IS NULL
You can first join the three tables, aggregate by user_id and compute, for each user
how many unfinished jobs they have
how many unfinished jobs they have within the last 30 days
how many unfinished jobs they have within the last 31-60 days
Then, you can add another level of aggreation and count how many users meet each criteria.
Query:
select
sum(cnt_jobs_unfinished = 0) cnt_users_no_unfinished_jobs,
sum(cnt_jobs_unfinished_30d > 0) cnt_users_unfinished_30d,
sum(cnt_jobs_unfinished_31_60d > 0) cnt_users_unfinished_31_60d
from (
select
u.user_id,
sum(l.job_id is null) cnt_jobs_unfinished,
sum(
l.job_id is null
and j.date >= curdate() - interval 30 day
) cnt_jobs_unfinished_30d,
sum(
l.job_id is null
and j.date < curdate() - interval 30 day
and j.date >= curdate() - interval 60 day
) cnt_jobs_unfinished_31_60d
from users u
inner join scheduled_jobs j
on j.date <= curdate()
and j.user_id = u.user_id
left join last_update l
on l.job_id = j.job_id
group by u.user_id
) t
Demo on DB Fiddle
cnt_users_no_unfinished_jobs | cnt_users_unfinished_30d | cnt_users_unfinished_31_60d
---------------------------: | -----------------------: | --------------------------:
2 | 2 | 1
Note: I had to modify your sample data so job 8, for user 3, has a date within 30-60 days, as it was not the case in your original data).
You can run the subquery independantly to see what it returns:
user_id | cnt_jobs_unfinished | cnt_jobs_unfinished_30d | cnt_jobs_unfinished_31_60d
------: | ------------------: | ----------------------: | -------------------------:
1 | 0 | 0 | 0
2 | 1 | 1 | 0
3 | 1 | 0 | 1
4 | 0 | 0 | 0
5 | 1 | 1 | 0

In mysql how to select columns in a union join

How to select join table's columns when I'm using union. Specifically how to select b.id(booking id) in the following query? Also is it the right way to do it? Can you please tell me any other way to do it? I also want to select available time for a selected date if any available. I have three categories of timing.
cat1(category 1): availability on particular days of week(Example:Mon, Tue, Wed, etc of any month)
cat2(category 2): availability on First monday, second saturday, etc of any months.
cat3(category 3): availability on last saturday, last friday, etc of
any month.
I'm using day_offset, and cat columns in my doctor_schedule table to calculate date and checking it to #selected_date. I am also filtering off days with the help of my off_day table (off_days table stores information for any holidays, or if doctor taking leave for any personal reasons).
mysql> select * from doctor;
+----+-------------+
| id | name |
+----+-------------+
| 1 | John Doe |
| 2 | Larry Jones |
+----+-------------+
mysql> select * from doctor_schedule;
+----+-----------+-----+------------+----------+---------------+--------+------------+-----+
| id | doctor_id | day | start_time | end_time | booking_limit | active | day_offset | cat |
+----+-----------+-----+------------+----------+---------------+--------+------------+-----+
| 1 | 2 | 5 | 10:00:00 | 12:00:00 | 1 | 1 | 3 | 2 |
| 2 | 2 | 5 | 19:00:00 | 22:00:00 | 1 | 1 | 3 | 2 |
| 3 | 2 | 6 | 19:00:00 | 22:00:00 | 1 | 1 | 0 | 3 |
+----+-----------+-----+------------+----------+---------------+--------+------------+-----+
mysql> select * from booking;
+----+---------+-------------+--------------+------+
| id | user_id | schedule_id | booking_date | paid |
+----+---------+-------------+--------------+------+
| 1 | 1 | 3 | 2017-06-26 | 1 |
+----+---------+-------------+--------------+------+
mysql> select * from off_day;
+----+-----------+-------------+------------+
| id | doctor_id | schedule_id | date |
+----+-----------+-------------+------------+
| 2 | 1 | 3 | 2017-06-26 |
+----+-----------+-------------+------------+
set #selected_date := "2017-06-26";
set #doctor_id := 2;
SET #first_day = DATE_SUB(#selected_date, INTERVAL DAYOFMONTH(#selected_date) - 1 DAY);
select s.*
from
(select
s1.id
from doctor_schedule s1
where
s1.cat = 1
and
s1.day = weekday(#selected_date)
UNION
select
s2.id
from doctor_schedule s2
where
s2.cat = 2
and
DATE_ADD(#first_day, INTERVAL (s2.day - WEEKDAY(#first_day)) + (s2.other*7) DAY) = #selected_date
UNION
select
s3.id
from doctor_schedule s3
where
s3.cat = 3
and
date_sub(LAST_DAY(#selected_date), INTERVAL ((7 + WEEKDAY(LAST_DAY(#selected_date)) - s3.other) % 7) DAY) = #selected_date
) as s
right join
booking b
on
s.id = b.schedule_id
and
b.booking_date >= #selected_date
and
b.paid = 1
left join
off_day o
on
s.id = o.schedule_id
and
o.date = #selected_date
and
o.doctor_id = #doctor_id
where
o.schedule_id is null
group by
s.id
Mr. Tim Biegeleisen pointed that there is no booking_id in my query. Thanks to him that I found the solution. It should be b.id.
set #selected_date := "2017-06-26";
set #doctor_id := 2;
SET #first_day = DATE_SUB(#selected_date, INTERVAL
DAYOFMONTH(#selected_date) - 1 DAY);
select s.*, b.id
from
(select
s1.id
from doctor_schedule s1
where
s1.cat = 1
and
s1.day = weekday(#selected_date)
UNION
select
s2.id
from doctor_schedule s2
where
s2.cat = 2
and
DATE_ADD(#first_day, INTERVAL (s2.day - WEEKDAY(#first_day)) + (s2.other*7) DAY) = #selected_date
UNION
select
s3.id
from doctor_schedule s3
where
s3.cat = 3
and
date_sub(LAST_DAY(#selected_date), INTERVAL ((7 + WEEKDAY(LAST_DAY(#selected_date)) - s3.other) % 7) DAY) = #selected_date
) as s
right join
booking b
on
s.id = b.schedule_id
and
b.booking_date >= #selected_date
and
b.paid = 1
left join
off_day o
on
s.id = o.schedule_id
and
o.date = #selected_date
and
o.doctor_id = #doctor_id
where
o.schedule_id is null
group by
s.id

Mysql: Compare and filter time entries

I have an table with user ids and login dates.
id | customer | timestamp
1 | 1 | 2017-02-10 11:30:28
2 | 1 | 2017-02-11 11:30:28
3 | 2 | 2017-02-12 11:30:28
4 | 3 | 2017-02-13 11:30:28
5 | 1 | 2017-02-14 11:30:28
Now I want to get the count of the longest continuous streak of logins per customer.
I got to the point, where the difference is calculated correctly for one customer.
SELECT a.id aId,
b.id bId,
a.customer,
a.timestamp aTime,
b.timestamp bTime,
DATEDIFF(b.timestamp, a.timestamp) as diff
FROM logins a
INNER JOIN logins b
ON a.customer = b.customer AND a.id < b.id
WHERE b.customer = 7
GROUP BY a.id
How can I do this for the whole table and count the following logins with a difference under 1 day?
The wanted result for this example should be:
customer | days of continuous login
1 | 2
2 | 1
3 | 1
You can do this with a LEFT JOIN
Query
SELECT
logins.customer
, COUNT(*) as "longest continuous streak of logins"
FROM (
SELECT
login1.*
FROM
login login1
LEFT JOIN
login login2
ON
login1.timestamp < login2.timestamp
AND
# Only JOIN if date diff is less or equal then 1 day
DATEDIFF(login2.timestamp, login1.timestamp) <= 1
WHERE
login2.id IS NOT NULL
AND
login2.customer IS NOT NULL
AND
login2.timestamp IS NOT NULL
ORDER BY
login1.customer
)
AS logins
GROUP BY
logins.customer
Result
| customer | longest continuous streak of logins |
|----------|-------------------------------------|
| 1 | 2 |
| 2 | 1 |
| 3 | 1 |
see demo http://www.sqlfiddle.com/#!9/ad581/17

Detecting last activity on table

I have 2 tables, orders and orderNotes:
orders:
orderID | customerID | orderName
--------+------------+-----------
1 | 1 | Test
2 | 1 | 1234
3 | 1 | Test 2
4 | 2 | ABC Test
orderNotes:
noteID | orderID | postedDate | message
-------+---------+----------------------+----------
1 | 1 | 2014-08-01 08:00:06 | Testing
2 | 1 | 2014-08-04 13:15:45 | Hello
3 | 2 | 2014-07-31 11:11:50 | Hi, World
4 | 2 | 2014-08-01 12:16:32 | Test 123
...I think you get the idea
What I want to do is to detect if any order has not been updated in X hours.
I've come up with this:
SELECT orders.orderID, postedDate FROM orders
JOIN orderNotes ON orders.orderID = orderNotes.orderID
WHERE orders.customerID = 1
AND postedDate <= NOW() - INTERVAL 2 HOUR
but, this checks every row in orderNotes. How can I get it to check only the last row? I want to know if any orders (by customerID = 1) have not been updated in 2 hours.
Here's a demo of my current SQL: http://sqlfiddle.com/#!2/d5fc2e/1
Create a view with maximum dates
CREATE VIEW maxOrderNotes AS
SELECT
orderID,
MAX(`postedDate`) AS postedDate
FROM
orderNotes
GROUP BY
orderID;
Use the view in join
SELECT orders.orderID, postedDate FROM orders
JOIN maxOrderNotes ON orders.orderID = maxOrderNotes.orderID
WHERE orders.customerID = 1
AND postedDate <= NOW() - INTERVAL 2 HOUR;
+---------+---------------------+
| orderID | postedDate |
+---------+---------------------+
| 1 | 2014-08-04 10:15:45 |
| 2 | 2014-08-01 12:16:32 |
+---------+---------------------+
sqlfiddle
I found a solution that seems to work.
SELECT orders.orderID, MAX(postedDate) AS maxDate
FROM orders
JOIN orderNotes ON orders.orderID = orderNotes.orderID
WHERE orders.customerID = 1
GROUP BY orders.orderID
HAVING MAX(postedDate) <= NOW() - INTERVAL 2 HOUR
DEMO: http://sqlfiddle.com/#!2/d5fc2e/16

How to update foreign key with the most recent associated record's id in MySQL

What is the most performant way to generate the latest_entry_id on checks table from the entries with the same user_id, with the newest start_date that is prior to create_date of the check?
Before:
checks table
id | user_id | create_date | latest_entry_id
------------------------------------------------------
1 | 1 | 2012-01-01 | NULL
2 | 2 | 2012-01-01 | NULL
entries table
id | user_id | start_date
-------------------------------------
1 | 1 | 2012-02-01
2 | 1 | 2011-01-01
3 | 2 | 2011-09-01
4 | 2 | 2011-10-01
After:
checks table
id | user_id | create_date | latest_entry_id
------------------------------------------------------
1 | 1 | 2012-01-01 | 2
2 | 2 | 2012-01-01 | 4
I think this can make it work:
UPDATE checks c
INNER JOIN (
SELECT e.user_id,
MAX(e.id) AS ID
FROM entries e
INNER JOIN checks c1 ON e.user_id = c1.user_id
AND e.start_date < c1.create_date
GROUP BY e.user_id
) a ON a.user_id = c.user_id
SET c.latest_entry_id = a.id;
sqlfiddle demo
p.s. Your second row in your expected results is not consistent with your requirements. latest_entry_id should be 4, not 3.
The best query I came up with is this
Update checks INNER JOIN
(
SELECT checks.id AS c_id, MAX(entries.start_date) AS max_start_date
FROM checks LEFT OUTER JOIN entries ON checks.user_id = entries.user_id
WHERE entries.start_date < checks.create_date
GROUP BY checks.id
) AS tmp
ON checks.id = tmp.c_id
LEFT JOIN entries
ON tmp.max_start_date = entries.start_date AND checks.user_id = entries.user_id
SET checks.latest_entry_id = entries.id
This query is performant and avoid running one subquery per check. If you know of another performant way to do this update to get the same results, I would like to hear your way.