Self join many-to-many relationship - sql

From sample data below, assuming Julie (1) has friends Adam, David, John (2, 3, 4).
Adam (2) has friends Julie, David, John (1, 3, 4).
ID Name
1 Julie
2 Adam
3 David
4 John
5 Sam
This make a self join and many-to-many relationship within ONE table.
In addition to the above problem, say Julie (1) added Sam (5) as friends, technically and practically speaking, Sam (5) is now friend of Julie (1) as well. This make things more complicated because the relationship bi-directional.
So I'm wondering:
How do I design the database?
How do I make a query that will return all friends of every users?
Thank you!

Example Data:
PEOPLE
PERS_ID PERS_NAME
1 Julie
2 Adam
3 David
4 John
5 Sam
FRIENDSHIPS
PERS_ID FRIEND_ID
1 2
1 3
1 4
2 3
2 4
Query:
select people.pers_id as person,
people.pers_name as person_name,
peoplef.pers_id as friend_id,
peoplef.pers_name as friend_name
from people
join friendships
on people.pers_id = friendships.pers_id
or people.pers_id = friendships.friend_id
join people peoplef
on (peoplef.pers_id = friendships.pers_id and
peoplef.pers_id <> people.pers_id)
or (peoplef.pers_id = friendships.friend_id and
peoplef.pers_id <> people.pers_id)
order by 2, 4
SQL Fiddle demo: http://sqlfiddle.com/#!2/97b41/6/0
This will work regardless of whether or not you record both directions on the friendships table.

Pretty much agree with the others. You need a link table. I'll give a bit more detail.. some examples of keys and indexes and the query you wanted (bi-directional).
CREATE TABLE dbo.tblUser
(
ID int identity(0,1),
name varchar(20)
CONSTRAINT PK_tblUser PRIMARY KEY (ID)
)
-- Many to many link table with FKs
CREATE TABLE dbo.tblFriend
(
ID1 int not null constraint FK_tblUser_ID1 foreign key references dbo.tblUser(ID),
ID2 int not null constraint FK_tblUser_ID2 foreign key references dbo.tblUser(ID)
CONSTRAINT PK_tblFriend PRIMARY KEY (ID1, ID2)
)
-- Add index (So you can get an index seek if using ID2)
CREATE INDEX IX_tblFriend_ID2 ON dbo.tblFriend (ID2)
-- Test data
INSERT INTO dbo.tblUser(name)
VALUES ('Julie'),('Adam'),('David'),('John'),('Sam');
Insert INTO dbo.tblFriend (ID1, ID2)
values(0, 1),(2, 0)
-- Get bi-directional friend to friend relationships
SELECT U1.Name as 'User1', U2.Name as 'User2' FROM dbo.tblFriend F
INNER JOIN dbo.tblUser U1 ON U1.ID = F.ID1
INNER JOIN dbo.tblUser U2 ON U2.ID = F.ID2
UNION
SELECT U2.Name as 'User1', U1.Name as 'User2' FROM dbo.tblFriend F
INNER JOIN dbo.tblUser U1 ON U1.ID = F.ID1
INNER JOIN dbo.tblUser U2 ON U2.ID = F.ID2
ORDER BY User1, User2

One approach could be that you create second table that stores the person and friend ids. In this scenario, consider the following tables.
CREATE TABLE User
(
id int auto_increment primary key,
name varchar(20)
);
CREATE TABLE Friend
(
user_id int ,
friend_id int
);
INSERT INTO User
(name)
VALUES
('Julie'),
('Adam'),
('David'),
('John'),
('Sam');
Insert INTO Friend
(user_id, friend_id)
values(1, 5),
(3, 1);
Now the Friend table will store the user_id and his/her friend_id. For getting the list of friends for a particular user, you can search the id matching in either of these two columns. Below are sample queries.
-- Get Friends of Julie
select 1 AS user_id, IF(user_id = 1, friend_id, user_id) AS friend_id
FROM Friend
WHERE user_id=1 OR friend_id=1;
-- Get Friends of David
select 3 AS user_id, IF(user_id = 3, friend_id, user_id) AS friend_id
FROM Friend
WHERE user_id=3 OR friend_id=3
I hope you get idea with this and can play around.

