Combined ORDER BY of two SELECT queries using UNION - mysql

How can I get combined order by results retrieved from these two queries joined by a UNION?
SELECT u.id, u.name, u.gender, n.user, n.other_user, n.type, n.notification, n.membership, n.link, n.created_at, p.photo FROM notifications n
INNER JOIN users u ON
CASE
WHEN n.user = :me THEN u.id = n.other_user
WHEN n.other_user = :me THEN u.id = n.user
END
LEFT JOIN photos p ON
CASE
WHEN n.user = :me THEN p.user = n.other_user AND p.order_index = (SELECT MIN(order_index) FROM photos WHERE user = n.other_user)
WHEN n.other_user = :me THEN p.user = n.user AND p.order_index = (SELECT MIN(order_index) FROM photos WHERE user = n.user)
END
UNION
SELECT '', '', '', '', '', '', n.notification, n.membership, n.link, n.created_at, '' FROM notifications n WHERE type = 'admin'
I want the returned records to be sorted in descending order as per their ids. For example, if the records returned from first query are 3,5,4,6,7 and from second query are 2,1,9 then all the records should be combined sorted like this 9,7,6,5,4,3,2,1.
I have tried this:
SELECT * FROM
(
*THE WHOLE QUERY ABOVE*
) AS x
ORDER BY x.id
This is not returning correct results. It is sorting the results from first query in descending order 7,6,5,4,3 and results from the 2nd query in ascending order 1,2,9. They are getting sorted individually instead of getting sorted together. How can I get them sorted combined together for 9,7,6,5,4,3,2,1.

Add notification id in both the queries and give them aliases as you haven't used aliases in your tables (I guess). Then just order by using the alias of the notification id as answered by "Thorsten Kettner".
SELECT u.id as uid, n.id as nid, u.name, u.gender, n.user, n.other_user, n.type, n.notification, n.membership, n.link, n.created_at, p.photo FROM notifications n
INNER JOIN users u ON
CASE
WHEN n.user = :me THEN u.id = n.other_user
WHEN n.other_user = :me THEN u.id = n.user
END
LEFT JOIN photos p ON
CASE
WHEN n.user = :me THEN p.user = n.other_user AND p.order_index = (SELECT MIN(order_index) FROM photos WHERE user = n.other_user)
WHEN n.other_user = :me THEN p.user = n.user AND p.order_index = (SELECT MIN(order_index) FROM photos WHERE user = n.user)
END
UNION
SELECT '', n.id as nid, '', '', '', '', '', n.notification, n.membership, n.link, n.created_at, '' FROM notifications n WHERE type = 'admin'
ORDER BY nid DESC

You have already found the issue yourself; you confused user ID and notification ID. So select the two, use alias names that tell which is which and sort:
select u.id as user_id, ..., n.id as notification_id, ...
from ...
union all
select ... from ...
order by notification_id;

Related

Consolidate 2 queries with similar where clauses but different aggregate function targets

