Join one table with two other ones by id - sql

I am trying to join one table with two others that are unrelated to each other but are linked to the first one by an id
I have the following tables
create table groups(
id int,
name text
);
create table members(
id int,
groupid int,
name text
);
create table invites(
id int,
groupid int,
status int \\ 2 for accepted, 1 if it's pending
);
Then I inserted the following data
insert into groups (id, name) values(1,'group');
insert into members(id, groupid, name) values(1,1,'admin'),(1,1,'other');
insert into invites(id, groupid, status) values(1,1,2),(2,1,1),(3,1,1);
Obs:
The admin does not has an invite
The group has an approved invitation with status 2 (because the member 'other' joined)
The group has two pending invites with status 1
I am trying to do a query that gets the following result
groupid | name | inviteId
1 | admin | null
1 | other | null
1 | null | 2
1 | null | 3
I have tried the following querys with no luck
select g.id, m.name, i.id from groups g
left join members m ON m.groupid = g.id
left join invites i ON i.groupid = g.id and i.status = 1;
select g.id, m.name, i.id from groups g
join (select groupid, name from members) m ON m.groupid = g.id
join (select groupid, id from invites where status = 1) i ON i.groupid = g.id;
Any ideas of what I am doing wrong?

Because members and invites are not related, you need to use two separate queries and use UNION (automatically removes duplicates) or UNION ALL (keeps duplicates) to get the output you desire:
select g.id as groupid, m.name, null as inviteid from groups g
join members m ON m.groupid = g.id
union all
select g.id, null, i.id from groups g
join invites i ON (i.groupid = g.id and i.status = 1);
Output:
groupid | name | inviteid
---------+-------+----------
1 | admin |
1 | other |
1 | | 3
1 | | 2
(4 rows)
Without a UNION, your query implies that the tables have some sort of relationship, so the columns are joined side-by-side. Since you want to preserve the null values, implying that the tables are not related, you need to concatenate/join them vertically with UNION
Disclosure: I work for EnterpriseDB (EDB)

Related

Efficiently getting multiple counts of foreign key rows in PostgreSQL

