SQL find team that only contains specified 2 users - mysql

I have a table called team_members with this structure and contents:
+---------+---------+
| team_id | user_id |
+---------+---------+
| 1 | 18 |
+---------+---------+
| 1 | 7 |
+---------+---------+
| 3 | 18 |
+---------+---------+
What i am trying to do is to find a team that only contains 2 users and this users are supplied by me (in this case users with id 7 and 18). Unfortunately, i am having no ideas about how to make this query properly. I have tried something like
SELECT a.team_uid
FROM team_members a
INNER JOIN (
SELECT team_uid, user_id, COUNT(*) cnt_team
FROM team_members
GROUP BY team_uid
HAVING COUNT(*) = 2
) b ON a.user_id = b.user_id

Use Case statement in Having clause and Count only the required user_id's. Try this.
select teamid from yourtable
group by teamid
having count(case when userid=7 then 1 end)=1
and count(case when userid=18 then 1 end)=1
and count(1)=2

Something to think about (and assuming a PK on team_id,user_id)...
SELECT x.*, COUNT(*),SUM(user_id IN(7,18)) FROM my_table x GROUP BY team_id;

A couple more ways to do this (where $id1 and $id2 are the users in question):
SELECT team_id
FROM team_members
GROUP BY team_id
HAVING COUNT(*) = 2
AND MIN(user_id) = LEAST($id1,$id2)
AND MAX(user_id) = GREATEST($id1,$id2)
See SQL Fiddle Demo here with values of 7 and 18 for $id1 and $id2. I am using LEAST() and GREATEST() in case it's not known which is the higher and which is the lower (for example, if they're coming from user input).
SELECT team_id
FROM team_members
GROUP BY team_id
HAVING GROUP_CONCAT(user_id ORDER BY user_id) = ('7,18')
See SQL Fiddle Demo here. Again, if it isn't known which is the higher and which is the lower, then this might be written as (the ORDER BY in GROUP_CONCAT() would be unnecessary):
SELECT team_id
FROM team_members
GROUP BY team_id
HAVING GROUP_CONCAT(user_id) IN ('$id1,$id2','$id2,$id1')

You can also use:
select team_id
from team_members
group by team_id
having sum(user_id not in(7, 18)) = 0
Example above assumes you want teams with only users 7 or 18 (no others, but not necessarily both).
If you want teams with BOTH users 7 and 18, and no others, you can use:
select team_id
from team_members
group by team_id
having sum(user_id not in(7, 18)) = 0
and sum(user_id in(7, 18)) = 2

Related

Can this query, which groups users by amount of comments posted, be simplified?

Two tables are used in this query, and all that matters in the result is the number of users which have or haven't posted any comments so far. The table user of course has the column id, which is the foreign key in the table comment, identified by the column user_id.
The first super-simple query groups users by whether or not they have any comments so far. It outputs two rows (a row with the user count who have comments, and a row with the user count who have no comments), with two columns (number of users, and whether or not they have posted any comments).
SELECT
COUNT(id) AS user_count,
IF( id IN ( SELECT user_id FROM `comment` ), 1, 0) AS has_comment
FROM `user`
GROUP BY has_comment
An example of how the output would look like here:
+------------+-------------+
| user_count | has_comment |
+------------+-------------+
| 150 | 0 |
| 140 | 1 |
+------------+-------------+
Now here comes my question. I want slightly more information here, by grouping these users into 3 groups instead:
Users that have posted no comments
Users that have posted fewer than 10 comments
Users that have posted 10 or more comments
And the best query that I know how to write for this purpose is as follows, which works, but unfortunately runs 4 subqueries and has 2 derived tables:
SELECT
COUNT(id) AS user_count,
CASE
WHEN id IN ( SELECT user_id FROM ( SELECT COUNT(user_id) AS comment_count, user_id FROM `comment` GROUP BY user_id HAVING comment_count >= 10 ) AS a) THEN '10 or more'
WHEN id IN ( SELECT user_id FROM ( SELECT COUNT(user_id) AS comment_count, user_id FROM `comment` GROUP BY user_id HAVING comment_count < 10 ) AS b) THEN 'less than 10'
ELSE 'none'
END AS has_comment
FROM `user`
GROUP BY has_comment
An example of the output here would be something like:
+------------+-------------+
| user_count | has_comment |
+------------+-------------+
| 150 | none |
| 130 | less than 10|
| 100 | 10 or more |
+------------+-------------+
This second query; can it be written more simply and efficiently, and still produce the same kind of result? (potentially maybe even be expanded into more of these kinds of "groups")
You can use two levels of aggregation:
select
count(*) no_users,
case
when no_comments = 0 then 'none'
when no_comments < 10 then 'less than 10'
else '10 or more'
end has_comment
from (
select
u.id,
(select count(*) from comments c where c.user_id = u.id) no_comments
from users u
) t
group by has_comment
order by no_comments
The subquery counts how many comments each user has (you could also express this with a left join and aggregation); then, the outer query classifies and count the users per number of comments.

