Joining Max Row where Col - mysql

I have this query:
SELECT
`shift`.`uid`,
`shift`.`activity`,
`users`.`fname`,
`users`.`lname`
FROM `shift`, `users`
WHERE `shift`.`uid` = `users`.`id`
It works fine just like that, but I need to add a new column from another table and order by it.
times :
| uid | User | time |
+++++++++++++++++++++
| 3 | bob | 1231 |
| 3 | bob | 1291 |
| 4 | ned | 1651 |
| 5 | ted | 5679 |
| 6 | joe | 7665 |
| 6 | joe | 7864 |
How can I include the maximum time from the time table for each user (WHERE times.uid = shift.uid) and then order by that column?
Trouble is, all the other tables have one row per user but the time table has multiple and I can't figure out the correct combination of joins and group by.

You could join on an aggregate query:
SELECT `shift`.`uid`,
`shift`.`activity`,
`users`.`fname`,
`users`.`lname` ,
t.max_time
FROM `shift`
JOIN `users` ON `shift`.`uid` = `users`.`id`
JOIN (SELECT `uid`, MAX(`time`) AS max_time
FROM `times`
GROUP BY `uid`) t ON shift.uid = t.uid
ORDER BY t.max_time

A pretty simple way to approach this is using a correlated subquery:
SELECT s.`uid`, s.`activity`, u.`fname`, u.`lname`,
(SELECT MAX(tt.time)
FROM timetable tt
WHERE tt.uid = u.id
) as maxtime
FROM `shift` s JOIN
`users` u
ON s.`uid` = u.`id`;
The advantage of this approach is performance. With an index on timetable(uid, time), this should work better than doing an aggregation at the outer level (because the query will take advantage of the index).

SELECT s.uid,
s.activity,
u.fname,
u.lname,
MAX(t.time) as maxtime
FROM shift s,
INNER JOIN users u ON u.id = s.uid
INNER JOIN times t ON t.uid = u.id
GROUP BY s.uid,
s.activity,
u.fname,
u.lname
ORDER BY maxtime

Related

MySQL Sort users by highest total count from counts on multiple tables

Similar to stackoverflow, I have a database of users who vote, comment, and make other actions. I am trying to return a sorted result of the top 10 users who have made the most actions based on the combined count of all of the actions a user has made, along with the actual count of total actions said user made.
Below is my table structure.
Users Table
Typical users data such as an incrementing id, username, email, etc.
| id | username |
-----------------
| 1 | bob |
| 2 | jane |
Votes Table
Has an incrementing id, user_id fk and type of vote made.
| id | user_id | type |
| 1 | 1 | up_vote |
| 2 | 2 | up_vote |
Comments Table
Same as the votes table, typical stuff here.
| id | user_id | comment |
---------------------------------
| 1 | 1 | hello, world |
| 1 | 1 | goodbye, world |
Intended results:
results needed
| total_actions | user_id | username |
-------------------------------------|
| 3 | 1 | bob |
| 1 | 2 | jane |
What I actually know how to do, albeit probably not the most efficient way...
Users sorted by most votes, along with the count
select `users`.*,
(
select count(*)
from `votes`
where `users`.`id` = `votes`.`user_id`
) as `votes_count`
from `users`
order by `votes_count` desc
limit 10
Users sorted by most comments, along with the count
select `users`.*,
(
select count(*)
from `comments`
where `users`.`id` = `comments`.`user_id`
) as `comments_count`
from `users`
order by `comments_count` desc
limit 10
Any help would be greatly appreciated!
You can left join aggregate queries that compute the total votes and comments per user, and sort in the outer query, like so:
select
coalesce(v.cnt, 0) + coalesce(c.cnt, 0) total_actions,
u.id,
u.username
from users u
left join (select user_id, count(*) cnt from votes group by user_id) v
on v.user_id = u.id
left join (select user_id, count(*) cnt from comments group by user_id) c
on c.user_id = u.id
order by total_actions desc
limit 10
While I prefer GMB's method (using LEFT JOIN with each subquery) I'll show here how to combine your existing queries. Just use both correlated subqueries, and add them together to get the total.
select `users`.*,
(
select count(*)
from `votes`
where `users`.`id` = `votes`.`user_id`
) +
(
select count(*)
from `comments`
where `users`.`id` = `comments`.`user_id`
) as total_actions
from `users`
order by total_actions desc
limit 10

