Many to Many restriction of one side - mysql

I have 3 tables
user
id | etc |
game
id | etc
user_game
user_id | game_id
I want to create a situation where n users couldn't create another game if and only if they already have a game between those n users.
so for example, assume we have user_ids: 1,2 and they're both playing in game_id 1, they can't create another game between them, they could however create a new game if user_id 3 joins the game.
Is it possible to query for such thing, if so - how could I go about it? tried playing with queries, joins, selects and where's for far too long :)
EDIT: the end result should be that I query for severel users, and see if they're elgible to play together (ie. no records were found of those n users playing together).

Assume that you have a table with the list of proposed users. I'll call it ProposedUsers. You can do the following to find any game_id that has those users:
select game_id
from user_game ug join
ProposedUsers pu
on ug.user_id = pu.user_id
group by game_id
having count(distinct ug.user_id) = (select count(distinct user_id) from ProposedUsers)
Your question is a bit unclear as to whether the match to an existing game is exact (no other users on the game) or not. The above handles the subset case. The exact match would be:
select game_id
from user_game ug left outer join
ProposedUsers pu
on ug.user_id = pu.user_id
group by game_id
having count(distinct ug.user_id) = (select count(distinct user_id) from ProposedUsers)

Related

How to join multiple SQL tables without matching columns and different number of row

I was hoping someone could help or point me in the right direction, I’m pretty new to SQL queries having mainly focused on FE and have the following issue.
I have 4 tables I need to connect and pull information from each:
Users, Squad, Games-played and Games.
The current possible column matches through FKs are:
Users
id
Squads
id
Player
Users.id FK
Games played
id
Squads.id FK
Games.id FK
Games
id
name
Games_played.id FK
I need to get Games.name from the Games table to the Squad table, Games_played is my middle point and I’m wonder how I can use it to get that information across.
I can pass Games.id through to Squads from it being the FK for Games_played.
Games.id corespondents with Games.name (the column I want),
Whilst Games and Squads have a different number of rows I wanted to display Games.name, which may repeat themselves on Squads.
Example desired output:
Users.id = 1 | Squads.Players = w | Games_played.Games.id = 1 | Games.name = x
Users.id = 2 | Squads.Players = y | Games_played.Games.id = 2 | Games.name = z
Users.id = 3 | Squads.Players = s | Games_played.Games.id = 1 | Games.name = x
So Games_played.Games.id = 1 will always show Games.name = x
Users.id passed to Squads via FK.
Player coming from Squads,
Games.id coming from Games but passed as FK to Games_played and able to join to Squads as Games_played.Games.I’d,
Games.name coming from Games and somehow attached to Squads.
Any help would be greatly appreciated. Thank you.
I need to get Games.name from the Games table to the Squad table
I guess you mean that you want a query that returns the contents of the Squad table, augmented by the Games.name of the associated Game. From the available structure, the natural and only plausible alternative appears to be this:
select
s.*,
gp.Games_id,
g.name
from
Squads s
left join Games_played gp on gp.Squads_id = s.id
left join Games g on gp.Games_id = g.id
If you want to ignore squads that that don't have any associated Games_played then use ordinary inner joins instead of left joins.
Whilst Games and Squads have a different number of rows I wanted to display Games.name, which may repeat themselves on Squads.
That's not inherently problematic, but note well that given your key structure, your result will have one row for every row of Games_played, plus (in the left join case) one row for every squad that is not associated with any Games_played. The latter group will have NULL Games_id and name. That may mean multiple rows for some squads, each with a different Games_id and possibly different names. It could also mean multiple rows for some Games, each with different Squad details.

Optimisation of subqueries