sum of count(*) for all rows in MySQL

I'm stuck with sum() query where I want the sum of count(*) values in all rows with group by.
Here is the query:
select
u.user_type as user,
u.count,
sum(u.count)
FROM
(
select
DISTINCT
user_type,
count(*) as count
FROM
users
where
(user_type = "driver" OR user_type = "passenger")
GROUP BY
user_type
) u;
Current Output:
----------------------------------
| user | count | sum |
----------------------------------
| driver | 58 | 90 |
----------------------------------
Expected Output:
----------------------------------
| user | count | sum |
----------------------------------
| driver | 58 | 90 |
| passenger | 32 | 90 |
----------------------------------
If I remove sum(u.count) from query then output is looks like:
--------------------------
| user | count |
--------------------------
| driver | 58 |
| passenger | 32 |
--------------------------
You need a subquery:
SELECT user_type,
Count(*) AS count,
(SELECT COUNT(*)
FROM users
WHERE user_type IN ("driver","passenger" )) as sum
FROM users
WHERE user_type IN ("driver","passenger" )
GROUP BY user_type ;
Note you dont need distinct here.
OR
SELECT user_type,
Count(*) AS count,
c.sum
FROM users
CROSS JOIN (
SELECT COUNT(*) as sum
FROM users
WHERE user_type IN ("driver","passenger" )
) as c
WHERE user_type IN ("driver","passenger" )
GROUP BY user_type ;
You can use WITH ROLLUP modifier:
select coalesce(user_type, 'total') as user, count(*) as count
from users
where user_type in ('driver', 'passenger')
group by user_type with rollup
This will return the same information but in a different format:
user | count
----------|------
driver | 32
passenger | 58
total | 90
db-fiddle
In MySQL 8 you can use COUNT() as window function:
select distinct
user_type,
count(*) over (partition by user_type) as count,
count(*) over () as sum
from users
where user_type in ('driver', 'passenger');
Result:
user_type | count | sum
----------|-------|----
driver | 32 | 90
passenger | 58 | 90
db-fiddle
or use CTE (Common Table Expressions):
with cte as (
select user_type, count(*) as count
from users
where user_type in ('driver', 'passenger')
group by user_type
)
select user_type, count, (select sum(count) from cte) as sum
from cte
db-fiddle
I would be tempted to ask; Are you sure you need this at the DB level?
Unless you are working purely in the database layer, any processing of these results will be built into an application layer and will presumably require some form of looping through the results
It could be easier, simpler, and more readable to run
SELECT user_type,
COUNT(*) AS count
FROM users
WHERE user_type IN ("driver", "passenger")
GROUP BY user_type
.. and simply add up the total count in the application layer
As pointed out by Juan in another answer, the DISTINCT is redundant as the GROUP BY ensures that each resultant row is different
Like Juan, I also prefer an IN here, rather than OR condition, for the user_type as I find it more readable. It also reduces the likelihood of confusion if combining further AND conditions in the future
As an aside, I would consider moving the names of the user types, "driver" and "passenger" into a separate user_types table and referencing them by an ID column from your users table
N.B. If you absolutely do need this at the DB level, I would advocate using one of Paul's excellent options, or the CROSS JOIN approach proffered by Tom Mac, and by Juan as his second suggested solution
Try this. Inline view gets the overall total :
SELECT a.user_type,
count(*) AS count,
b.sum
FROM users a
JOIN (SELECT COUNT(*) as sum
FROM users
WHERE user_type IN ("driver","passenger" )
) b ON TRUE
WHERE a.user_type IN ("driver","passenger" )
GROUP BY a.user_type;
You could simply combine SUM() OVER() with COUNT(*):
SELECT user_type, COUNT(*) AS cnt, SUM(COUNT(*)) OVER() AS total
FROM users WHERE user_type IN ('driver', 'passenger') GROUP BY user_type;
db<>fiddle demo
Output:
+------------+------+-------+
| user_type | cnt | total |
+------------+------+-------+
| passenger | 58 | 90 |
| driver | 32 | 90 |
+------------+------+-------+
Add a group by clause at the end for user-type, e.g:
select
u.user_type as user,
u.count,
sum(u.count)
FROM
(
select
DISTINCT
user_type,
count(*) as count
FROM
users
where
(user_type = "driver" OR user_type = "passenger")
GROUP BY
user_type
) u GROUP BY u.user_type;
Tom Mac Explain Properly Your answer. Here is the another way you can do that.
I check the query performance and not found any difference within 1000 records
select user_type,Countuser,(SELECT COUNT(*)
FROM users
WHERE user_type IN ('driver','passenger ') )as sum from (
select user_type,count(*) as Countuser from users a
where a.user_type='driver'
group by a.user_type
union
select user_type,count(*) as Countuser from users b
where b.user_type='passenger'
group by b.user_type
)c
group by user_type,Countuser
Try this:
WITH SUB_Q AS (
SELECT USER_TYPE, COUNT (*) AS CNT
FROM USERS
WHERE USER_TYPE = "passenger" OR USER_TYPE = "driver"
GROUP BY USER_TYPE
),
SUB_Q2 AS (
SELECT SUM(CNT) AS SUM_OF_COUNT
FROM SUB_Q
)
SELECT A.USER_TYPE, A.CNT AS COUNT, SUB_Q2 AS SUM
FROM SUB_Q JOIN SUB_Q2 ON (TRUE);
I used postgresql dialect but you can easily change to a subquery.
select
u.user_type as user,
u.count,
sum(u.count)
FROM users group by user