Left join sum, group trouble

I have this query
SELECT
`from_id` as user_id,
MAX(`createdon`) as updated_at,
SUM(`unread`) as new,
u.username,
p.sessionid,
s.access
FROM (
SELECT `from_id`, `createdon`, `unread`
FROM `modx_messenger_messages`
WHERE `to_id` = {$id}
UNION
SELECT `to_id`, `createdon`, 0
FROM `modx_messenger_messages`
WHERE `from_id` = {$id}
ORDER BY `createdon` DESC
) as m
LEFT JOIN `modx_users` as u ON (u.id = m.from_id)
LEFT JOIN `modx_user_attributes` as p ON (p.internalKey = m.from_id)
LEFT JOIN `modx_session` as s ON (s.id = p.internalKey)
GROUP BY `from_id`
ORDER BY `new` DESC, `createdon` DESC;
table
id | message | createdon | from_id | to_id | unread
1 | test | NULL | 5 | 6 | 0
2 | test2 | NULL | 6 | 5 | 1
3 | test3 | NULL | 6 | 5 | 1
result new = 28. Why?
If remove joins new = 2, correctly.
Though it depends on the actual database, pure SQL says that a statement using GROUP BY requires all non-aggregated columns to be in the GROUP BY. Without including all columns, weird stuff can happen, which might explain why you get different results. If you know that the other columns are going to be the same within the user_id, you could do MAX(u.username) or something similar (again, depending on your database server). So I'd try and clean up the SQL statement first.

UPDATE query SUM multiple selects

I need to update a field calculated by the sum of multiple selects. The selection part is working, but I can't find a way to update the user table
user
+------+---------+
| id | total |
+------+---------+
| 1 | |
| 2 | |
unita
+------+-------+-----+
| id | uid | num |
+------+-------+-----+
| 1 | 1 | 25 |
| 1 | 2 | 10 |
unitb
+------+-------+-----+
| id | uid | num |
+------+-------+-----+
| 9 | 1 | 225 |
| 9 | 2 | 10 |
class
+------+--------+------+
| id | name | cost |
+------+--------+------+
| 1 | class1 | 100 |
| 9 | class9 | 500 |
SELECT uid, SUM(score) FROM (
SELECT unita.uid, SUM(class.cost * unita.num) AS cost FROM unita, class WHERE unita.id = class.id GROUP BY unita.uid
UNION
SELECT unita.uid, SUM(class.cost * unitb.num) AS cost FROM unitb, class WHERE unitb.id = class.id GROUP BY unitb.uid
) x GROUP BY uid
The update command should sum all cost per user
User 1: (25*100)+(225*500) = 115000
User 2: (10*100)+(10*500) = 6000
It this possible within 1 SQL command. The unit tables are locked, so I can't modify anything
You can use join to bring in the results from your subquery:
UPDATE user u JOIN
(SELECT uid, SUM(score) as total
FROM (SELECT unita.uid, SUM(class.cost * unita.num) AS cost
FROM unita JOIN
class
ON unita.id = class.id
GROUP BY unita.uid
UNION ALL
SELECT unita.uid, SUM(class.cost * unitb.num) AS cost
FROM unitb JOIN
class
ON unitb.id = class.id
GROUP BY unitb.uid
) x
GROUP BY uid
) newvals
ON u.id = newvals.uid
SET u.total = newvals.total;
Three notes:
Note the use of UNION ALL instead of UNION. Not only does this improve performance because duplicates are not eliminated, but it also fixes a potential problem if both subqueries return the same value.
Note the use of proper join syntax. Simple rule: never use commas in the from clause.
This will not set the total to 0 if there is no match. If you desire this, change the join to a left join and the set to SET u.total = COALESCE(newvals.total, 0).
You can use the update-join syntax:
UPDATE `user` u
JOIN (SELECT uid, SUM(score) AS total
FROM (SELECT unita.uid, SUM(class.cost * unita.num) AS cost
FROM unita, class
WHERE unita.id = class.id
GROUP BY unita.uid
UNION ALL
SELECT unitb.uid, SUM(class.cost * unitb.num) AS cost
FROM unitb, class
WHERE unitb.id = class.id
GROUP BY unitb.uid) x
GROUP BY uid) s ON s.uid = u.id
SET u.total = s.total
Notes:
The inner query in the OP has a bug. Since it uses union instead of union all, if the same uid has the same total score in both units, it will only be counted once, instead of twice. The above query fixes this.
Implicit joins have been deprecated for ages. The above query still uses them to math the OP's style, but the use of explicit joins is highly recommended.
E.g.:
UPDATE `user` u
JOIN (SELECT uid, SUM(score) AS total
FROM (SELECT unita.uid, SUM(class.cost * unita.num) AS cost
FROM unita
JOIN class ON unita.id = class.id
GROUP BY unita.uid
UNION ALL
SELECT unitb.uid, SUM(class.cost * unitb.num) AS cost
FROM unitb
JOIN class ON unitb.id = class.id
GROUP BY unitb.uid) x
GROUP BY uid) s ON s.uid = u.id
SET u.total = s.total

