Get groups(only allowed) for user using single mysql query - mysql

Recently i came across one interesting problem. Let add some tables namely User, Group and their Join table User_Group. Following is the dummy representation of the tables
User
user_id | user_name
1 | a
2 | b
3 | c
Group
group_id | group_code
1 | G1
2 | G2
3 | G3
User_Group
userid | groupid | exclusion
1 | 1 | 0
1 | 2 | 0
2 | 1 | 1
Well the problem is to get the list of groups for the user on which he have access on.
General assumption of the user access in the group is :
If user have no entry in User_group join table the user has access on
all groups (ie. G1, G2 and G3)
If user have entry in User_Group with
exclusion 0 then that user have access in that group only (user 1 has
access in G1 and g2)
If user have entry in User_Group with exclusion
1 then that user have no access in that group but he has access in
other all other groups(user 2 is denied only on g1 and should have
access in G2 and G3)
Here's the query that get me results for case 1 & 3 but fails for case 2.
SELECT g.id, g.code, ug.user_groups_id, ug.exclusion FROM group g
LEFT JOIN user_group ug ON ug.group_id = g.id
AND ug.user_id = 1 -- works for user 2 and 3 but fails for user 1
WHERE (ug.exclusion = 0 OR ug.exclusion IS NULL)
Please suggest if we can get the list of groups using just single query

Interesting problem.
You can solve this by generating a list of all users and all groups (using cross join). Then use left join to match back to the list of groups that are known. The logic is then:
Case 1 is to use all the groups when there is no match.
Case 2 is to use only the groups that do match.
Case 3 is to use only the groups that do not match.
Here is the query:
select u.userid,
(case when max(ug.exclusion) is null -- all groups
then group_concat(g.groupcode)
when max(ug.exclusion) = 0 -- only those included
then group_concat(case when ug.groupid is not null then g.groupcode end)
when max(ug.exclusion) = 1 -- exclude these
then group_concat(case when ug.groupid is null then g.groupcode end)
end)
from users u cross join
groups g left join
user_groups ug
on u.userid = ug.userid and g.groupid = ug.groupid
group by u.userid;
Do note that this only uses the exclusion flag in the aggregate -- basically assuming that it is the same on all rows in user_groups. In fact, this flag could go at the user level rather than at the user_groups level. This assumption is consistent both with the sample data and the explanation of the problem.

Related

Getting an output based on conditions and creating a new column and updating it based on a condition in SQL

We have two tables: “riders” and “drivers”. Some riders are also drivers, so will show up on both tables. Each
table looks like this:
Table riders
City | user_id | signup_date
SF | u1 | 3/12
CH | u2 | 12/12
SF | u3 | 5/10
I have tried this code:
SELECT riders.id, 'flag' as flag FROM riders INNER JOIN drivers ON riders.id = drivers.id where riders.City ='SF';
Write a query that outputs all SF riders and add a column called ‘flag’ that prints ‘true’ if the rider is also a SF driver and ‘false’ if the rider is not an SF driver.
My output should look like a column of user_id and flag
user_id | flag
u1 | true if u1 is also in Table 'drivers'
u3 | false if u3 is not in Table 'drivers' but in City SF
You are on the right track, but your query has two problems:
The definition of flag is just a constant.
Your join conditions are not correct.
You need to JOIN the tables on the user_id and then check if there is a match in the second table:
SELECT r.user_id, (d.user_id is not null) as flag
FROM riders r LEFT JOIN
drivers d
ON r.user_id = d.user_id
WHERE riders.City = 'SF';
If you want true and false as strings:
(CASE WHEN d.user_id IS NOT NULL THEN 'true' ELSE 'false' END)

Selecting a count of rows having a max value