I have a relation between users and groups. Users can be in a group or not.
EDIT : Added some stuff to the model to make it more convenient.
Let's say I have a rule to add users in a group considering it has a specific town, and a custom metadata like age 18).
Curently, I do that to know which users I have to add in the group of the people living in Paris who are 18:
SELECT user.id AS 'id'
FROM user
LEFT JOIN
(
SELECT user_id
FROM user_has_role_group
WHERE role_group_id = 1 -- Group for Paris
)
AS T1
ON user.id = T1.user_id
WHERE
(
user.town = 'Paris' AND JSON_EXTRACT('custom_metadata', '$.age') = 18
)
AND T1.user_id IS NULL
It works & gives me the IDs of the users to insert in group.
But when I have 50 groups to proceed, like for 50 town or various ages, it forces me to do 50 requests, it's very slow and not efficient for my Database.
How could I generate a result for each group ?
Something like :
role_group_id user_to_add
1 1
1 2
2 1
2 3
The only way I know to do that for now is to do an UNION on several sub queries like the one above, but of course it's very slow.
Note that the custom_metadata field is a user defined field. I can't create specific columns or tables.
Thanks a lot for your help.
if I good understood you:
select user.id, grp.id
from user, role_group grp
where (user.id, grp.id) not in (select user_id, role_group_id from user_has_role_group) and user.town in ('Paris', 'Warsav')
that code give list of users and group which they not belong from one of towns..
To add the missing entries to user_has_role_group, you might want to have some mapping between those town names and their group_id's.
The example below is just using a subquery with unions for that.
But you could replace that with a select from a table.
Maybe even from role_group, if those names correlate with the user town names.
insert into user_has_role_group (user_id, group_id)
select u.user_id, g.group_id
from user u
join (
select 'Paris' as name, 1 as group_id union all
select 'Rome', 2
-- add more towns here
) g on (u.town = g.name)
left join user_has_role_group ug
on (ug.user_id = u.user_id and ug.role_group_id = g.group_id)
where u.town in ('Paris','Rome') -- add more towns here
and json_extract(u.custom_metadata, '$.age') = 18
and ug.id is null;

mySQL IF condition THEN condition

I am struggling with the WHERE part of a query. The query itself contains a LEFT JOIN based on an ID that is present in both tables. However I require the where statement to only return the largest single result that is present in one of the columns. Currently I am return all the values in the join, including values that I do not want.
My Current SQL is
SELECT u.uid, t.id
GROUP_CONCAT(u.forename, ' ', u.surname) AS name,
GROUP CONCAT(DISTINCT scores.points) AS point
FROM users AS U
JOIN teamname AS t
LEFT JOIN (
SELECT team_id, id
FROM games AS g
LEFT JOIN (
SELECT points, team_id
FROM scores as s
) AS S ON t.id = S.team_id
WHERE IF (S.points > 3, S.points > 2, S.point =1)
) AS G ON t.id = G.team_id
ORDER BY surname ASC;
The result of such might be something along the lines of
NAME | TEAM | GAMES | POINTS
Joe | 1 | 1,2,3,4 | 1,3,3,2,3
In this instance the first game was a draw and was replied resulting in a higher points score, I am only wanting the higher points score based on that game.
Any help would be appreciated.
Updated with Tables
users
uid
forename
surname
Team
id
teamname
uid
games
id
team_id
points
Still not quite sure if I understood your tables correctly. It seems a users has one or more teams, each team has one or more games with one or more results per game. You want to show for each user and each team the games concatenated in one column and the highest points for each game concatenated in a second column.
If my assumptions are correct the following query should do the trick. Basically, you first group the data by user/team/game and select the max points per game, then you group the results by user/team and concatenate the games and points.
Please let me know if I misunderstood any of your requirements.
Example in an SQL Fiddle
SELECT
t.uid,
t.forename,
t.team_id,
GROUP_CONCAT(t.game_id) as games,
GROUP_CONCAT(t.max_points) as max_points
FROM (
SELECT
users.uid,
users.forename,
teams.id AS team_id,
games.id AS game_id,
max(games.points) as max_points
FROM
users
LEFT JOIN teams ON users.uid = teams.uid
LEFT JOIN games ON teams.id = games.team_id
GROUP BY
users.uid,
users.forename,
teams.id,
games.id
) t
GROUP BY
t.uid,
t.forename,
t.team_id

Finding mutual friend in one way relationship table