I have 2 queries that work fine separately. Given they are similar, I'd like to consolidate them into one performant query. Seems straightforward as the where clauses are similar. But the sum, count, and min functions all apply to different rows and get in the way.
Context:
Users can score (or rate) a location and get points
User A can refer User B and get referral points when User B first submits a score
Points expire after a certain date
Goal is to build a leaderboard of users and their total points for scoring and referring for a particular location (area/country)
Positional parameters are filled in with hard values for 'Massachusetts', 'United States', and the scoreDateTime expiration date and are unfortunately duplicated in both select subqueries.
Question:
How can the query below be reorganized to combine constraints? There must be a way to start with a list of scores from a specific location after a certain date. The only complication is to get User B's first score date and only offer referral points to User A if it is after the expiration date.
select scoring.userId, scoring.points + referring.points as leaderPoints
from (
select userId, sum(ratingPoints) as points
from scores s, locations l
where s.locationId = l.locationId and
l.locationArea = 'Massachusetts' and
l.locationCountry = 'United States' and
s.scoreDateTime > '2016-04-16 18:50:53.154' and
s.userId != 0
group by s.userId
) as scoring
join (
select u1.userId, count(*) * 20 as points
from users u0
join users u1 on u0.userId = u1.userId
join users u2 on u2.referredByEmail = u1.emailAddress
join scores s on u2.userId = s.userId
join locations l on s.locationId = l.locationId
where l.locationArea = 'Massachusetts' and
l.locationCountry = 'United States' and
scoreDateTime = (
select min(scoreDateTime)
from scores
where userId = u2.userId
) and
scoreDateTime >= '2016-04-16 18:50:53.154'
group by u1.userId
) as referring on scoring.userId = referring.userId
order by leaderPoints desc
limit 10;
This is untested code, but it should do the trick. The Cross Apply is for readability...it'll hurt performance, but this doesn't seem to be a particularly process-intensive query, so I would keep it.
Please give it a try and let me know if you have any questions.
SELECT U.UserID,
ISNULL(SUM(CASE WHEN S.UserID IS NULL THEN 0 ELSE S.ratingPoints END), 0) AS [Rating Points],
ISNULL(SUM(CASE WHEN SS.userID IS NULL THEN 0 ELSE 20 END), 0) AS [Referral Points]
FROM Users U
LEFT OUTER JOIN scores S
ON S.userID = U.userID
AND S.scoreDateTime >= '2016-04-16 18:50:53.154'
LEFT OUTER JOIN locations L
ON S.locationID = L.locationID
AND L.locationArea = 'Massachusetts'
AND L.LocationCountry = 'United States'
LEFT OUTER JOIN Users U2
ON U2.referredByEmail = U.emailAddress
LEFT OUTER JOIN scores SS
ON SS.userID = U2.userID
LEFT OUTER JOIN locations LL
ON SS.locationID = LL.locationID
AND LL.locationArea = 'Massachusetts'
AND LL.locationCountry = 'United States'
AND SS.scoreDateTime >= '2016-04-16 18:50:53.154'
AND SS.scoreDateTime =
(
SELECT MIN(scoreDateTime)
FROM scores
where userID = U2.userID
)
GROUP BY U.userID
EDIT:
Modified answer to remove Cross Apply
Thanks Stan Shaw but I was unable to get your query to work on MySQL to test the results. However, I did notice a special case that was not covered by my original query. A user can get refer points from areas in which they themselves have not submitted scores. As long as the new user scores in that area, they get refer points there.
Here is the final query I'm using. I was not able to consolidate the duplicate where clauses in a way that appeared performant.
select userId, sum(points) as leaderPoints
from (
select s.userId, sum(s.ratingPoints) as points
from scores s, locations l
where s.locationId = l.locationId and
l.locationArea = 'Georgia' and
l.locationCountry = 'United States' and
s.scoreDateTime >= '2016-04-05 03:00:00.000' and
s.userId != 1
group by userId
union
select u1.userId, 20 as points
from users u0, users u1, users u2, scores s, locations l
where u0.userId = u1.userId and
u2.referredByEmail = u1.emailAddress and
u2.userId = s.userId and
s.locationId = l.locationId and
l.locationArea = 'Georgia' and
l.locationCountry = 'United States' and
scoreDateTime >= '2016-04-05 03:00:00.000' and
scoreDateTime = (
select min(scoreDateTime)
from scores
where userId = u2.userId
)
) as pointsEarned
group by userId
order by leaderPoints desc
limit 10
order by leaderPoints desc
limit 100;

MySQL nested ANDs and several conditions