I have a database that consists of users who can perform various actions, which I keep track of in multiple tables. I'm creating a point system, so I need to count how many of each type of action the user did. For example, if I had:
users posts comments shares
id | username id | user_id id | user_id id | user_id
------------- -------------- -------------- --------------
1 | abc 1 | 1 1 | 1 1 | 2
2 | xyz 2 | 1 2 | 2 2 | 2
I would want to return:
user_details
id | username | post_count | comment_count | share_count
---------------------------------------------------------
1 | abc | 2 | 1 | 0
2 | xyz | 0 | 1 | 2
This is slightly different from this question about foreign key counts since I want to return the individual counts per table.
What I've tried so far (example code):
SELECT
users.id,
users.username,
COUNT( DISTINCT posts.id ) as post_count,
COUNT( DISTINCT comments.id ) as comment_count,
COUNT( DISTINCT shares.id ) as share_count
FROM users
LEFT JOIN posts ON posts.user_id = users.id
LEFT JOIN comments ON comments.user_id = users.id
LEFT JOIN shares ON shares.user_id = users.id
GROUP BY users.id
While this works, I had to use DISTINCT in all of my counts because the LEFT JOINS were causing high numbers of duplicate rows. I feel like there must be a better way to do this since (please correct me if I'm wrong) on each LEFT JOIN, the DISTINCT is having to filter out an exponentially growing number of duplicated rows.
Thank you so much for any help you could give me with this!
You can join derived tables that already do the aggregation.
SELECT u.id,
u.username,
coalesce(pc.c, 0) AS post_count,
coalesce(cc.c, 0) AS comment_count,
coalesce(sc.c, 0) AS share_count
FROM users AS u
LEFT JOIN (SELECT p.user_id,
count(*) AS cc
FROM posts AS p
GROUP BY p.user_id) AS pc
ON pc.user_id = u.id
LEFT JOIN (SELECT c.user_id,
count(*) AS
FROM comments AS c
GROUP BY c.user_id) AS cc
ON cc.user_id = u.id
LEFT JOIN (SELECT s.user_id,
count(*) AS c
FROM shares AS s
GROUP BY s.user_id) AS sc
ON sc.user_id = u.id;

SQL Query : how to Select Maximum value of each group of joined tables

I have this problem of returning maximum AGE of players in these 2 tables I have, Table tblplayers (with 34 records) when this table is joined to another table called tblClubs (with 9 records).
tblPlayers fields are:
ID(Autonumber) | CLubID(Number) | Player Name(Text) | PlayerAge(Number)
tblClubs fields are:
ID(Autonumber) | ClubName (Text)
Now I need to show Names of players with maximum ages among other players in their own clubs and the club name beside that like this :
Club Name | Player Name | Maximum Age (older player of each club)
please tell me how can i make it?
You can solve this with window functions, if your database supports them:
select c.club_name, p.player_name, p.player_age
from clubs c
inner join (
select
p.*,
rank() over(partition by p.club_id order by p.player_age desc) rn
players p
) p on p.club_id = c.id and p.rn = 1
A common and quite portable alternative is to filter with a subquery:
select
c.club_name,
t.player_name,
t.player_age
from players p
inner join clubs c on c.id = p.club_id
where p.age = (select max(p1.age) from players p1 where p1.club_id = p.club_id)
SQL Fiddle
MS SQL Server 2017 Schema Setup:
CREATE TABLE tblPlayers (ID INT, ClubID INT,PlayerName VARCHAR(255)
,PlayerAge INT)
CREATE TABLE tblClubs (ID int,ClubName VARCHAR(255))
INSERT INTO tblPlayers(ID,ClubID,PlayerName
,PlayerAge) VALUES (1,1,'John',30)
,(2,1,'Mark',25)
,(3,1,'Albert',36)
,(4,2,'David',33)
,(5,2,'John',31)
INSERT INTO tblClubs(ID, ClubName) VALUES(1,'TEAM 1')
,(2,'TEAM 2')
Query 1:
SELECT
*
FROM
(SELECT A.*,RANK() OVER
(PARTITION BY A.ClubID ORDER BY A.PlayerAge desc) as rn
FROM tblPlayers A
INNER JOIN tblClubs B ON A.ClubID=B.ID) t
WHERE
t.rn = 1
Results:
| ID | ClubID | PlayerName | PlayerAge | rn |
|----|--------|------------|-----------|----|
| 3 | 1 | Albert | 36 | 1 |
| 4 | 2 | David | 33 | 1 |

PostgreSQL - How to remove duplicates when doing LEFT OUTER JOIN with WHERE clause?

I have 2 tables:
users table
+--------+---------+
| id | integer |
+--------+---------+
| phone | string |
+--------+---------+
| active | boolean |
+--------+---------+
statuses table
+---------+---------+
| id | integer |
+---------+---------+
| user_id | integer |
+---------+---------+
| step_1 | boolean |
+---------+---------+
| step_2 | boolean |
+---------+---------+
I'm doing LEFT OUTER JOIN statuses table on users table with WHERE clause like this:
SELECT users.id, statuses.step_1, statuses.step_2
FROM users
LEFT OUTER JOIN statuses ON users.id = statuses.user_id
WHERE (users.active='f')
ORDER BY users.id DESC
My problem
There are some users that have same phone number inside the users table and I want remove the duplicate users based on the phone number.
I don't want to delete them from database. But just want to exclude them for this query only.
For example, say John (ID: 1) and Sara (ID: 2) shared same phone number (+6012-3456789), removing one of them, either John or Sara is fine for me.
What I've tried but did not work?
First:
SELECT DISTINCT users.phone
FROM users
LEFT OUTER JOIN statuses ON users.id = statuses.user_id
WHERE (users.active='f')
ORDER BY users.id DESC
Second:
SELECT users.phone, COUNT(*)
FROM users
LEFT OUTER JOIN statuses ON users.id = statuses.user_id
WHERE (users.active='f')
GROUP BY phone
HAVING COUNT(users.phone) > 1
I would do this before doing the join. In Postgres, select distinct on is a very useful construct:
SELECT u.id, s.step_1, s.step_2
FROM (SELECT distinct on (phone) u.*
FROM users u
WHERE u.active = 'f'
ORDER BY phone
) u LEFT OUTER JOIN
statuses s
ON u.id = s.user_id
WHERE u.active = 'f'
ORDER BY u.id DESC;
distinct on returns one row for whatever is in parentheses. In this case, that would be by phone (based on "I want remove the duplicate users based on the phone number"). Then, the join should not be showing these as duplicates.
Here is one way
Self Join the users table and join using phone numbers and filter any one of the duplicate name by comparison operator.
SELECT *
FROM (SELECT u.*
FROM users u
JOIN users u1
ON u. u.phone = u1.phone -- to
AND u.name >= u1.name) u
LEFT OUTER JOIN statuses
ON users.id = statuses.user_id
WHERE ( users.active = 'f' )
or use ROW_NUMBER
Generate row number for each phone numbers and filter the first phone number with row number as 1
SELECT *
FROM (SELECT u.*,
Row_number()OVER(partition BY phone ORDER BY name) rn
FROM users u) u
LEFT OUTER JOIN statuses
ON users.id = statuses.user_id
WHERE ( users.active = 'f' )
AND rn = 1

Join tables with distinct highest ranked row

I have three tables defined like this:
[tbMember]
memberID | memberName
1 | John
2 | Peter
[tbGroup]
groupID | groupName
1 | Alpha
2 | Beta
3 | Gamma
[tbMemberGroupRelation]
memberID | groupID | memberRank (larger number is higher)
1 | 1 | 0
1 | 2 | 1
2 | 1 | 5
2 | 2 | 3
2 | 3 | 1
And now I want to perform a table-join selection to get result which contains (distinct) member with his highest ranked group in each row, for the given example above, the query result is desired to be:
memberID | memberName | groupName | memberRank
1 | John | Beta | 1
2 | Peter | Alpha | 5
Is there a way to implement it in a single SQL like following style ?
select * from tbMember m
left join tbMemberGroupRelation mg on (m.MemberID = mg.MemberID and ......)
left join tbGroup g on (mg.GroupID = g.GroupID)
Any other solutions are also appreciated if it is impossible to write in a simple query.
========= UPDATED =========
Only ONE highest rank is allowed in table
One solution would be to create an inverted sequence/rank of the memberRank so that the highest rank per member is always equal to 1.
This is how I achieved it using a sub-query:
SELECT
m.memberID,
m.memberName,
g.groupName,
mg.memberRank
FROM
tbMember m
LEFT JOIN
(
SELECT
memberID,
groupID,
groupName,
memberRank,
RANK() OVER(PARTITION BY memberID ORDER BY memberRank DESC) AS invRank
FROM
tbMemberGroupRelation
) mg
ON (mg.memberID = m.memberID)
AND (mg.invRank = 1)
LEFT JOIN
tbGroup g
ON (g.groupID = mg.groupID);
An alternative method:
SELECT
M.memberID,
M.memberName,
G.groupName,
MG.memberRank
FROM
Member M
LEFT OUTER JOIN MemberGroup MG ON MG.memberID = M.memberID
LEFT OUTER JOIN MemberGroup MG2 ON
MG2.memberID = M.memberID AND
MG2.memberRank > MG.memberRank
INNER JOIN [Group] G ON G.groupid = MG.groupid
WHERE
MG2.memberid IS NULL
Might perform better in some situations due to indexing, etc.
create table [tbGroup] (groupid int, groupname varchar(8000))
Insert [tbGroup] Values (1, 'Alpha')
Insert [tbGroup] Values (2, 'Beta')
Insert [tbGroup] Values (3, 'Gamma')
create table [tbMemberGroupRelation] (memberid int, groupid int, memberrank int)
Insert [tbMemberGroupRelation] Values (1,1,0)
Insert [tbMemberGroupRelation] Values (1,2,1)
Insert [tbMemberGroupRelation] Values (2,1,5)
Insert [tbMemberGroupRelation] Values (2,2,3)
Insert [tbMemberGroupRelation] Values (2,3,1)
;With cteMemberGroupRelation As
(
Select *, Row_Number() Over (Partition By MemberID Order By MemberRank Desc) SortOrder
From [tbMemberGroupRelation]
)
Select *
From tbMember M
Join (Select * From cteMemberGroupRelation Where SortOrder = 1) R On R.memberid = M.memberid
Join tbGroup G On G.groupid = R.groupid

How to SQL in many-to-many relationship

I want to display how many hobbies does john have. Could you tell me how to write SQL statement?
PERSON table
ID | NAME
1 | John
HOBBY table
ID | NAME
1 | music
2 | sport
PERSON_HOBBY_COMBINATION table
ID | PERSON_ID | HOBBY_ID
1 | 1 | 1
expected result
HOBBY_NAME | HOBBY_EXIST
music | YES
sport | NO
This might work for you:
SELECT h.name,
CASE WHEN ph.id IS NULL THEN 'No' ELSE 'Yes' END AS hobby_exist
FROM hobby h
CROSS JOIN person p
LEFT JOIN person_hobby_conbination ph ON (ph.p_id = p.id AND ph.h_id = h.id)
WHERE (p.name = 'John')
Select NAME, count(*) AS NoHobbies from Person p
inner join PERSON_HOBBY_COMBINATION phc
on p.ID = phc.PERSON_ID
group by p.ID, p.NAME
Note that you should group on both ID and NAME of the person.
You need to group on NAME because you have it in the output, but if you have duplicate names grouping on NAME will sum several persons hobbies together, that is why you need to group on ID too.
Edit
When inspection your expected result you don't want how many hobbies John has, but which hobbies. Then you need to write
Select p.NAME as PersonName, h.Name as HobbyName, case when phc.ID is null then 'No' else 'Yes' end as HasHobby from Person p
inner join Hobby h
on 1 = 1
left outer join dbo.PersonHobbyCombination phc
on p.ID = phc.PersonID and h.ID = phc.HobbyID
If you already have John's ID and just want a raw count, then this should work.
select count(*) from person_hobby_combination where person_id=?