ORDER BY before GROUP BY for leaderboard

I have a table with all players results:
id |result| user_id
------------------------
1 | 130 | 5C382072
2 | 145 | 5C382072
3 | 130 | 8QHDTz7w
4 | 166 | 6155B6D0
5 | 100 | DFSA3444
Smaller result is better. I need to make query for leaderboard.
Each player must appear once in leaderboard with his best result. If 2 players have equal results, the one with smaller id should appear first.
So I'm expecting this output:
id |result| user_id
------------------------
5 | 100 | DFSA3444
1 | 130 | 5C382072
3 | 130 | 8QHDTz7w
4 | 166 | 6155B6D0
I can't get desired result, cause grouping by user_id goes before ordering it by result, id.
My code:
SELECT id, MIN(result), user_id
FROM results
GROUP BY user_id
ORDER BY result, id
It output something close to desired result, but id field is not connected to row with smallest user result, it can be any id from group with the same user_id. Because of that ordering by id not work at all.
EDIT:
What I didn't mention before is that I need to handle situations when user have identical results.
I came up with two solutions that I don't like. :)
1) A bit slow and ugly:
SELECT t1.*
FROM (SELECT * FROM results WHERE results_status=1) t1
LEFT JOIN (SELECT * FROM results WHERE results_status=1) t2
ON (t1.user_id = t2.user_id AND (t1.result > t2.result OR (t1.result = t2.result AND t1.id > t2.id)))
WHERE t2.id IS NULL
ORDER BY result, id
2) Ten times slower but more clear:
SELECT *
FROM results t1
WHERE id = (
SELECT id
FROM results
WHERE user_id = t1.user_id AND results_status=1
ORDER BY result, id
LIMIT 1
)
ORDER BY result, id
I'm stuck. :(
This should get you close. Avoid MySQL's lenient (errant) GROUP BY syntax, which lets you form a GROUP BY clause without naming unaggregated columns from the SELECT list. Use standard SQL's GROUP BY syntax instead.
select t.user_id, m.min_result, min(t.id) id
from results t
inner join (select user_id, min(result) min_result
from results
group by user_id) m
on t.user_id = m.user_id
and t.result = m.min_result
group by t.user_id, m.min_result
Edit: I think you need a subquery:
SELECT a.id, a.result, a.user_id FROM results a
WHERE a.user_id, a.result IN (SELECT b.user_id, MIN(b.result) FROM results b
GROUP BY b.user_id)
ORDER BY a.user_id
This will return an undefined id if the same user had the same score more than once, but will order the users correctly and will match id to the correct user_id.