I have two table for a multiple choice questionnaire (each user answers a series of questions):
users (userID, name, email)
votes (voteID, userID, questionID, answerID)
Sample data (users):
0, Some Name, some#thing.com
1, Other Name, some#one.com
Sample data (votes):
0, 1, 1, 1
1, 1, 2, 2
2, 1, 3, 2
I would like select all users who has the correct answers.
I tried this (where I've hardcoded the answers in):
$sql = "SELECT users.userID, users.name, users.email FROM users
INNER JOIN votes ON (users.userID = votes.userID)
WHERE (votes.questionID = '1' AND votes.answerID = '1')
AND (votes.questionID = '2' AND votes.answerID = '2')
AND (votes.questionID = '3' AND votes.answerID = '2')
AND (votes.questionID = '4' AND votes.answerID = '3')
AND (votes.questionID = '5' AND votes.answerID = '1')
GROUP BY users.userID";
But this doesn't return anything.
I've also tried something like this (where I've also hardcoded the answers in):
$sql = "SELECT users.userID, users.name, users.email FROM users
INNER JOIN transfertipsvotes ON (users.userID = transfertipsvotes.userID)
WHERE (transfertipsvotes.questionID = '1' AND transfertipsvotes.answerID = '1') GROUP BY users.userID
UNION
SELECT users.userID, users.name, users.email FROM users
INNER JOIN transfertipsvotes ON (users.userID = transfertipsvotes.userID)
WHERE (transfertipsvotes.questionID = '2' AND transfertipsvotes.answerID = '2') GROUP BY users.userID
UNION
SELECT users.userID, users.name, users.email FROM users
INNER JOIN transfertipsvotes ON (users.userID = transfertipsvotes.userID)
WHERE (transfertipsvotes.questionID = '3' AND transfertipsvotes.answerID = '2') GROUP BY users.userID";
But this just returns all users with one correct answer.
How do I make the correct query to select all users with the correct answers?
As far as I can see it now you need to use an INNER JOIN for each question, so each inner join will look like this:
INNER JOIN votes AS q1 ON (users.userID = q1.userID) AND q1.questionID = '1' AND q1.answerID ='1'
Repeat this for each question and you can check it.
If i understood you correctly, you want users who have answered all questions correct. In that case you should use INTERSECT instead of UNION
But for this you are hitting the table as many times as your questions. Its better to use OR clause in where "((question1 and Answer1) or (question2 and Answer2))". and at last do a group based on userid and get count of correct answer and fetch only those members whose has all correct answer.

Count number of occurrences based on user id