I tried whatever you written in your query:
declare #table table
(
id int,
name varchar(40)
)
insert into #table values
(1, 'Julie'),
(2, 'Adam'),
(3, 'David'),
(4, 'John'),
(5, 'Sam')
select
t1.name ,
t2.name as friend
from #table t1, #table t2 where t1.id <> t2.id
and t1.id in (1,2) and t2.id <> 5
order by t1.id

Related

SQL to Select only full groups of data

Let's say I have three sample tables for groups of people as shown below.
Table users:
id
name
available
1
John
true
2
Nick
true
3
Sam
false
Table groups:
id
name
1
study
2
games
Table group_users:
group_id
user_id
role
1
1
teach
1
2
stdnt
1
3
stdnt
2
1
tank
2
2
heal
And I need to show to a user all groups that he participates in and also available right now, which means all users in that group have users.available = true.
I tried something like:
SELECT `groups`.*, `users`.* , `group_users`.*
FROM `groups`
LEFT JOIN `group_users` ON `groups`.`id` = `group_users`.`group_id`
LEFT JOIN `users` ON `users`.`id` = `group_users`.`user_id`
WHERE `users`.`available` = true AND `users`.`id` = 1
But it just shows groups and part of their users, that are available. And I need to have ONLY the groups that have all their users available.
If I were to find all available groups as User 1 - I should get only group 2 and it's users. How to do this the right way?
Tables DDL:
CREATE TABLE users (
id int PRIMARY KEY,
name varchar(256) NOT NULL,
available bool
);
CREATE TABLE teams (
id int PRIMARY KEY,
name varchar(256) NOT NULL
);
CREATE TABLE team_users (
team_id int NOT NULL,
user_id int NOT NULL,
role varchar(64)
);
INSERT INTO users VALUES
(1, 'John', true ),
(2, 'Nick', true ),
(3, 'Sam' , false);
INSERT INTO teams VALUES
(1, 'study'),
(2, 'games');
INSERT INTO team_users VALUES
(1, 1, 'teach'),
(1, 2, 'stdnt'),
(1, 3, 'stdnt'),
(2, 1, 'tank' ),
(2, 2, 'heal' );
mySQL select version() output:
10.8.3-MariaDB-1:10.8.3+maria~jammy
Check do you need in this:
WITH cte AS (
SELECT users.name username,
teams.id teamid,
teams.name teamname,
SUM(NOT users.available) OVER (PARTITION BY teams.id) non_availabe_present,
SUM(users.name = #user_name) OVER (PARTITION BY teams.id) needed_user_present
FROM team_users
JOIN users ON team_users.user_id = users.id
JOIN teams ON team_users.team_id = teams.id
)
SELECT username, teamid, teamname
FROM cte
WHERE needed_user_present
AND NOT non_availabe_present;
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=605cf10d147fd904fb2d4a6cd5968302
PS. I use user name as a criteria, you may edit and use user's identifier, of course.
Join the tables and aggregate with the conditions in the HAVING clause:
SELECT t.id, t.name
FROM teams t
INNER JOIN team_users tu ON t.id = tu.team_id
INNER JOIN users u ON u.id = tu.user_id
GROUP BY t.id
HAVING MIN(u.available) AND SUM(u.id = 1);
The HAVING clause is a simplification of:
HAVING MIN(u.available) = true AND SUM(u.id = 1) > 0
See the demo.
first you need to find those group which users is available. then find the all the group details of those group which is not related to those group which user is available.
SELECT * FROM team_users a
JOIN teams b ON a.team_id=b.id
JOIN users c ON a.user_id=c.id
WHERE NOT EXISTS
(
SELECT 1 FROM team_users tu
JOIN users u ON tu.user_id=u.id AND u.available =1
WHERE tu.team_id=a.Team_id
)

Get room members, room's owner and admin at the same time in one query with grouped by id (unique) on PostgreSQL 12

I want to get room's member list, room's owner member in case of he doesn't exists in other table and admin member at the same time. Currently i fetch them individually.
CREATE TABLE public.room_members (
id bigint NOT NULL,
member_id bigint,
room_id bigint,
group_id bigint
);
CREATE TABLE public.rooms (
id bigint NOT NULL,
member_id bigint,
group_id bigint,
name varchar(128)
);
CREATE TABLE public.members (
id bigint NOT NULL,
group_id bigint,
username varchar(128),
is_admin bool default false
);
CREATE TABLE public.groups (
id bigint NOT NULL,
name varchar(128)
);
-- My Group created
INSERT INTO "groups" (id, name) VALUES (1, 'My Group');
-- Create users for this group. We have 4 users/members
INSERT INTO "members" (id, group_id, username, is_admin) VALUES (1, 1, 'Pratha', true);
INSERT INTO "members" (id, group_id, username) VALUES (2, 1, 'John');
INSERT INTO "members" (id, group_id, username) VALUES (3, 1, 'Mike');
INSERT INTO "members" (id, group_id, username) VALUES (4, 1, 'April');
-- April creates a room and he is owner of this room
INSERT INTO "rooms" (id, group_id, member_id, name) VALUES (1, 1, 4, 'My Room'); -- 4 is April
-- April also adds Mike to the room members. But she does not add herself. As she is owner.
INSERT INTO "room_members" (id, group_id, room_id, member_id) VALUES (1, 1, 1, 3); -- 3 is Mike
What I want is:
room_members list of 'My Room' (Which is only Mike at the moment)
My Room's owner in case of he didn't add himself to room_members table. Because he is the owner of that room (Which is April)
Plus, admin member (Which is Pratha)
And this should be unique. For example, if user add himself to room_members and also owner then it should fetch member one time only.
What I tried so far?
select * from members
left outer join room_members cm on cm.member_id = members.id
left outer join rooms c on c.id = cm.room_id
where c.name = 'My Room' or members.id = 1
I couldn't use group by here either. Also i don't need the all fields. Just room_members table fields only.
See here: https://rextester.com/XWDS42043
Expected output for room_members:
+-------------+------------+------------+
| member_id | group_id | username |
+-------------+------------+------------+
| 1 | 1 | Pratha |
+-------------+------------+------------+
| 3 | 1 | Mike |
+-------------+------------+------------+
| 4 | 1 | April |
+-------------+------------+------------+
Pratha: Because he is ADMIN
Mike: Because he is member of My Room. MEMBER
April: Because she created that room. OWNER
room_members can be many. I just added only Mike but it can have multiple members including admins and owners.
You can address this with UNION:
-- list the admin(s) of the room group
select m.id, m.group_id, m.username
from rooms r
inner join members m on m.group_id = r.group_id and m.is_admin = true
where r.name = 'My Room'
union
-- list the members of the room
select m.id, m.group_id, m.username
from rooms r
inner join room_members rm on r.id = rm.room_id
inner join members m on rm.member_id = m.id
where r.name = 'My Room'
union
-- recover the room owner
select m.id, m.group_id, m.username
from rooms r
inner join members m on r.member_id = m.id
where r.name = 'My Room'
UNION eliminates duplicates accross queries, so if a user is both member and/or group admin and/or owner of the room, they will only appear once.
In your fiddle, this query returns:
id group_id username
1 4 1 April
2 3 1 Mike
3 1 1 Pratha

SQL Join Table as JSON data

I am trying to join reviews and likes onto products, but it seems, for some reason that the output of "reviews" column is duplicated by the length of another foreign table, likes, the output length of "reviews" is
amount of likes * amount of reviews
I have no idea why this is happening
My desired output is that the "reviews" column contains an array of JSON data such that one array is equal to one row of a related review
Products
Title Image
----------------------
Photo photo.jpg
Book book.jpg
Table table.jpg
Users
Username
--------
Admin
John
Jane
Product Likes
product_id user_id
---------------------
1 1
1 2
2 1
2 3
Product Reviews
product_id user_id review
-------------------------------------
1 1 Great Product!
1 2 Looks Great
2 1 Could be better
This is the query
SELECT "products".*,
array_to_json(array_agg("product_review".*)) as reviews,
EXISTS(SELECT * FROM product_like lk
JOIN users u ON u.id = "lk"."user_id" WHERE u.id = 4
AND "lk"."product_id" = products.id) AS liked,
COUNT("product_like"."product_id") AS totalLikes from "products"
LEFT JOIN "product_review" on "product_review"."product_id" = "products"."id"
LEFT JOIN "product_like" on "product_like"."product_id" = "products"."id"
group by "products"."id"
Query to create schema and insert data
CREATE TABLE products
(id SERIAL, title varchar(50), image varchar(50), PRIMARY KEY(id))
;
CREATE TABLE users
(id SERIAL, username varchar(50), PRIMARY KEY(id))
;
INSERT INTO products
(title,image)
VALUES
('Photo', 'photo.jpg'),
('Book', 'book.jpg'),
('Table', 'table.jpg')
;
INSERT INTO users
(username)
VALUES
('Admin'),
('John'),
('Jane')
;
CREATE TABLE product_review
(id SERIAL, product_id int NOT NULL, user_id int NOT NULL, review varchar(50), PRIMARY KEY(id), FOREIGN KEY (product_id) references products, FOREIGN KEY (user_id) references users)
;
INSERT INTO product_review
(product_id, user_id, review)
VALUES
(1, 1, 'Great Product!'),
(1, 2, 'Looks Great'),
(2, 1, 'Could be better')
;
CREATE TABLE product_like
(id SERIAL, product_id int NOT NULL, user_id int NOT NULL, PRIMARY KEY(id), FOREIGN KEY (product_id) references products, FOREIGN KEY (user_id) references users)
;
INSERT INTO product_like
(product_id, user_id)
VALUES
(1, 1),
(1, 2),
(2, 1),
(2, 3)
fiddle with the schema and query:
http://sqlfiddle.com/#!15/dff2c/1
Thanks in advance
The reason you are getting multiple results is because of the one-to-many relationships between product_id and product_review and product_like causing duplication of rows prior to aggregation. To work around that, you need to perform the aggregation of those tables in subqueries and join the derived tables instead:
SELECT "products".*,
"pr"."reviews",
EXISTS(SELECT * FROM product_like lk
JOIN users u ON u.id = "lk"."user_id" WHERE u.id = 4
AND "lk"."product_id" = products.id) AS liked,
COALESCE("pl"."totalLikes", 0) AS totalLikes
FROM "products"
LEFT JOIN (SELECT product_id, array_to_json(array_agg("product_review".*)) AS reviews
FROM "product_review"
GROUP BY product_id) "pr" on "pr"."product_id" = "products"."id"
LEFT JOIN (SELECT product_id, COUNT(*) AS "totalLikes"
FROM "product_like"
GROUP BY product_id) "pl" on "pl"."product_id" = "products"."id"
Output:
id title image reviews liked totallikes
1 Photo photo.jpg [{"id":1,"product_id":1,"user_id":1,"review":"Great Product!"},{"id":2,"product_id":1,"user_id":2,"review":"Looks Great"}] f 2
2 Book book.jpg [{"id":3,"product_id":2,"user_id":1,"review":"Could be better"}] f 2
3 Table table.jpg f 0
Demo on dbfiddle

Inner join an inner join with another inner join

I'm wondering if it is possible to inner join an inner join with another inner join.
I have a database of 3 tables:
people
athletes
coaches
Every athlete or coach must exist in the people table, but there are some people who are neither coaches nor athletes.
What I am trying to do is find a list of people who are active (meaning play or coach) in at least 3 different sports. The definition of active is they are either coaches, athletes or both a coach and an athlete for that sport.
The person table would consist of (id, name, height)
the athlete table would be (id, sport)
the coaching table would be (id, sport)
I have created 3 inner joins which tell me who is both a coach and and an athlete, who is just a coach and who is just an athlete.
This is done via inner joins.
For example,
1) who is both a coach and an athlete
select
person.id,
person.name,
coach.sport as 'Coaches and plays this sport'
from coach
inner join athlete
on coach.id = athlete.id
and coach.sport = athlete.sport
inner join person
on athlete.id = person.id
That brings up a list of everyone who both coaches and plays the same sport.
2) To find out who only coaches sports, I have used inner joins as below:
select
person.id,
person.name,
coach.sport as 'Coaches this sport'
from coach
inner join person
on coach.id = person.id
3) Then to find out who only plays sports, I've got the same as 2) but just tweaked the words
select
person.id,
person.name,
athlete.sport as 'Plays this sport'
from athlete
inner join person
on athlete.id = person.id
The end result is now I've got:
1) persons who both play and coach the same sport
2) persons who coach a sport
3) persons who play a sport
What I would like to know is how to find a list of people who play or coach at least 3 different sports? I can't figure it out because if someone plays and coaches a sport like hockey in table 1, then I don't want to count them in table 2 and 3.
I tried using these 3 inner joins to make a massive join table so that I could pick the distinct values but it is not working.
Is there an easier way to go about this without making sub-sub-queries?
What I would like to know is how to find a list of people who play /
coach at least 3 different sports? I can't figure it out because if
someone plays and coaches a sport like hockey in table 1, then I don't
want to count them in table 2 and 3.
you can do something like this
select p.id,min(p.name) name
from
person p inner join
(
select id,sport from athlete
union
select id,sport from coach
)
ca
on ca.id=p.id
group by p.id
having count(ca.sport)>2
CREATE TABLE #person (Id INT, Name VARCHAR(50));
CREATE TABLE #athlete (Id INT, Sport VARCHAR(50));
CREATE TABLE #coach (Id INT, Sport VARCHAR(50));
INSERT INTO #person (Id, Name) VALUES(1, 'Bob');
INSERT INTO #person (Id, Name) VALUES(2, 'Carol');
INSERT INTO #person (Id, Name) VALUES(2, 'Sam');
INSERT INTO #athlete (Id, Sport) VALUES(1, 'Golf');
INSERT INTO #athlete (Id, Sport) VALUES(1, 'Football');
INSERT INTO #coach (Id, Sport) VALUES(1, 'Tennis');
INSERT INTO #athlete (Id, Sport) VALUES(2, 'Tennis');
INSERT INTO #coach (Id, Sport) VALUES(2, 'Tennis');
INSERT INTO #athlete (Id, Sport) VALUES(2, 'Swimming');
-- so Bob has 3 sports, Carol has only 2 (she both coaches and plays Tennis)
SELECT p.Id, p.Name
FROM
(
SELECT Id, Sport
FROM #athlete
UNION -- this has an implicit "distinct"
SELECT Id, Sport
FROM #coach
) a
INNER JOIN #person p ON a.Id = p.Id
GROUP BY p.Id, p.Name
HAVING COUNT(*) >= 3
-- returns 1, Bob
I have created a SQL with some test data - should work in your case:
Connecting the two results in the subselect with UNION:
UNION will return just non-duplicate values. So every sport will be just counted once.
Finally just grouping the resultset by person.Person_id and person.name.
Due to the HAVING clause, just persons with 3 or more sports will be returned-
CREATE TABLE person
(
Person_id int
,name varchar(50)
,height int
)
CREATE TABLE coach
(
id int
,sport varchar(50)
)
CREATE TABLE athlete
(
id int
,sport varchar(50)
)
INSERT INTO person VALUES
(1,'John', 130),
(2,'Jack', 150),
(3,'William', 170),
(4,'Averel', 190),
(5,'Lucky Luke', 180),
(6,'Jolly Jumper', 250),
(7,'Rantanplan ', 90)
INSERT INTO coach VALUES
(1,'Football'),
(1,'Hockey'),
(1,'Skiing'),
(2,'Tennis'),
(2,'Curling'),
(4,'Tennis'),
(5,'Volleyball')
INSERT INTO athlete VALUES
(1,'Football'),
(1,'Hockey'),
(2,'Tennis'),
(2,'Volleyball'),
(2,'Hockey'),
(4,'Tennis'),
(5,'Volleyball'),
(3,'Tennis'),
(6,'Volleyball'),
(6,'Tennis'),
(6,'Hockey'),
(6,'Football'),
(6,'Cricket')
SELECT person.Person_id
,person.name
FROM person
INNER JOIN (
SELECT id
,sport
FROM athlete
UNION
SELECT id
,sport
FROM coach
) sports
ON sports.id = person.Person_id
GROUP BY person.Person_id
,person.name
HAVING COUNT(*) >= 3
ORDER BY Person_id
The coaches & athletes, ie people who are coaches or athletes, are relevant to your answer. That is union (rows in one or another), not (inner) join rows in one and another). (Although outer join involves a union, so there is a complicated way to use it here.) But there's no point in getting that by unioning only-coaches, only-athletes & coach-athletes.
Idiomatic is to group & count the union of Athletes & Coaches.
select id
from (select * from Athletes union select * from Coaches) as u
group by id
having COUNT(*) >= 3
Alternatively, you want ids of people who coach or play a 1st sport and coach or play a 2nd sport and coach or play a 3rd sport where the sports are all different.
with u as (select * from Athletes union select * from Coaches)
select u1.id
from u u1
join u u2 on u1.id = u2.id
join u u3 on u2.id = u3.id
where u1.sport <> u2.sport and u2.sport <> u3.sport and u1.sport <> u3.sport
If you wanted names you would join that with People.
Is there any rule of thumb to construct SQL query from a human-readable description?](https://stackoverflow.com/a/33952141/3404097)

MySQL: Finding users with similar interests

I have 2 tables in my SQL db:
Users: id | email | religion | politics
Interests: id | user_id | interest_name
Given a user1 id, what is the best way to find a second user with at least 1 matching interest? Also, note that the religion/politics in the Users table should also be used for this match.
Any help appreciated,
- Andy
select * from users
where id in (
select id from interests where interest_name in
( select interest_name from interests where id = :current_user_id ))
(PostgreSQL)
I won't argue with your decision to elevate religion and politics above ordinary interests. (That doesn't mean it's a good idea; it just means I won't argue with you about it.)
create table users (
user_id integer primary key,
email_addr varchar(35) not null,
current_religion varchar(35) not null default 'None',
current_politics varchar(35) not null default 'None'
);
insert into users values
(1, 'user#userdomain.com', 'Muslim', 'Liberal'),
(2, 'user#differentdomain.com', 'Muslim', 'Conservative'),
(3, 'user#yadn.com', 'Christian', 'Liberal');
create table interests (
user_id integer not null references users (user_id),
user_interest varchar(20) not null,
primary key (user_id, user_interest));
insert into interests values
(1, 'Walks on the beach'),
(1, 'Women'),
(1, 'Polar bears'),
(2, 'Walks on the beach'),
(2, 'Women'),
(2, 'Little Big Man'),
(3, 'Running on the beach'),
(3, 'Coffee'),
(3, 'Polar bears');
-- Given one user id (1), find a different user with at least
-- one matching interest. You can do this without referring
-- to the users table at all.
select t1.user_id, t1.user_interest, t2.user_id
from interests t1
inner join interests t2 on (t2.user_interest = t1.user_interest)
where t1.user_id = 1 and t2.user_id <> 1;
Returns
1 Walks on the beach 2
1 Women 2
1 Polar bears 3
To also match on, say, religion, you can do essentially the same thing with the table "users".
select t1.user_id, t1.current_religion as interest, t2.user_id
from users t1
inner join users t2 on (t1.current_religion = t2.current_religion)
where t1.user_id = 1 and t2.user_id <> 1
Returns
1 Muslim 2
You can exploit the similar structure to bring religious interests and ordinary interests together using UNION.
select t1.user_id, t1.current_religion as interest, t2.user_id
from users t1
inner join users t2 on (t1.current_religion = t2.current_religion)
where t1.user_id = 1 and t2.user_id <> 1
union
select t1.*, t2.user_id
from interests t1
inner join interests t2 on (t2.user_interest = t1.user_interest)
where t1.user_id = 1 and t2.user_id <> 1;
Returns
1 Walks on the beach 2
1 Women 2
1 Polar bears 3
1 Muslim 2