mySQL - subquery with multiple fields

I have 2 tables.
Table Matches
id | TeamA | TeamB
--------------
1 | Barça | Madrid
2 | Valencia | Depotivo
Table Payments
idMatch | User | Quantity
---------------------------
1 | Me | 50
2 | Me | 100
Then in one query I want to get TeamA, TeamB, User and Quantity if they have same id.
I've tried this but it fails.
SELECT TeamA, TeamB FROM Matches WHERE id IN (SELECT idMatch, TeamA, TeamB FROM Payments)
try this
SELECT TeamA, TeamB ,User , Quantity FROM Matches m
inner join Payments p
on p.idMatch = m.id
WHERE id IN
(SELECT idMatch FROM Payments)
DEMO HERE
you can use joins
select Matches.TeamA,Matches.TeamB,Payments.User,Payments.Quantity from Matches,Payments where Matches.id=Payments.idMatch
try this
SELECT payments.user,
payments.quantity
FROM
Payments
INNER JOIN Matches ON (Matches.id = Payments.idMatch);
IN() should contain a list of values like IN('red','green','blue') or IN(1,3,53). SELECT is fine, but it has to return a single field. This would work.
SELECT TeamA, TeamB FROM Matches
WHERE id IN (
SELECT idMatch FROM Payments
)
However it looks like you want to achieve something you need JOIN or GROUP BY for.
Try
SELECT
TeamA,
TeamB,
payments.user,
payments.quantity
FROM
matches
JOIN payments ON ( matches.id = payments.matchid )
select m.TeamA,m.TeamB,p.`User`,p.Quantity from matches m INNER join
payments p where p.idMatch=m.id

MySQL - Complex COUNT Query