I'm very new to SQL/MySQL and Stackoverflow for that matter, and I'm trying to create a query through iReport (though I don't have to use iReport) for SugarCRM CE. What I need is to create a report that displays the number of "Referrals", "Voicemails", "Emails", and "Call_ins" that are linked to a specific "user" (employee). The query I currently have set up works; however it is running through the data multiple times generating a report that is 200+ pages. This is the code that I am currently using:
SELECT
( SELECT COUNT(*) FROM `leads` INNER JOIN `leads_cstm` ON `leads`.`id` = `leads_cstm`.`id_c` WHERE (leadtype_c = 'Referral' AND users.`id` = leads.`assigned_user_id`) ),
( SELECT COUNT(*) FROM `leads` INNER JOIN `leads_cstm` ON `leads`.`id` = `leads_cstm`.`id_c` WHERE (leadtype_c = 'VM' AND users.`id` = leads.`assigned_user_id`) ),
( SELECT COUNT(*) FROM `leads` INNER JOIN `leads_cstm` ON `leads`.`id` = `leads_cstm`.`id_c` WHERE (leadtype_c = 'Email' AND users.`id` = leads.`assigned_user_id`) ),
users.`first_name`,users.`last_name`
FROM
`users` users,
`leads` leads
I would appreciate any guidance!
You want to use conditional summation. The following uses MySQL syntax:
SELECT sum(leadtype_c = 'Referral') as Referrals,
sum(leadtype_c = 'VM') as VMs,
sum(leadtype_c = 'Email') as Emails,
users.`first_name`, users.`last_name`
FROM users join
`leads`
on users.`id` = leads.`assigned_user_id` INNER JOIN
`leads_cstm`
ON `leads`.`id` = `leads_cstm`.`id_c`
group by users.id;
You can use COUNT with CASE for this:
SELECT u.first_name,
u.last_name,
count(case when leadtype_c = 'Referral' then 1 end),
count(case when leadtype_c = 'VM' then 1 end),
count(case when leadtype_c = 'Email' then 1 end)
FROM users u
JOIN leads l ON u.id = l.assigned_user_id
JOIN leads_cstm lc ON l.id = lc.id_c
GROUP BY u.id
To match your exact results, you should probably use an OUTER JOIN instead, but this gives you the idea.
A Visual Explanation of SQL Joins

Getting usernames in forum topic information from linked tables

For a forum i want to fetch a forumTopic with additional, linked information like the lastPostDate, lastPostUserName and starterUserName.
The problem arises with the lastPostUserName and starterUserName. When a forumTopic has only one linked post it seems to work correctly and both the lastPostUserName as the starterUserName are filled. When there are multiple posts linked to a topic only the starterUserName is filled and the lastPostUserName is NULL
The structure of the database is a formCategory has a number of formTopic the forumTopic has a number of forumPost and a forumPost is linked to a user.
SELECT forumTopic.*,
COUNT( forumPost.id ) AS postCount,
MAX(forumPost.date) AS lastPostDate,
(SELECT name FROM user AS u1 WHERE u1.id = forumPost.posterUserId AND forumPost.date = MAX(forumPost.date) )
AS lastPostUserName,
(SELECT name FROM user AS u2 WHERE u2.id = forumPost.posterUserId AND forumPost.date = MIN(forumPost.date) )
AS starterUserName
FROM forumCategory
LEFT JOIN forumTopic ON forumCategory.id = forumTopic.forumCategoryId
LEFT JOIN forumPost ON forumPost.forumTopicId = forumTopic.id
WHERE forumCategory.rewrittenName='someforumcategory'
AND forumCategory.active='Y'
AND forumTopic.active='Y'
AND forumPost.active='Y'
GROUP BY forumTopic.id
ORDER BY forumPost.date ASC
Try this
SELECT forumTopic.*,
innerv.*,
(SELECT name FROM user AS u1 WHERE u1.id = innerv.first_user)
AS startedUserName,
(SELECT name FROM user AS u2 WHERE u2.id = innerv.last_user )
AS lastUserName
FROM forumTopic
LEFT JOIN forumCategory ON forumCategory.id = forumTopic.forumCategoryId
LEFT JOIN (
SELECT forumTopicId, MAX(date) AS LAST_POSTED_dATE, MIN(date) as FIRST_POSTED_DATE,
SUBSTRING_INDEX(
GROUP_CONCAT(posterUserId ORDER BY date),
',',
1
) as first_user,
SUBSTRING_INDEX(
GROUP_CONCAT(posterUserId ORDER BY date),
',',
-1
) as last_user, count(1) as posts_under_topic
FROM forumPost where forumPost.active='Y'
GROUP BY forumTopicId ) innerv ON innerv.forumTopicId = forumTopic.id
WHERE forumCategory.rewrittenName='someforumcategory'
AND forumCategory.active='Y'
AND forumTopic.active='Y'
The subquery (innerv) filter active records and groups the records in the forumPost by topicId.

Set limit for MySQL query

I have query like this:
SELECT `all_messages`.`user_1`, `messages`.*, `users`.`username`
FROM `all_messages`
JOIN `messages` ON (`all_messages`.`user_2` = `messages`.`from_user`)
JOIN `users` ON (`all_messages`.`user_2` = `users`.`id`)
WHERE `all_messages`.`user_1` = '12'
ORDER BY `messages`.`sent` DESC LIMIT 2
Now this query does what I need but my problem is with this line
ON (`all_messages`.`user_2` = `messages`.`from_user`)
It selects all data from messages where the matches was found but I need only one newest record. I hope you guys get what I mean.
If you need one "newest record" you should have a date column or something, lets name it "CREATION_TIME", so you could do something like this
SELECT AM.user_1, M.*, U.username
FROM all_messages AM, messages M , users U
WHERE AM.user_1 = '12'
AND AM.user_2 = M.from_user
AND AM.user_2 = U.id
AND M.CREATION_TIME =
(
SELECT MAX(CREATION_TIME)
FROM messages
WHERE from_user= M.from_user
)
ORDER BY M.sent DESC LIMIT 2
Edit
SELECT AM.user_1, M.*, U.username
FROM all_messages AM, messages M, users U
WHERE AM.user_1 = '12'
AND AM.user_2 = M.from_user
AND AM.user_2 = U.id
AND M.sent =
(
SELECT MAX(sent)
FROM messages
WHERE from_user= M.from_user
)
ORDER BY M.sent DESC LIMIT 2
It should work