I have the following table structure with data
TABLE: USER
USER ID | USER NAME
1 | Joe
2 | Mary
TABLE : USER GROUP
USER ID | GROUP ID
1 | 1
1 | 2
TABLE : GROUP
GROUP ID | GROUP NAME
1 | Company 1
2 | Company 2
TABLE : ROLE
ROLE ID | ROLE NAME
1 | Administrator
2 | Users
TABLE : USER ROLE
USER ID | ROLE ID
1 | 1
2 | 1
As you can see user #2 does not belong to any group. Roles & Groups are optional forcing me to left joint but when I run a query as below
`SELECT a.user_id,
a.user_name
GROUP_CONCAT(r.role_name) AS role_names,
GROUP_CONCAT(g.group_name) AS group_names
FROM user a
LEFT JOIN role_map m ON a.user_id = m.user_id
INNER JOIN role r ON m.role_id = r.role_id
LEFT JOIN user_group s ON a.user_id = s.user_id
INNER JOIN group g ON s.group_id = g.group_id
GROUP BY a.user_id`
I get a cartesian product in the role_names column - the result looks like this
Joe | Administrators, Administrators | Company 1, Company 2
What am I doing wrong?
The easiest way to solve this is by using DISTINCT in your GROUP_CONCAT (SQL Fiddle). Also, you will need to add GROUP BY a.user_id in order to group per user:
SELECT a.user_id,
a.user_name,
GROUP_CONCAT(DISTINCT r.role_name) AS role_names,
GROUP_CONCAT(DISTINCT g.group_name) AS group_names
FROM `user` a
LEFT JOIN `user_role` m ON a.user_id = m.user_id
LEFT JOIN `role` r ON m.role_id = r.role_id
LEFT JOIN `user_group` s ON a.user_id = s.user_id
LEFT JOIN `group` g ON s.group_id = g.group_id
GROUP BY a.user_id;
Related
Running into a seemingly simple JOIN problems here..
I have two tables, users and courses
| users.id | users.name |
| 1 | Joe |
| 2 | Mary |
| 3 | Mark |
| courses.id | courses.name |
| 1 | History |
| 2 | Math |
| 3 | Science |
| 4 | English |
and another table that joins the two:
| users_id | courses_id |
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
I'm trying to find distinct user names who are in course 1 and course 2
It's possible a user is in other courses, too, but I only care that they're in 1 and 2 at a minimum
SELECT DISTINCT(users.name)
FROM users_courses
LEFT JOIN users ON users_courses.users_id = users.id
LEFT JOIN courses ON users_courses.courses_id = courses.id
WHERE courses.name = "History" AND courses.name = "Math"
AND courses.name NOT IN ("English")
I understand why this is returning an empty set (since no single joined row has History and Math - it only has one value per row.
How can I structure the query so that it returns "Joe" because he is in both courses?
Update - I'm hoping to avoid hard-coding the expected total count of courses for a given user, since they might be in other courses my search does not care about.
Join users to a query that returns the user ids that are in both courses:
select u.name
from users u
inner join (
select users_id
from users_courses
where courses_id in (1, 2)
group by users_id
having count(distinct courses_id) = 2
) c on c.users_id = u.id
You can omit distinct from the condition:
count(distinct courses_id) = 2
if there are no duplicates in users_courses.
See the demo.
If you want to search by course names and not ids:
select u.name
from users u
inner join (
select uc.users_id
from users_courses uc inner join courses c
on c.id = uc.courses_id
where c.name in ('History', 'Math')
group by uc.users_id
having count(distinct c.id) = 2
) c on c.users_id = u.id
See the demo.
Results:
| name |
| ---- |
| Joe |
You can use in operator and use select to generate list of potential users_id attending the second course, to find matching ones in the first course. This is many times faster than using joins.
select distinct u.users_id, users.name
from users_courses u, users
where u.users_id in (select distinct users_id from users_courses where courses_id = 2)
and u.courses_id = 1
and users.users_id = u.users_id
Almost similar to what #Nae's solution.
select u.name from users u
where exists
(select 1
from users_courses uc
where uc.course_id in (1, 2)
and uc.user_id = u.id
group by uc.user_id
having count(0) = 2);
Your code is close. Just use GROUP BY and a HAVING clause:
SELECT u.name
FROM users_courses uc JOIN
users u
ON uc.users_id = u.id JOIN
courses c
ON uc.courses_id = c.id
WHERE c.name IN ('History', 'Math')
GROUP BY u.name
HAVING COUNT(DISTINCT c.name) = 2;
Notes:
This assumes that users cannot have the same name. You might want to use GROUP BY u.id, u.name to ensure that you are counting individual users.
If users cannot take the same course multiple times, then use COUNT(*) = 2 rather than COUNT(DISTINCT).
I'd write:
SELECT MAX(u.name)
FROM users_courses uc
LEFT JOIN users u ON uc.users_id = u.id
WHERE uc.courses_id IN (1, 2)
GROUP BY uc.users_id
HAVING COUNT(0) = 2
;
For more complex conditions (for example requiring the user to be in certain classes but also not in certain classes such as "Science") this should also work:
SELECT MAX(u.name)
FROM users_courses uc
LEFT JOIN users u ON uc.users_id = u.id
GROUP BY uc.users_id
HAVING (
SUM(uc.courses_id = 1) = 1
-- user enrolled exactly once in the course 2
AND SUM(uc.courses_id = 2) = 1
-- user enrolled in course 3, 0 times
AND SUM(uc.courses_id = 3) = 0
)
;
If i had two database tables
users -> (id, username)
and
messages -> (msg_id, sender_id, rec_id, text)
and they both joind on users.id = messages.sender_id and also on users.id = messages.rec_id so how i can print out results as follow
msg_id | sender | reciver | text
------------------------------------
1 | david | michael | hello friend
2 | eva | robert | pick me up
I've try this
SELECT users.*, messages.*
FROM messages
INNER JOIN users
ON users.id = messages.sender_id
AND users.id = messages.rec_id
but it seems not working as i want .. so any idea
update i ment by it deosn't seems to be working, that it gives the sender and reciver name are the same which is wrong !!
msg_id | sender | reciver | text
------------------------------------
1 | david | david | hello friend
2 | eva | eva | pick me up
You probably want to join multiple times
SELECT users1.*, users2.*,messages.*
FROM messages
INNER JOIN users1
ON users1.id = messages.sender_id
INNER JOIN users2
AND users2.id = messages.rec_id
You need two joins:
SELECT m.*, us.username as sender_name, ur.username as receiver_nae
FROM messages m JOIN
users us
ON us.id = m.sender_id JOIN
users ur
ON ur.id = m.rec_id;
In your current condition, the user id is equal to both the sender and the receiver id, meaning you will only query messages someone sent himself - which is probably not what you want to achieve. Instead, you can join on users twice, once for the sender and once for the receiver:
SELECT m.msg_id, s.username AS sender, r.username AS receiver, m.text
FROM messages m
JOIN users s ON m.sender_id = s.id
JOIN users r ON m.rec_id = r.id
I'm trying to simplify my query so that it only contains the session ID (SID) once.
The abstract structure of the Users table is:
+----+------+----------+
| ID | Name | Username |
+----+------+----------+
The Friends table has an abstract structure like:
+----+-----------------+----------+--------+---------+
| ID | UserID | FriendID | Hidden | Deleted |
| | (Foreign key | | | |
| | of ID in Users) | | | |
+----+-----------------+----------+--------+---------+
The abstract structure of the Sessions table:
+----+-----------------+-----+
| ID | UserID | SID |
| | (Foreign key | |
| | of ID in Users) | |
+----+-----------------+-----+
I have the following query, which has been adapted from the answer of a previous question of mine. As you can see, the session ID (SID) is repeated 4 times, is it possible to condense the query as a whole so that the SID is only required once?
SELECT *
,CASE
WHEN D.ID IS NULL
THEN "Wants to be your friend"
ELSE "Friends"
END AS STATUS
FROM (
SELECT DISTINCT A.ID
,A.NAME
,E.Hidden
FROM Users A
INNER JOIN Friends E ON A.ID = E.UserID
WHERE A.ID IN (
SELECT A.UserID
FROM Friends A
INNER JOIN Sessions S ON A.FriendID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) C
LEFT JOIN (
SELECT DISTINCT B.ID
,B.NAME
,F.Hidden
FROM Users B
INNER JOIN Friends F ON B.ID = F.FriendID
WHERE B.ID IN (
SELECT A.FriendID
FROM Friends A
INNER JOIN Sessions S ON A.UserID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) D ON C.ID = D.ID
UNION
DISTINCT
SELECT *
,CASE
WHEN C.ID IS NULL
THEN "Request Sent"
ELSE "Friends"
END AS STATUS
FROM (
SELECT DISTINCT A.ID
,A.NAME
,E.Hidden
FROM Users A
INNER JOIN Friends E ON A.ID = E.UserID
WHERE A.ID IN (
SELECT A.UserID
FROM Friends A
INNER JOIN Sessions S ON A.FriendID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) C
RIGHT JOIN (
SELECT DISTINCT B.ID
,B.NAME
,F.Hidden
FROM Users B
INNER JOIN Friends F ON B.ID = F.FriendID
WHERE B.ID IN (
SELECT A.FriendID
FROM Friends A
INNER JOIN Sessions S ON A.UserID = S.UserID
WHERE S.SID = "1234"
AND Deleted = 'No'
)
) D ON C.ID = D.ID
A basic way of explaining the system is that if two users are friends, then there is two records within the database. One from the first user to the second and another record from the second user to the first.
A friend request has been sent if there is a record from the current user to another, and a friend request has been received if there is a record from one user to the current one.
Here is a vann diagram of how it works:
SQL Fiddle - http://sqlfiddle.com/#!2/c5587/1
Sql fiddle : http://sqlfiddle.com/#!2/06e08/68/0
This return Friends and Request Sent :
SELECT
f.FriendID,
u.Name,
f.Hidden,
CASE
WHEN reqs.FriendID IS NULL
THEN "Request Sent"
WHEN reqs.FriendID = f.UserID
THEN "Friends"
END AS Status
FROM
Friends AS f
INNER JOIN
Sessions AS s
ON f.UserId = s.UserID
INNER JOIN
Users AS u
ON u.ID = f.FriendID
LEFT JOIN
Friends AS reqs
ON reqs.FriendID = f.UserID
AND reqs.UserID = f.FriendID
WHERE
s.SID = "sid1"
If you want Also Request Received, append this :
UNION
SELECT
f.UserID,
u.Name,
f.Hidden,
"Request Received" AS Status
FROM
Friends AS f
INNER JOIN
Sessions AS s
ON f.FriendID = s.UserID
INNER JOIN
Users AS u
ON u.ID = f.UserID
WHERE
f.UserID NOT IN
(
SELECT
ff.FriendID
FROM
Friends AS ff
INNER JOIN
Sessions AS ss
ON ff.UserID = ss.UserID
WHERE ss.SID = "sid1"
)
AND s.SID = "sid1"
Can't figure out how to optimise the last part. Since it's a SELF JOIN it's a damn mind twister.
I understand this is not what you expected but, i can't get ride of all the SID, but this request should be faster than the one you currently use
I have the following 3 tables:
users
| id | name | address |
|----+-------+----------+
| 1 | user1 | address1 |
| 2 | user2 | address2 |
locations
| id | user_id | name |
|----+---------+-----------+
| 1 | 1 | location1 |
| 2 | 1 | location2 |
| 3 | 2 | location3 |
orders
| id | user_id | from_id | to_id |
|----+---------+---------+-------+
| 1 | 1 | 1 | 2 |
| 2 | 1 | 1 | 0 |
from_id or to_id can have 0, meaning that user's address was used in this case.
Executing a 'naive' join on these tables:
SELECT u.name uname, fl.location flocation, tl.location tlocation
FROM users u, orders o, locations fl, locations tl
WHERE u.id = o.user_id
AND o.from_id = fl.id
AND o.to_id = tl.id
doesn't show records with 0:
user1 | location1 | location2
what I would like to see is the following data:
user1 | location1 | location2
user1 | location1 | address1
Using mysql, is there a way to extend the join to show such results ?
The following SQL should do it:
SELECT u.name uname, IF(fl.id, fl.location, u.address) AS flocation, IF(tl.id, tl.location, u.address) AS tlocation
FROM users u
INNER JOIN orders o ON u.id = o.user_id
LEFT JOIN locations fl ON o.from_id = fl.id
LEFT JOIN locations tl ON o.to_id = tl.id;
And if I may make a suggestion, I would recommend that you not use the WHERE clause as a way to join tables.
SELECT u.name uname, fl.location flocation, tl.location tlocation
FROM users u
left join orders o
on u.id = o.user_id
left join locations fl
on o.from_id = fl.id
left join locations tl
on o.to_id = tl.id
This will give you data for all users.
And as Tim Burch suggested, its best to use the ANSI SQL Syntax.
To use the user's address as a default for both the from and the to names, you can do a left join and use COALESCE to replace nulls with the address;
SELECT
u.name uname,
COALESCE(fl.name, u.address) flocation,
COALESCE(tl.name, u.address) tlocation
FROM users u
JOIN orders o ON u.id = o.user_id
LEFT JOIN locations fl ON o.from_id = fl.id
LEFT JOIN locations tl ON o.to_id = tl.id
An SQLfiddle to test with.
I recommend abandoning the "comma" syntax for the join operation; use the JOIN keyword, and move the join predicates to an ON clause rather than the WHERE clause.
To get all rows from orders returned, whether or not there is a "matching" row from the other tables, you can use an outer join operation.
In the SELECT list, you can test whether the location column from locations IS NULL, and return the address column from the user table when it is, using the COALESCE function.
SELECT u.name uname
, COALESCE(fl.location,u.address) flocation
, COALESCE(tl.location,u.address) tlocation
FROM orders o
LEFT
JOIN users u
ON u.id = o.user_id
LEFT
JOIN locations fl
ON fl.id = o.from_id
LEFT
JOIN locations tl
ON tl.id = o.to_id
The COALESCE(x,y) function is essentially shorthand for 'CASE WHEN x IS NULL THEN y ELSE x END`.
There's a corner case there, what if a matching row in the locations table is found, but the location column actually contains a NULL.
To more precisely match your specification, you could test for a value of "0" in the to_id column of the orders table:
SELECT u.name uname
, IF(o.from_id = 0, u.address, fl.location) flocation
, IF(o.to_id = 0, u.address, tl.location) tlocation
FROM orders o
LEFT
JOIN users u
ON u.id = o.user_id
LEFT
JOIN locations fl
ON fl.id = o.from_id
LEFT
JOIN locations tl
ON tl.id = o.to_id
But there's a corner case there; what if there is a row in locations that has an id value of "0".
The normative pattern would be to test for whether a row was returned from the locations table by checking a column that is guaranteed to be NOT NULL if a match is found, and will be NULL if a match is not found:
SELECT u.name uname
, IF(fl.id IS NULL, u.address, fl.location) flocation
, IF(tl.id IS NULL, u.address, tl.location) tlocation
FROM orders o
LEFT
JOIN users u
ON u.id = o.user_id
LEFT
JOIN locations fl
ON fl.id = o.from_id
LEFT
JOIN locations tl
ON tl.id = o.to_id
All three of those queries will return identical results for the "normal" cases; they just differ in how the "corner cases" are handled.
In summary:
use LEFT [OUTER] JOIN operations with orders as the driving table.
use conditional expressions in the SELECT list to determine which column value to return
I have 3 tables as follows in my database. this is used to a application just like foursqure. i need help with the problem of writing the sql statement i have asked in the bottom of this.
thank you
user_details
user_id | fname
----------------
1 | Losh
8 | Dush
9 | Rosh
10 | NELL
friends
user_idf |user_idff
----------------
1 | 8
8 | 9
10 | 1
Check_in
check_in_id |user_id | place | date
--------------------------------------------
1 | 8 | Hotel | 01/01/2012
2 | 9 | Home | 05/01/2012
3 | 1 | Junction | 08/01/2012
4 | 1 | Rest | 11/01/2012
5 | 9 | Hotel | 15/01/2012
6 | 8 | Home | 15/01/2012
i get the user's who are friends with 8 and user 8 details AND the check in places as follows
SELECT a.`user_id`, a.`fname` , b.*
FROM `user_details` a, `check_in` b
WHERE (b.user_id = 8
OR b.user_id in (select user_idf from friends where user_idff = '8' union select user_idff from friends where user_idf = '8')) AND b.user_id = a.user_id
how do i write the sql to select who are friends with 8 and user 8 details AND the last check in place of those users
explanation::
i seeks for a answer such as
user id name place date
1 LOSH Rest 11/01/2012
8 DUSH HOME 15/01/2012
9 ROSH HOTEL 15/01/2012
Join it to the table returned by:
(SELECT `user_id`, `place` FROM Check_in GROUP BY user_id ORDER BY `date` DESC)
That should give you one entry per user, and since it's sorted in reverse by date, that entry should be the most recent.
But when i group by it gives me the first dates not the latest date
How about this:
(SELECT user_id, place
FROM (SELECT * FROM Check_in ORDER BY `date` DESC) tmp
GROUP BY user_id)
SELECT user_id, fname, c.place
FROM user_details u
INNER JOIN (SELECT IF(user_idff = 8, user_idf, user_idff) AS user_id
FROM friends
WHERE (user_idff = 8 OR user_idf = 8)
) f
ON u.user_id = f.user_id
LEFT JOIN (SELECT c1.user_id, place
FROM Check_in c1
LEFT JOIN Check_in c2
ON c1.user_id = c2.user_id AND
c1.date < c2.date
WHERE c2.date IS NULL
) c
ON u.user_id = c.user_id;
This doesn't break ties but it's a straighforward way of answering your question.
EDIT
I just re-read you question and I see that you want user 8 info also. It's not clear whether you want user 8 as a separate row or with info in line with the friends' rows.
select *
from
friends as f inner join check_in as ci on ci.user_id = f.user_idff
inner join user_details as ud on ud.user_id = f.user_idff
inner join user_details as ud8 on ud8.user_id = f.user_idf
where
f.user_idf = 8
and date = (
select max(date)
from friends as f2 inner join check_in as ci on ci.user_id = f2.user_idff
where f2.user_idf = f.user_idf
)
EDIT 2
You request may be a small bit unclear about determining which check-in location to return. Use this option if you want the latest location of each friend individually. The first query finds the most recent location among all friends. Obviously these are two variations on an identical theme.
select *
from
friends as f inner join check_in as ci on ci.user_id = f.user_idff
inner join user_details as ud on ud.user_id = f.user_idff
inner join user_details as ud8 on ud8.user_id = f.user_idf
where
f.user_idf = 8
and date = (
select max(date)
from check_in as ci
where ci.user_id = f.user_idff
)
(SELECT a.user_id, a.place, b.fname, a.date, a.time, a.check_in_id
FROM (SELECT * FROM check_in ORDER BY date DESC) as a, user_details as b
WHERE a.user_id = b.user_id AND (a.user_id in (select user_idf from friends where user_idff = '8' union select user_idff from friends where user_idf = '8') OR a.user_id = 8)
GROUP BY a.user_id)
above query gave me the required answer.
thank you all for the help given