Oh MySQL, how doth thou join related records by date

I have what can only be described as a seemingly simple problem with most likely a simple solution; yet that simple solution escapes me. I've searched and trudged through the vast web of StackOverflow only to come up short finding only solutions that seem to be extremely complex. I've already kind of solved this problem, but, it's disgusting and I'm ashamed of it. Surely there is a better way, for there must be a Knight in shining MySQL armor wielding a query sword who can come up with a simple and elegant solution. Here goes:
For the sake of this question, we'll keep the two tables simple.
Table 1
users (user_id, active, name)
and
Table 2
user_projects (user_project_id, user_id, start_date, details)
In this case, records for users are added as needed. Related records are then added as projects are completed by users.
My question is: Using a query, how can I get 1 record containing all of the information in table 1 active users joined with the record from table 2 with the most recent date based on start_date?
In other words, if I have this:
| user_id | active | name |
| 1 | 1 | brian |
and this:
| user_project_id | user_id | start_date | details |
| 1 | 1 | 2013-10-02 | proj 1 |
| 2 | 1 | 2013-11-26 | proj 2 |
| 3 | 1 | 2014-01-02 | proj 3 |
produce the query that gives me this:
| user_id | active | name | user_project_id | user_id | start_date | details |
| 1 | 1 | brian | 3 | 1 | 2014-01-02 | proj 3 |
Oh please oh please let there be an answer for I will surely wither without one.
Since in MySQL there is not such things as top selectors, you can use triple JOIN, like:
SELECT
user_projects.*,
users.*
FROM
(SELECT
MAX(start_date) AS max_date,
user_id
FROM
user_projects
GROUP BY
user_id) AS max_dates
LEFT JOIN
user_projects
ON max_dates.max_date=user_projects.start_date
AND max_dates.user_id=user_projects.user_id
LEFT JOIN
users
ON users_projects.user_id=users.user_id
First select the projects per user that are the most recent:
SELECT a.*
FROM user_projects a
JOIN (
SELECT user_id, MAX(start_date) AS max_start_date
FROM user_projects
GROUP BY user_id
) b ON a.user_id = b.user_id AND a.start_date = b.max_start_date
It creates a small helper table from user_projects comprising the user and for each row their most recent project date; to get all the corresponding table fields you must join that with user_projects again.
Then, you simply join users with the above outcome to get the final result:
SELECT *
FROM users
JOIN (
SELECT a.*
FROM user_projects a
JOIN (
SELECT user_id, MAX(start_date) AS max_start_date
FROM user_projects
GROUP BY user_id
) b ON a.user_id = b.user_id AND a.start_date = b.max_start_date
) c ON users.user_id = c.user_id
select u1.user_id, u1.name, u.strat_date, u.user_project_id, u.details
from user_projects u
left outer join users u1 on u1.user_id=u.user_id
left outer join
(select user_id, max(strat_date) as strat_date user_projects group by user_id) as A
on A.user_id=u.user_id and A.strat_date=u.strat_date
You can use top 1 and order by date
select top 1 a.user_id,a.name,b.user_project_id,b.user_id,b.start_date,b.details
from users a join users_project b on a.user_id = b.user_id
order by b.start_date desc
Sorry i didnt notice mysql tag in OP's question. You can use Limit keyword.
select a.user_id,a.name,b.user_project_id,b.user_id,b.start_date,b.details
from users a join users_project b on a.user_id = b.user_id
order by b.start_date desc Limit 1