want mysql query for finding mutual friend between two friend but
I am maintain the friendship of user in one way relationship for ex.
first is users table
id name
1 abc
2 xyz
3 pqr
Now second table is friend
id user_id friend_id
1 1 2
2 1 3
3 2 3
Now here i can say that abc(id=1) is friend of xyz(id=2) now similar way the xyz is friend of abc but now i want to find mutual friend between abc(id=1) and xyz(id=2) that is pqr so I want mysql query for that.
REVISED
This query will consider the "one way" relationship of a row in the friend table to be a "two way" relationship. That is, it will consider a friend relationship: ('abc','xyz') to be equivalent to the inverse relationship: ('xyz','abc'). (NOTE: we don't have any guarantee that both rows won't appear in the table, so we need to be careful about that. The UNION operator conveniently eliminates duplicates for us.)
This query should satisfy the specification:
SELECT mf.id
, mf.name
FROM (
SELECT fr.user_id AS user_id
, fr.friend_id AS friend_id
FROM friend fr
JOIN users fru
ON fru.id = fr.user_id
WHERE fru.name IN ('abc','xyz')
UNION
SELECT fl.friend_id AS user_id
, fl.user_id AS friend_id
FROM friend fl
JOIN users flf
ON flf.id = fl.friend_id
WHERE flf.user IN ('abc','xyz')
) f
JOIN users mf
ON mf.id = f.friend_id
GROUP BY mf.id, mf.name
HAVING COUNT(1) = 2
ORDER BY mf.id, mf.name
SQL Fiddle here http://sqlfiddle.com/#!2/b23a5/2
A more detailed explanation of how we arrive at this is given below. The original queries below assumed that a row in the friend table represented a "one way" relationship, in that "'abc' ff 'xyz'" did not imply "'xyz' ff 'abc'". But additional comments from the OP hinted that this was not the case.
If there is a unique constraint on friend(user_id,friend_id), then one way to get the result would be to get all of the friends of each user, and get a count of rows for that friend. If the count is 2, then we know a particular friend_id appears for both user 'abc' and for 'xyz'
SELECT mf.id
, mf.name
FROM friend f
JOIN users uu
ON uu.id = f.user_id
JOIN users mf
ON mf.id = f.friend_id
WHERE uu.name IN ('abc','xyz')
GROUP BY mf.id, mf.name
HAVING COUNT(1) = 2
ORDER BY mf.id, mf.name
(This approach can also be extended to find a mutual friend of three or more users, by including more users in the IN list, and changing the value we compare the COUNT(1) to.
This isn't the only query that will return the specified resultset; there are other ways to get it as well.
Another way to get an equivalent result:
SELECT u.id
, u.name
FROM ( SELECT f1.friend_id
FROM friend f1
JOIN users u1
ON u1.id = f1.user_id
WHERE u1.name = 'abc'
) t1
JOIN ( SELECT f2.friend_id
FROM friend f2
JOIN users u2
ON u2.id = f2.user_id
WHERE u2.name = 'xyz'
) t2
ON t2.friend_id = t1.friend_id
JOIN users u
ON u.id = t1.friend_id
ORDER BY u.id, u.name
NOTES
These queries do not check whether user 'abc' is a friend of 'xyz' (the two user names specified in the WHERE clause). It is only finding the common friend of both 'abc' and 'xyz'.
FOLLOWUP
The queries above satisfy the specified requirements, and all the examples and test cases provided in the question.
Now it sounds as if you want a row in that relationship table to be considered a "two way" relationship rather than just a "one way" relationship. It sounds like you want to want to consider the friend relationship ('abc','xyz') equivalent to ('xyz','abc').
To get that, then all that needs to be done is to have the query create the inverse rows,, and that makes it easier to query. We just need to be careful that if both those rows ('abc','xyz') and ('xyz','abc') already exist, that we don't create duplicates of them when we invert them.
To create the inverse rows, we can use a query like this. (It's simpler to look at this when we don't have the JOIN to the users table, and we use just the id value:
SELECT fr.user_id
, fr.friend_id
FROM friend fr
WHERE fr.user_id IN (1,2)
UNION
SELECT fl.friend_id AS user_id
, fl.user_id AS friend_id
FROM friend fl
WHERE fl.friend_id IN (1,2)
It's simpler if we don't include the predicates on the user_id and friend_id table, but that could be a very large (and expensive) rowset to materialize.
try this:
given that you want to get the mutual friends of friends 1 & 2
select friend_id into #tbl1 from users where user_id = 1
select friend_id into #tbl2 from users where friend_id = 2
select id, name from users where id in(select friend_id from #tbl1 f1, #tbl2 f2 where f1.friend_id=f2.friend_id)

Building SQL query on a one-to-many relationship

I have a search page where I am trying to build a complex search condition on two tables which look something like:
Users
ID NAME
1 Paul
2 Remy
...
Profiles
FK_USERS_ID TOPIC TOPIC ID
1 language 1
1 language 2
1 expertise 1
1 expertise 2
1 expertise 3
2 language 1
2 language 2
The second table Profiles, lists the "languages" or the "expertises" (among other stuff) of each user, and topic id is a foreign key to another table depending on the topic (if topic is "language", than topic ID is the ID of a language in the languages table, etc...).
The search needs to find something like where user name LIKE %PAU% and the user "has" language 1 and has language 2 and has expertise 1 and has expertise 2.
Any help would be really appreciated! I am performing a LEFT JOIN on the two tables although I am not sure that is the correct choice. My main problem lies on the "AND". The same user has to have both languages 1 and 2, and at the same time expertise 1 and 2.
I work in PHP and I usually try to avoid inner SELECTs and even joins, but I think an inner SELECT is imminent here?
You can accomplish this by building a set of users that matches the criterias from your profile tables, something like this:
SELECT FK_USERS_ID
FROM Profiles
WHERE topic='x'
AND TOPIC_ID IN (1,2)
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 2
Here you list your users that matches the topics you need. By grouping by the user id and specifying the amount of rows that should be returned, you can effectively say "only those that has x and y in topic z. Just make sure that the COUNT(1) = x has the same number of different TOPIC_IDs to look for.
You can then query the user table
SELECT ID
FROM Users
WHERE name like '%PAU%'
AND ID IN (<insert above query here>)
You can also do it in a join and a derived table, but the essence should be explained above.
EDIT:
if you are looking for multiple combinations, you can use mysql's multi-column IN:
SELECT FK_USERS_ID
FROM Profiles
WHERE (topic,topic_id) IN (('x',3),('x',5),('y',3),('y',6))
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 4
This will look for uses matching the pairs x-3, x-5, y-3 and y-6.
You should be able to build the topic-topic_id pairs easily in php and stuffing it into the SQL string, and also just counting the number of pairs you generate into a variable for using for the count(1) number. See http://www.mysqlperformanceblog.com/2008/04/04/multi-column-in-clause-unexpected-mysql-issue/ for performance talk using this approach.
Isn't it just a simple classical INNER JOIN?
SELECT
p.topic, p.topic_id
FROM
profiles p
INNER JOIN
users u
ON
u.id = p.fk_users_id
WHERE
u.name LIKE '%Paul%'
This query would return all the languages and expertise with their IDs for the users matching the pattern, in this case containing Paul in their name. Is this what you like? Or something else?
select *
from users u, profiles p
where u.id = p.fk_users_id
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'language'
and topic_id = 1)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'language'
and topic_id = 22)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'expertise'
and topic_id = 1)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'expertise'
and topic_id = 1)
and u.name like '%PAU%'
EDIT:
Ok, a slight variation on #cairnz' answer:
SELECT ID
FROM Users
WHERE name like '%PAU%'
AND ID IN (SELECT FK_USERS_ID
FROM Profiles
WHERE topic='x'
AND ((TOPIC_ID = 1 AND TOPIC = 'language')
OR (TOPIC_ID = 2 AND TOPIC = 'language')
OR (TOPIC_ID = 1 AND TOPIC = 'expertise')
OR (TOPIC_ID = 2 AND TOPIC = 'expertise'))
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 4)
I would do based on JOIN conditions multiple times against each condition that you are "requiring". I would also ensure an index on the Profiles table based on the each part of the key looking for... (FK_User_ID, Topic_ID, Topic)
SELECT STRAIGHT_JOIN
U.ID
FROM Users U
JOIN Profiles P1
on U.ID = P1.FK_User_ID
AND P1.Topic_Id = 1
AND P1.Topic = "language"
JOIN Profiles P2
on U.ID = P2.FK_User_ID
AND P2.Topic_Id = 2
AND P2.Topic = "language"
JOIN Profiles P3
on U.ID = P3.FK_User_ID
AND P3.Topic_Id = 1
AND P3.Topic = "expertise"
JOIN Profiles P4
on U.ID = P4.FK_User_ID
AND P4.Topic_Id = 2
AND P4.Topic = "expertise"
WHERE
u.name like '%PAU%'
This way, any additional criteria as expressed in other answer provided shouldn't be too much an impact. The tables are setup by the criteria as if simultaneous, and if any are missing, they will be excluded from the result immediately instead of trying to do a sub-select counting for every entry (which I think might be the lag you are encountering).
So, each of your "required" criteria would take the same "JOIN" construct, and as you can see, I'm just incrementing the "alias" of the join instance.