I have a table of member-to-member connections. The schema is member_id, friend_id, is_active. I want to build a list of member connections of people who are friends of friends. I'm not really sure how to tackle the query, let alone in a semi-optimized way.
The table above works in a manner where the member_id and friend_id are essentially the same thing on another table. In my system, these id's are generally referred to as member_id except on this one table. For example, let's say my member_id is 21. My number can be on an infinite amount of other rows as either the member_id or the friend_id it's either based on who initiated the actual friendship request originally, that and I didn't want redundant data where I'd have dupe rows to basically do the same thing.
I'd like to have a query where I can not only establish a level of degree (think LinkedIn) but I can also establish how many mutual friends one person may have that's being displayed (think Facebook). The x factor here is the is_active column I mentioned earlier. This column could be 0 or 1. It's a simple tinyint column that acts as a on/off switch. Any friend connections with a 1 would be an active friendship whereas 0 is pending. I need to base this query off my active friends and their active friends and so on. Where none of the active friends my friends have are active friends of mine.
How can I construct a query like this (even if I can't show the level of separation and only get a mutual count)? Right now, I can sort of think of something but it involves query after query some nested in loops, and yea, I just can't picture that being anything good for my servers' overall performance or health over time.
Here's how to perform the search using a breadth-first, shortest path search, using JOIN. There is no magic in this algorithm, as we're using MySQL to find our answer, and we're not incorporating any fancy search algorithm that uses any kind of heuristics or optimization.
My 'friends' table has unidirectional relationships, so we do have duplicates in the sense that both '1 to 2' and '2 to 1' are stored. I'm also excluding is_active since the implementation will be obvious:
Here's the data:
member_id friend_id
1 2
1 3
1 4
2 1
2 3
2 5
2 6
3 2
3 1
4 1
5 2
6 2
6 7
7 6
7 8
8 7
We have member 1 selected, and we're asking is 1 friends with 7, a friend of a friend, etc? A count of 0 means no, and a count of 1 means yes.
SELECT COUNT(*)
FROM friends f1
WHERE f1.member_id = 1
AND f1.friend_id = 7
If no, then are they friend of a friend?
SELECT COUNT(*)
FROM friends f1
JOIN friends f2
ON f2.member_id = f1.friend_id
WHERE f1.member_id = 1
AND f2.friend_id = 7
If no, then friend of a friend of a friend?
SELECT COUNT(*)
FROM friends f1
JOIN friends f2
ON f2.member_id = f1.friend_id
JOIN friends f3
ON f3.member_id = f2.friend_id
WHERE f1.member_id = 1
AND f3.friend_id = 7
And so on...
The third query would find the path '1 to 2', '2 to 6', and '6 to 7', returning the count of 1.
Each query becomes more expensive (due to the larger number of joins), so you may want to limit the search at some point. One cool thing is that this search works from both ends toward the middle, which is one simple optimization suggested for shortest path searches.
Here's how to find those mutual friend recommendations for member 1:
SELECT f2.friend_id
FROM friends f1
JOIN friends f2
ON f2.member_id = f1.friend_id
LEFT JOIN friends f3
ON f3.member_id = f1.member_id
AND f3.friend_id = f2.friend_id
WHERE f1.member_id = 1
AND f2.friend_id <> f1.member_id // Not ourself
AND f3.friend_id IS NULL // Not already a friend
Without specifics of tables, I can offer the following guidance... If you run your query to ALWAYS put the LOWER ID in the first position, and do distinct (or even count to see how frequent common person is/may be to other parties), you would remove the bloat.
ex:
select
case when table.MemberID < table.FriendID
then table.MemberID else table.FriendID end as FirstPerson,
case when table.MemberID < table.FriendID
then table.FriendID else table.MemberID end as SecondPerson
from
...
where...
So, if your data has
member ID Friend ID
1 2
1 3
1 4
2 1
2 3
2 5
3 2
5 2
and you queried for friends / associations with member ID 1 you would start with
1 2
1 3
1 4
but then friendships from ID #2 would return
1 2 (reversal of 2 / 1 entry) would be duplicate
2 3
2 5
then from friendship 3
2 3 (reversal of 3 / 2 entry) would be duplicate
then from friendship 5 from member 2
2 5 (reversal of 5 / 2 entry) would be dupliate
Not sure this is exactly what you are looking for, but sounds similar to other "social network" finding friends/associations. As for how many "degrees" from a person's association/friendship, you would probably have to nest your queries, or at least keep querying from within some looping structure.
To further improve on the accepted answer, you can utilize coalesce to check each degree of separation until it's found. e.g:
SELECT COALESCE(
(SELECT 1
FROM friends f1
WHERE f1.member_id = 1
AND f1.friend_id = 7
LIMIT 1),
(SELECT 2
FROM friends f1
JOIN friends f2
ON f2.member_id = f1.friend_id
WHERE f1.member_id = 1
AND f2.friend_id = 7 LIMIT 1) /*, ..ETC*
) as degrees_away
Related
I am not entirely sure even how to name this post, because I do not know exactly how to ask it.
I have three tables. One with users, one with foods and one with the users rating of the foods, like such (simplified example):
Foods
id name type
---------------------
1 Apple fruit
2 Banana fruit
3 Steak meat
Users
id username
-----------------
1 Mark
2 Harrison
3 Carrie
Scores (fid = food id, uid = user id)
fid uid score
---------------------
1 1 3
1 2 5
2 1 2
3 2 3
Now, I have this query, which works perfectly:
SELECT fn.name as Food, ROUND(AVG(s.score),1) AS AvgScore FROM Foods fn LEFT JOIN Scores s ON fn.id = s.fid GROUP BY fn.id ORDER BY fn.name ASC
As you can tell, it lists all names from Foods including an average from all users ratings of the food.
I also want to add the unique users own score. (Assume that when Mark is logged in, his uid is set in a session variable or whatever)
I need the following output, if you are logged in as Mark:
Food AvgScore Your Score
Apple 4 3
I have made several attempts to make this happen, but I cannot find the solution. I have learned that if you have a question, it is very likely that someone else has asked it before you do, but I do not quite know how to phrase the question, so I get no answers when googling. A pointer in the right direction would be much appreciated.
You can try with case:
SELECT fn.name as Food,
ROUND(AVG(s.score),1) AS AvgScore,
sum(case s.uid = $uid when s.score else 0 end) as YourScore
FROM Foods fn
LEFT JOIN Scores s ON fn.id = s.fid
GROUP BY fn.id
ORDER BY fn.name ASC
$uid is variable off course.
I have four tables like this:
**USERS**
___________________________
user_ID username password
---------------------------
1 user1 1234
2 user2 5678
**TEAMS**
______________________________________
team_ID formation team_name user_ID
--------------------------------------
1 4-4-2 team1 1
2 4-3-3 team2 2
**PLAYERS**
____________________________________
player_ID name position rating
------------------------------------
1 Ronaldo LW 94
2 Messi RW 93
3 Hazard LW 90
**ACTIVE PLAYERS**
___________________________________
ID player_ID team_ID cardview_ID
-----------------------------------
1 1 2 9
2 3 1 7
3 2 1 3
Each user has a team with a formation and a team name. The "active players" tables references the player_ID with the team_ID to see which players are currently active on which teams.
Let's say that user1 logs in to the application, then I want to get all the players name, ratingand their cardview_ID. Something that should look like this:
_____________________________
name rating cardview_ID
-----------------------------
Hazard 90 7
Messi 94 3
These are the players that are currently active on user1's team which is team1.
How can I get this joined table? I have tried with an inner join but that didn't seem to do the work for me.
_______________________________ EDIT_____________________________________
This is the query that doesn't give the desired result:
SELECT players.name, players.rating, activeplayers.cardview_ID
FROM players
INNER JOIN
activeplayers
ON players.player_ID = usedplayers.player_ID
I also tried to join them on team_ID.
Assuming you have the logged in user's ID available, I think this will give you what you're asking for:
SELECT
[PLAYERS].name,
[PLAYERS].rating,
[ACTIVE PLAYERS].cardview_ID
FROM [TEAMS]
JOIN [ACTIVE PLAYERS]
ON [TEAMS].team_ID = [ACTIVE PLAYERS].team_id
JOIN [PLAYERS]
ON [PLAYERS].player_id = [ACTIVE PLAYERS].player_id
WHERE [TEAMS].user_id = <logged_in_user_id>
Please also note the questions asking for clarifying details, and also feel free to respond if this query gets you part of the way but you need more information. The content in angle brackets are of course a placeholder. I also don't know your exact table names so you may need to replace what is in the square brackets with the actual table names.
Assuming that the query in your post contains a typo and is actually this:
SELECT players.name, players.rating, activeplayers.cardview_ID
FROM players
INNER JOIN
activeplayers
ON players.player_ID = activeplayers.player_ID
This query will correctly return all the players who are active players. Now to limit it only to the team for User1, you need to add an additional join to the Teams table, same way you did the join above, and then add a WHERE clause that filters on the Teams.UserID.
That's it.
I have master and detail tables as described below (with representative data). I want to select (mysql-compliant) Student.id for students that have an "A" in both Biology and Chemistry.
Students grades
id name id student_id class grade
1 ken 1 1 Biology A
2 beth 2 1 Chemistry A
3 joe 3 1 Math B
4 2 Biology A
5 2 Chemistry A
6 2 Math A
7 3 Biology B
8 3 Chemistry A
9 3 Math A
Currently, I'm just pulling in all the data into my program (java) but figure there's got to be a way in SQL to get the right records.
The results I'm looking for from the data above would be 1 & 2 (ken and beth). I've tried a few variations using joins and inner selects but can't quite get it to work. My main problem seems to be I'm ANDing my detail records eg., ...where grades.class='Biology' and grades.grade='A'
I took a look at SQL select from header table where detail table rows have multiple values but that didn't quite get me where I need to be.
Assistance greatly appreciated.
Try this
select s.id from students as s
inner join grades as g on s.id=g.student_id
group by s.id
having max(case when g.class='Biology' and g.grade='A' then 1 else 0 end)=1
and
having max(case when g.class='Chemistry' and g.grade='A' then 1 else 0 end)=1
Here is one way:
select g.student_id
from grades g
where g.class in ('Biology', 'Chemistry') and g.grade = 'A'
group by g.student_id
having count(distinct class) = 2;
Notes:
A join is not necessary because the grades table has the student id.
The where clause only gets records where a student has an 'A' in either class.
The having guarantees that a student has an 'A' in both classes.
I should note that there is an alternative method that uses join but not group by:
select gb.student_id
from grades gb join
grades gc
on gb.student_id = gc.student_id and
gb.class = 'Biology' and gc.class = 'Chemistry' and
gb.grade = 'A' and gc.grade = 'A';
This works -- and the performance might even be better. I like the group by and having approach because it is more flexible.
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 7 years ago.
Improve this question
I have three tables in my database which are listed below:
Table: teams_info
team_id team_name entry_year status
1 team 1 2015 1
2 team 2 2015 1
3 team 2 2016 1
Table: team_players
player_id team_id status members_id position
1 1 0 1 1
2 1 1 2 2
3 1 1 3 3
4 1 1 4 4
5 2 0 5 1
6 2 0 6 2
7 2 1 7 3
Table: members
members_id first_name surname gender
1 joe blogg male
2 james smith male
3 sarah marshall female
4 tony walker male
5 peter jones male
6 jessica varley female
7 jane varley female
I'm trying to get my head around how I would get the following information,
1) How many team_players from team_info.entry_year = 2015 have accepted the to join a team (this will be all players with status = 1)
2) How many team_players from team_info.entry_year = 2015 have been invited (basically counting all records that belong to 2015 teams in table team_info)
3) The total Males players from 2015 teams.
4) The total female players from 2015 teams.
I'm fairly new to SQL and it seems I have made things complicated whilst trying to follow the best standards, however I can't wrap my head around where to start.
Are you using mysql or some other RDBMS?
I'll assume mysql here, as there are a variety of join syntaxes you can use and they vary by database.
What you want to do here is do an INNER join of all 3 tables which will give you one row per intersection.
Fortunately, you have a good basic normalized structure that has separated the TEAM entity from the MEMBERS (players).
Because your app supports "seasons" you have a good many-to-many resolver table in team_players.
So, to join the tables together, strictly speaking it doesn't really matter which table you start with, but in a case like this, I will start with the many to many resolution table:
SELECT *
FROM team_players as tp LEFT JOIN teams_info as ti ON (ti.team_id = tp.team_id) LEFT JOIN members as m ON (m.members_id = tp.members_id) At this point, you are inner joining and should have 1 row essentially for every match, which is going to be one row for every row in team_players.
In your examples, you want a count, so rather than SELECT , you just want COUNT() as 'some_name'. What you alias that name to be is up to you. For example you could have:
SELECT count(*) as accepted_in_2015
FROM team_players as tp
LEFT JOIN teams_info as ti ON (ti.team_id = tp.team_id)
LEFT JOIN members as m ON (m.members_id = tp.members_id)
Of course, what is missing now is the filtration. You can add these with a WHERE clause in most cases, but I typically add them in the appropriate join criteria. So when I'm adding a WHERE/filtration criteria that has to do with a team, I will put it in the ON that involves the teams_info join.
Where it's a 'member' table criteria (status in your first question) I add that there.
So to answer your first question, this should probably work:
1) How many team_players from team_info.entry_year = 2015
have accepted the to join a team (this will be all players with status
= 1)
SELECT count(*) as accepted_in_2015
FROM team_players as tp
LEFT JOIN teams_info as ti ON (ti.team_id = tp.team_id AND ti.entry_year = 2015)
LEFT JOIN members as m ON (m.members_id = tp.members_id AND m.status = 1)
The other questions are all variations on this same blue print and can be determined the same way, however, in the case of male/female I'd probably just group by and get counts for male vs/ female in one query.
I've been struggling on the following.
I have 3 tables: players, players_clothes, and teams_clothes.
Table players:
id user team_id
1 tom 4
2 robo 5
3 bob 4
So tom and bob are both on the same team
Table players_clothes:
id clothes_id p_id
1 13 1
2 35 3
3 45 3
Bob has clothing article 35 and 45, robo has none.
Table teams_clothes:
id clothes_id team_id
1 35 4
2 45 4
3 55 4
4 65 5
This shows which teams have rights to which articles of clothing. The problem: tom is wearing an article of clothing that does no belong to his team... Let's assume this is illegal.
I'm having trouble figuring out how to capture all those who are wearing illegal clothes for a particular team.
SELECT pc.clothes_id FROM players AS p
JOIN players_clothes AS pc
ON p.id = pc.p_id
AND p.team_id = 4 GROUP BY pc.clothes_id
(I group by players_clothes.clothes_id because believe it or not, two players can be assigned the same piece of clothing)
I think this results the following set (13, 35, 45)
Now I would like to check against the actual set of clothes that team 4 owns.
SELECT clothes_id FROM teams_clothes WHERE team_id = 4 and this return (35, 45, 55)
How can I create a query so that it returns (13)? I've tried things like NOT EXISTS IN but I think the GROUP BY players_clothes.clothes_id part gets in the way
I suggest
select * from A where team_id = $team_id join B on B.a_id = A.id
where not exists
(
select 1 from C where C.clothes_id = B.clothes_id and team_id = $team_id
)
Basically, we find all As who are on their team and for each A join to all clothing they wear, and then only return the row IF we can't find indication in table C that the clothing is on our team (this covers not existent in C and exists but in the wrong team on C)
This should do the trick:
SELECT b.a_id, b.clothes_id
FROM
b INNER JOIN a
ON b.a_id = a.id
LEFT OUTER JOIN c
ON a.team_id = c.team_id
WHERE
c.clothes_id = NULL
The thought is to do an outer join on the combination of tables A/B against table C. And then only look for the cases where c.clothes_id is NULL, which would represent those cases where there is no relational match on the outer join (i.e. the clothes item is not approved for that user's team).
Not sure if this is too late for you, but I'd change the database model itself to make this situation impossible in the first place:
("Unimportant" fields omitted for brevity, including surrogate keys such as PLAYER_ID.)
Note how TEAM_ID migrates through the identifying relationship from TEAM to PLAYER, and then to the PLAYER_ARTICLE, where it merges with the same field migrated through the TEAM_ARTICLE. Since there is only one physical TEAM_ID field in the PLAYER_ARTICLE table, you can never insert a row that would reference different teams.
To put it in more abstract terms: this is a diamond-shaped dependency, where TEAM is at the top and PLAYER_ARTICLE at the bottom of the diamond. The merger at the bottom (enabled by the usage of identifying relationships) ensures sides must always point to the same top.
Your example data would be represented like this...
PLAYER:
TEAM_ID PLAYER_NO
4 1 -- Tom
5 1 -- Robo
4 2 -- Bob
TEAM_ATRICLE:
TEAM_ID ARTICLE_ID
4 35
4 45
4 55
5 65
PLAYER_ARTICLE:
TEAM_ID PLAYER_NO ATRICLE_ID
4 1 13 -- Tom: this is impossible (FK violation).
4 2 35 -- Bob
4 2 45 -- Bob