Making large SQL query efficicent

I'm stuck on a rather complex query.
I'm looking to write a query that shows the "top five customers" as well as some key metrics (counts with conditions) about each of those customers. Each of the different metrics uses a totally different join structure.
+-----------+------------+ +-----------+------------+ +-----------+------------+
| customer | | | metricn | | | metricn_lineitem |
+-----------+------------+ +-----------+------------+ +-----------+------------+
| id | Name | | id | customer_id| |id |metricn_id |
| 1 | Customer1 | | 1 | 1 | | 1 | 1 |
| 2 | Customer2 | | 2 | 2 | | 2 | 1 |
+-----------+------------+ +-----------+------------+ +-----------+------------+
The issue this is that I always want to group by this customer table.
I first tried to put all of my joins into the original query, but the query was abysmal with performance. I then tried using subqueries, but I couldn't get them to group by the original hospital id.
Here's a sample query
SELECT
customer.name,
(SELECT COUNT(metric1_lineitem.id)
FROM metric1 INNER JOIN metric1_lineitem
ON metric1_lineitem.metric1_id = metric1.id
WHERE metric1.customer_id = customer_id
) as metric_1,
(SELECT COUNT(metric2_lineitem.id)
FROM metric2 INNER JOIN metric2_lineitem
ON metric2_lineitem.metric2_id = metric2.id
WHERE metric2.customer_id = customer_id
) as metric_2
FROM customer
GROUP BY customer.name
SORT BY COUNT(metric1.id) DESC
LIMIT 5
Any advice? Thanks!
SELECT name, metric_1, metric_2
FROM customer AS c
LEFT JOIN (SELECT customer_id, COUNT(*) AS metric_1
FROM metric1 AS m
INNER JOIN metric1_lineitem AS l ON m.id = l.metric1_id
GROUP BY customer_id) m1
ON m1.customer_id = c.customer_id
LEFT JOIN (SELECT customer_id, COUNT(*) AS metric_2
FROM metric2 AS m
INNER JOIN metric2_lineitem AS l ON m.id = l.metric2_id
GROUP BY customer_id) m1
ON m2.customer_id = c.customer_id
ORDER BY metric_1 DESC
LIMIT 5
You should also avoid using COUNT(columnname) when you can use COUNT(*) instead. The former has to test every value to see if it's null.
Although your data structure may be lousy, your query may not be so bad, with two exceptions. I don't think you need the aggregation on the outer level. Also, the "correlation"s in the where clause (such as metric1.customer_id = customer_id) are not doing anything, because customer_id is coming from the local tables. You need metric1.customer_id = c.customer_id:
SELECT c.name,
(SELECT COUNT(metric1_lineitem.id)
FROM metric1 INNER JOIN
metric1_lineitem
ON metric1_lineitem.metric1_id = metric1.id
WHERE metric1.customer_id = c.customer_id
) as metric_1,
(SELECT COUNT(metric2_lineitem.id)
FROM metric2 INNER JOIN
metric2_lineitem
ON metric2_lineitem.metric2_id = metric2.id
WHERE metric2.customer_id = c.customer_id
) as metric_2
FROM customer c
ORDER BY 1 DESC
LIMIT 5;
How can you make this run faster? One way is to introduce indexes. I would recommend metric1(customer_id), metric2(customer_id), metric1_lineitem(metric1_id) and metric2_lineitem(metric2_id).
This may be faster than the aggregation method (proposed by Barmar) because MySQL is inefficient with aggregations. This should allow the aggregations to take place only using indexes instead of the base tables.