I have a table called user_scores as below:
id | af_id | uid | level | record_date
----------------------------------------
1 | 1.1 | 1 | 3 | 2012-01-01
2 | 1.1 | 1 | 4 | 2012-02-01
3 | 1.2 | 1 | 3 | 2012-01-01
4 | 1.2 | 1 | 5 | 2012-03-01
...
I have another table call user_info as below:
uid | forename | surname | gender
-----------------------------------
1 | Homer | Simpson | M
2 | Marge | Simpson | F
3 | Bart | Simpson | M
4 | Lisa | Simpson | F
...
In user scores uid is the user id of a registered user on the system, af_id identifies a particular test a user submits. A user scores a level between 1 - 5 for each test, which can be submitted every month.
My problem is I need to produce an analysis at the end of the year to COUNT the number of users that have achieved each level for a particular test. The analysis is to show a gender split for male and female.
So for example an administrator would select test 1.1 and the system would generate stats based that would COUNT of the total MAX level achieved by each user in the year, with a gender split.
Any help is much appreciated. Thank you in advance.
-
I think I need to clarify myself a bit. Because a user can complete the test multiple times throughout the year, there will be multiple scores for the same test. The query should take the highest level achieved and include this in the count. An example result would be:
Male Results:
level1 | level2 | level3 | level4 | level5
------------------------------------------
2 | 5 | 10 | 8 | 1
I am not certain I get exactly what you mean, but as always I'll have a go. As I understand it you want to know how many people from each gender reached each level in a certain year.
SELECT MaxLevel,
COUNT(CASE WHEN ui.Gender = 'M' THEN 1 END) AS Males,
COUNT(CASE WHEN ui.Gender = 'F' THEN 1 END) AS Females
FROM User_Info ui
INNER JOIN
( SELECT MAX(Level) AS MaxLevel,
UID
FROM User_Scores us
WHERE af_ID = '1.1'
AND YEAR(Record_Date) = 2012
GROUP BY UID
) AS MaxUs
ON MaxUs.uid = ui.UID
GROUP BY MaxLevel
I've put some sample data on SQL Fiddle so you see if it is what you were after.
EDIT
To transpose the data so levels are along the top and Gender in the rows the following will work:
SELECT Gender,
COUNT(CASE WHEN MaxLevel = 1 THEN 1 END) AS Level1,
COUNT(CASE WHEN MaxLevel = 2 THEN 1 END) AS Level2,
COUNT(CASE WHEN MaxLevel = 3 THEN 1 END) AS Level3,
COUNT(CASE WHEN MaxLevel = 4 THEN 1 END) AS Level4,
COUNT(CASE WHEN MaxLevel = 5 THEN 1 END) AS Level5
FROM User_Info ui
INNER JOIN
( SELECT MAX(Level) AS MaxLevel,
UID
FROM User_Scores us
WHERE af_ID = '1.1'
AND YEAR(Record_Date) = 2012
GROUP BY UID
) AS MaxUs
ON MaxUs.uid = ui.UID
GROUP BY Gender
Note, that if there are ever more than 5 levels you will need to add more to the select statement, or start building dynamic SQL.
Assuming record_date holds only dates (without time parts):
SELECT
s.maxlevel,
COUNT(NULLIF(gender, 'F')) AS M,
COUNT(NULLIF(gender, 'M')) AS F
FROM user_info u
INNER JOIN (
SELECT
uid,
MAX(level) AS maxlevel
FROM user_scores
WHERE record_date > DATE_SUB(CURDATE(), INTERVAL DAYOFYEAR(CURDATE()) DAY)
AND af_id = '1.1'
GROUP BY
uid
) s ON s.uid = u.uid
GROUP BY
s.maxlevel
That will show you only the maximum levels found in the user_scores table. If you have a Levels table where all possible levels (1 to 5) are listed, you could use that table to get a complete list of levels. If some levels are not present in the requested subset of data, the corresponding rows will show 0s in both columns.
Here's the above script with minor changes to show the complete chart of levels:
SELECT
l.level AS maxlevel,
COUNT(NULLIF(gender, 'F')) AS M,
COUNT(NULLIF(gender, 'M')) AS F
FROM user_info u
INNER JOIN (
SELECT
uid, MAX(level) AS maxlevel
FROM user_scores
WHERE record_date > DATE_SUB(CURDATE(), INTERVAL DAYOFYEAR(CURDATE()) DAY)
AND af_id = '1.1'
GROUP BY
uid
) s ON s.uid = u.uid
RIGHT JOIN Levels l ON s.maxlevel = l.level
GROUP BY
l.level
Hope this is what your looking for!
Show number of records group by userid and gender of the max score for af_id '1.1'.
select count(*), info.uid, info.gender, max(score.level)
from user_info as info
join user_scores as score
on info.uid = score.uid
where score.af_id = '1.1'
group by info.uid, info.gender;
EDITED based on your edit.
select sum(if(a.gender="M",1,0)) Male_users, sum(if(a.gender="F",1,0)) Female_users
from myTable a where
a.level = (select max(b.level) from myTable b where a.uid=b.uid)
group by af_id.
I typed this in a rush. But it should work or at least get you where you need to go. E.G. if you need to specify time frame, add that.
You need something like
SELECT
uid,
MAX(level)
WHERE
record_date BETWEEN '2012-01-01' AND '2012-12-31'
AND af_id='1.1'
GROUP BY uid
If you need the gender splits then depending on what stat you need per gender you can either add a JOIN on the user_info table into this query (to get the MAX per gender) to wrap this as a sub-query and JOIN on the whole thing.