Working example: http://sqlfiddle.com/#!9/80995/20
I have three tables, a user table, a user_group table, and a link table.
The link table contains the dates that users were added to user groups. I need a query that returns the count of users currently in each group. The most recent date determines the group that the user is currently in.
SELECT
user_groups.name,
COUNT(l.name) AS ct,
GROUP_CONCAT(l.`name` separator ", ") AS members
FROM user_groups
LEFT JOIN
(SELECT MAX(added), group_id, name FROM link LEFT JOIN users ON users.id = link.user_id GROUP BY user_id) l
ON l.group_id = user_groups.id
GROUP BY user_groups.id
My question is if the query I have written could be optimized, or written better.
Thanks!
Ben
You actual query is not giving you the answer you want; at least, as far as I understand your question. John actually joined group 2 on 2017-01-05, yet it appears on group 1 (that he joined on 2017-01-01) on your results. Note also you're missing one Group 4.
Using standard SQL, I think the next query is what you're looking for. The comments in the query should clarify what each part is doing:
SELECT
user_groups.name AS group_name,
COUNT(u.name) AS member_count,
group_concat(u.name separator ', ') AS members
FROM
user_groups
LEFT JOIN
(
SELECT * FROM
(-- For each user, find most recent date s/he got into a group
SELECT
user_id AS the_user_id, MAX(added) AS last_added
FROM
link
GROUP BY
the_user_id
) AS u_a
-- Join back to the link table, so that the `group_id` can be retrieved
JOIN link l2 ON l2.user_id = u_a.the_user_id AND l2.added = u_a.last_added
) AS most_recent_group ON most_recent_group.group_id = user_groups.id
-- And get the users...
LEFT JOIN users u ON u.id = most_recent_group.the_user_id
GROUP BY
user_groups.id, user_groups.name
ORDER BY
user_groups.name ;
This can be written in a more compact way in MySQL (abusing the fact that, in older versions of MySQL, it doesn't follow the SQL standard for the GROUP BY restrictions).
That's what you'll get:
group_name | member_count | members
:--------- | -----------: | :-------------
Group 1 | 2 | Mikie, Dominic
Group 2 | 2 | John, Paddy
Group 3 | 0 | null
Group 4 | 1 | Nellie
dbfiddle here
Note that this query can be simplified if you use a database with window functions (such as MariaDB 10.2). Then, you can use:
SELECT
user_groups.name AS group_name,
COUNT(u.name) AS member_count,
group_concat(u.name separator ', ') AS members
FROM
user_groups
LEFT JOIN
(
SELECT
user_id AS the_user_id,
last_value(group_id) OVER (PARTITION BY user_id ORDER BY added) AS group_id
FROM
link
GROUP BY
user_id
) AS most_recent_group ON most_recent_group.group_id = user_groups.id
-- And get the users...
LEFT JOIN users u ON u.id = most_recent_group.the_user_id
GROUP BY
user_groups.id, user_groups.name
ORDER BY
user_groups.name ;
dbfiddle here

mysql join table on condition that compares two records

I am trying to create a left join with an on statement that has requirements in two records. Essentially, i am trying to determine if two users are members of a group via a groupId and userId
This is where I started
LEFT JOIN groups AS g1 ON g1.status = "active" AND g1.userId = "user1"
LEFT JOIN groups AS g2 ON g2.status = "active" AND g1.groupId = g2.groupId AND g2.userId = "user2"
The problem with the two the joins is that the first join is going to select the first available record that meets this condition. The second join will be generated from the first join due to the groupId. This may work if the groupId for the second is the same. However, this is not the case as users can be members of a bunch of different groups. Essentially, I am trying to create a join statements that attempts to find a similar group (via groupId) between the two users. If the group is not there is just returns null (reason for me using left join).
How can I create a join that compares a similar groupId between two records?
Thanks
Here is the entire query
SELECT u*, code.description AS statusText...., CASE WHEN u.id = "ME" THEN 1 ELSE g2.groupId END AS public
FROM users AS u
LEFT JOIN codes AS code ON code.code = u.status
LEFT JOIN groups AS g1 ON g1.status = "active" AND g1.userId = u.id
LEFT JOIN groups AS g2 ON g2.status = "active" AND g1.groupId = g2.groupId AND g2.userId = "ME"
WHERE u.id = :u
LIMIT 1
Where ME equals your user login and :u is the user we are looking up
Essentially, i am trying to compare if you and another user a part of the same group.
Results would look like an array with the following details
[userId] => 1
[date] => 2015-06-16 00:00:00
[player] => first name
[email] => email
....
[public] => 1 <- this is the important variable i am trying to get. I want to return a number that i will treat as a boolean. The number tells me that i have a group in common with the user i am looking up
The group table
| userId | groupId | colum1 | ... |
+--------+---------+--------+------+
| 1 | 1 | 1 | ALL |
+--------+---------+--------+------+
| 1 | 5 | 1 | NONE |
+--------+---------+--------+------+
| 6 | 1 | 1 | ALL |
+--------+---------+--------+------+
| 2 | 3 | 1 | NONE |
+--------+---------+--------+------+
From this example of the table data you can see that users 1 and users 6 are part of the same group. If this was the only data in the table than my join statement would work. However, this is not the case and if user 1 and ME were both part of group 5 my statement would fail
I would prefer not to use a subquery

MySQL - JOIN multiple rows to single row multiple times

I am trying to join multiple rows of information for single row, but it seems to multiply every time there is more rows in one of the joins.
My tables structure is as follows:
news
id | title | public
------------------------
1 | Test | 0
news_groups_map
id | news_id | members_group_id
------------------------------------
1 | 1 | 5
2 | 2 | 6
members_groups_map
id | member_id | group_id
------------------------------
1 | 750 | 5
2 | 750 | 6
The query I've got so far is:
SELECT
n.title,
n.public,
CAST(GROUP_CONCAT(ngm.members_group_id) AS CHAR(1000)) AS news_groups,
CAST(GROUP_CONCAT(member_groups.group_id) AS CHAR(1000)) AS user_groups
FROM news n
LEFT JOIN news_groups_map ngm ON n.id = ngm.news_id
JOIN (
SELECT group_id
FROM members_groups_map
WHERE member_id = 750
) member_groups
WHERE n.public = 0
GROUP BY n.id
However, the result is as follows:
title | public | news_groups | user_groups
-------------------------------------------------
Test | 0 | 5,6,5,6 | 6,6,5,5
As you can see, the news_group and user_groups are duplicating, so if a news article is in 3 groups, the user_groups will be multiplied as well and show something like 5,6,6,6,5,5.
How can I group those groups, so that they are only displayed once?
The ultimate goal here is to compare news_groups and user_groups. So if at least one group matches (meaning user has enough permissions), then there should be a boolean with true returned, and false otherwise. I don't know how to do that either, however, I thought I should sort out the grouping first, as once the number of groups gets bigger there is going to be unnecessary lots of same data selected.
Thanks!
The simplest method is to use distinct:
SELECT n.title, n.public,
GROUP_CONCAT(DISTINCT ngm.members_group_id) AS news_groups,
GROUP_CONCAT(DISTINCT mg.group_id) AS user_groups
FROM news n LEFT JOIN
news_groups_map ngm
ON n.id = ngm.news_id CROSS JOIN
(SELECT group_id
FROM members_groups_map
WHERE member_id = 750
) mg
WHERE n.public = 0
GROUP BY n.id;
This query doesn't actually make sense. First, the subquery is not needed:
SELECT n.title, n.public,
GROUP_CONCAT(DISTINCT ngm.members_group_id) AS news_groups,
GROUP_CONCAT(DISTINCTD mg.group_id) AS user_groups
FROM news n LEFT JOIN
news_groups_map ngm
ON n.id = ngm.news_id CROSS JOIN
members_groups_map mg
ON member_id = 750
WHERE n.public = 0
GROUP BY n.id;
Second, the CROSS JOIN (or equivalently, JOIN without an ON clause) doesn't make sense. Normally, I would expect a join condition to one of the other tables.
Use DISTINCT in the GROUP_CONCAT
...
CAST(GROUP_CONCAT(DISTINCT ngm.members_group_id) AS CHAR(1000)) AS news_groups,
CAST(GROUP_CONCAT(DISTINCT member_groups.group_id) AS CHAR(1000)) AS user_groups
...

Select with Multiple Counts on Left Join on Same Table

I'm not certain that this can be done, I have a table of users with a related table of user activity joined on a foreign key. Activity has different types, e.g. comment, like etc. I need to get users filtered by the number of each different type of activity.
What I have so far is this:
SELECT
users.*,
COUNT(t1.id) AS comments,
COUNT(t2.id) AS likes
FROM users
LEFT JOIN activity AS t1 ON users.id = t1.user_id
LEFT JOIN activity AS t2 ON users.id = t2.user_id
WHERE t1.activity_type_id = 1 AND t2.activity_type_id = 2
GROUP BY users.id
HAVING comments >= 5 AND likes >= 5
This seems to be close but it's returning a user with a count of 5 both likes and comments, when in reality the user has 5 likes and 1 comment.
To be clear I want this query to return users who have 5 or more likes and also users who have 5 or more comments.
UPDATE:
I've created an SQL Fiddle. In this case I have 3 users:
User 1: 6 comments, 8 likes
User 2: 3 comments, 2 likes
User 3: 5 comments, 2 likes
I want the query to return only user 1 and 3, with their respective totals of likes and comments.
http://sqlfiddle.com/#!2/dcc63/4
You can use conditional summing to do the count and due to the way MySQL treats boolean expressions an expression like sum(case when et.name = 'comment' then 1 else 0 end) (the "normal" SQL syntax) can be reduced to sum(case when et.name = 'comment').
SELECT
u.id,
sum(et.name = 'comment') AS comments,
sum(et.name = 'like') AS likes
FROM users AS u
LEFT JOIN engagements AS e ON u.id = e.user_id
JOIN engagement_types AS et ON e.engagement_type_id = et.id
GROUP BY u.id
HAVING sum(et.name = 'comment') >= 5
OR sum(et.name = 'like') >= 5
Result:
| ID | COMMENTS | LIKES |
|----|----------|-------|
| 1 | 6 | 8 |
| 3 | 5 | 2 |
Sample SQL Fiddle