sql: many to many relationship join - sql

I'm very new to SQL so if there are multiple possibilities I'd like to see them all (and hear which possibilities are better than others). I'm using sqlite3.
I have the following 3 tables: user, channel, subscriptions
user:
user_id name
1 Johnny
2 Stacy
3 Allana
channel:
channel_id channel_name
1 ESPN
2 Disney
subscriptions:
user_id channel_id
1 1
2 2
3 1
3 2
What SQL command do I need to perform to get the following table? I basically want to see who is subscribed to which channels by names (so exactly what's laid out in the subscriptions table but mapping numbers to names based on the other tables).
user_id channel_id
Johnny ESPN
Stacy Disney
Allana ESPN
Allana Disney
I've tried the following but I'm getting nothing in the return statement:
select user.name, channel.channel_name from user, channel, subs where user.user_id == subs.user_id and channel.channel_id == subs.channel_id

Try this out and let me know in case you face any difficulty.
select a.name,c.channel_name
from
user a
left join
subscriptions b
on a.user_id = b.user_id
left join
channel c
on b.channel_id = c.channel_id;
or (in the format u asked in comments)
select u.name,c.channel_name
from
user u
left join
subscriptions s
on u.user_id = s.user_id
left join
channel c
on s.channel_id = c.channel_id;

Haven't tested it but try this:
select
u.name
,c.channel
from
user_id u
inner join subscriptions s
on u.user_id=s.user_id
inner join channel c
on s.channel_id=c.channel_id

Related

Unify columns from different tables while selecting distinct rows

Tables
User
id
name
email
is_active
1
john
john#albert.com
FALSE
2
mike
mike#ss.com
TRUE
3
monica
monica#dunno.com
TRUE
4
joey
joey#as.com
FALSE
5
ross
ross#boss.com
FALSE
Subscriptions
id
house_id
plan name
status
1
1
A banana a month
inactive
2
2
An apple a month
active
3
3
A pear a month
active
House
id
name
1
John's House
2
Mike's House
3
Monica's House
4
Joey's House
5
Ross's House
House_Contact (legacy table)
id
house_id
is_primary
1
1
TRUE
2
2
FALSE
2
3
TRUE
House_User (new table)
id
house_id
is_owner
user_id
1
2
FALSE
2
2
4
FALSE
4
3
5
FALSE
5
Expected Results
The resulting table should include the following:
Does the user have a subscription regardless of status? If so, include, if not, disregard.
Get email & is_active from User table (if they have subscription)
Get is_primary OR is_owner (if they have a subscription)
Results should be distinct (no duplicate users)
house_id
email
is_owner
is_active
1
john#albert.com
TRUE
FALSE
2
mike#ss.com
FALSE
TRUE
3
monica#dunno.com
TRUE
TRUE
What I tried
SELECT
u.email AS "email",
u.is_active AS "is_active",
h.id AS "house_id",
is_owner
FROM
house c
INNER JOIN (
SELECT
house_id,
user_id
FROM
house_user) hu ON h.id = hu.house_id
INNER JOIN (
SELECT
id,
email,
is_active
FROM
USER) u ON hu.user_id = u.id
INNER JOIN (
SELECT
id,
email,
is_primary
FROM
house_contact) hc ON u.email = ch.email
INNER JOIN (
SELECT
house_id,
is_primary is_owner
FROM
house_contact
UNION
SELECT
house_id,
is_owner is_owner
FROM
house_user) t ON u.id = t.house_id)
ORDER BY
u.email
Results are half than if I remove the INNER JOIN with UNION statement. No idea how to proceed.
I'm particularly confused with unifying the column and the possible duplication.
My educated guess:
SELECT DISTINCT ON (u.id)
u.id, u.email, u.is_active, h.house_id, h.is_primary
FROM "user" u
LEFT JOIN (
SELECT hu.user_id, hu.house_id
, GREATEST(hc.is_primary, hu.is_owner) AS is_primary
FROM house_user hu
LEFT JOIN house_contact hc USING (house_id)
WHERE EXISTS (SELECT FROM subscription WHERE house_id = hu.house_id)
) h ON h.user_id = u.id
ORDER BY u.id, h.is_primary DESC NULLS LAST, h.house_id;
We don't need table house in the query at all.
I see three possible sources of conflict:
house_contact.is_primary vs. house_user.is_owner. Both seem to mean the same. The DB design is broken in this respect. Taking GREATEST() of both, which means true if either is true.
We don't care about subscription.status, so just make sure the house has at least one subscription of any kind with EXISTS, thereby avoiding possible duplicates a priori.
A user can live in multiple houses. We want only one row per user. So show the first house with is_primary (the one with the smallest house_id) if any. If there is no house, there is also no subscription. But the outer LEFT JOIN keeps the user in the result. Change to JOIN to skip users without subscription.
About DISTINCT ON:
Select first row in each GROUP BY group?
About sorting boolean values:
Sorting null values after all others, except special
Sort NULL values to the end of a table
You can use the joins as follows:
Select distinct hu.house_id, u.email, hu.is_owner, hc.is_primary
From user u join house_user hu on u.id = hu.user_id
Join subscriptions s on s.house_id = hu.house_id
Join house_contract hc on hc.house_id = s.house_id;
I have used distinct to remove duplicates if you have multiple data in the table for matching condition. You can remove it if not required in case it is not required.
From what I can tell, you want to start with a query like this:
select s.house_id, u.email, hu.is_owner, u.is_active
from subscriptions s left join
house_user hu
on s.house_id = hu.house_id left join
users u
on hu.user_id = u.id;
This does not return what you want, but it is rather unclear how your results are derived.

select a column from another table mulipletimes problem

User
USER_ID USERNAME
1 -
2 Chris
3 Dave
4 Vlad
Issue
Creator RESOLVER VERIFIER
2 3 4
2 3 1
3 1 1
expected output:
Creator RESOLVER VERIFIER
Chris Dave Vlad
Chris Dave -
Dave - -
current code I have:
SELECT creatorid.username, resolverid.username, verifierid.username
FROM issue
JOIN user creatorid ON issue.creator = creatorid.user_id
JOIN user resolverid ON issue.resolver = resolverid.user_id
JOIN user verifierid ON issue.verifier = verifierid.user_id
do i have to join the table 3 times to get the corresponding username of the user_id in issue table or is there is a simpler way of doing this? Asking as this is a simplified version of the tables, the User and Issue table contains a lot of other columns. Thanks
Because of the join, you will see each issue three times which is not what you want. You could use three scalar subqueries to get around that:
select i.id,
(select username from users u1 where u1.user_id = i.creator) as creator,
(select username from users u2 where u2.user_id = i.resolver) as resolver,
(select username from users u3 where u3.user_id = i.verifier) as verifier
from issue i;
This isn't going to be fast though.
Another option is to aggregate all user_id / username pairs into a JSON object, then use that in a sub-query:
select i.id, -- other columns from the issue table
u.names ->> i.creator::text as creator,
u.names ->> i.resolver::text as resolver,
u.names ->> i.verifier::text as verifier
from issue i
join lateral (
select jsonb_object_agg(user_id, username) as names
from users u
where u.user_id in (i.creator, i.resolver, i.verifier)
) u on true;
The traditional way to do this is:
select i.*, uc.username, ur.username, uv.username
from issue i left join
users uc
on uc.user_id = i.creator left join
users ur
on ur.user_id = i.resolver left join
users uv
on uv.user_id = i.verifier;

Select users that have n roles for reporting functions SQL

I have an application that uses a standard Role, User and UserRoleJoin architecture.
The application needs to send reports to users based on what group of roles they may be in. For example
Role:
ID Name
1 Lead
2 Mech
3 Elec
4 Auditor
5 Assigner
UserRole:
UserID RoleID
1 1
1 2
1 4
2 1
2 4
I want to be able to send one report to all users that are Leads, Mech and Auditors or a different report to all users that are leads and auditors.
The first report would only be sent to user 1 while the second report would be sent to user 2
Should I have a different table that handle roles to reports associations or should I deal with these groups by a select query?
There are several ways. I would be inclined to use group by and having:
select ur.UserId
from UserRole ur join
Role r
on ur.RoleID = r.Id
where name in ('Leads', 'Mech', 'Auditors')
group by ur.UserId
having count(*) = 3;
The advantage of this approach is that the logic is all in the having clause. You can make it more flexible, such as all Leads and Mechs who are not Auditors.
If those groups are stable enough they deserve a way to persist them in the DB, kind of Group(ID PK, Name), RoleGroup(GroupID , RoleID)
declare #grpName varchar(50) = 'my_group';
select ur.UserId
from UserRole ur
join RoleGroup rg on rg.RoleID = ur.RoleID
join Group g on rg.GroupID = g.ID and g.Name = #grpName
group by ur.UserId
having count(*) = (select count(*) n
from RoleGroup rg
join Group g on rg.GroupId = g.ID and g.Name = #grpName);

SQL query (Join without duplicates)

I have tables users and topics. Every user can have from 0 to several topics (one-to-many relationship).
How I can get only those users which have at least one topic?
I need all columns from users (without columns from topics) and without duplicates in table users. In last column I need number of topics.
UPDATED:
Should be like this:
SELECT user.*, count(topic.id)
FROM ad
LEFT JOIN topic ON user.id = topic.ad
GROUP BY user.id
HAVING count(topic.id) > 0;
but it takes 0 result. But it should not be 0.
Firstly you need to have your two tables, because you have left limited information about your table structure I will use an example to explain how this works, you should then be able to easily apply this to your own tables.
Firstly you need to have two tables (which you do)
Table "user"
id | name
1 | Joe Bloggs
2 | Eddy Ready
Table "topic"
topicid | userid | topic
1 | 1 | Breakfast
2 | 1 | Lunch
3 | 1 | Dinner
Now asking for a count against each user is done using the follwing;
SELECT user.name, count(topic.topicid)
FROM user
INNER JOIN topic ON user.id = topic.userid
GROUP BY user.name
If you use a left join, this will include records from the "user" table which does not have any rows in the "topic" table, however if you use an INNER JOIN this will ONLY include users who have a matching value in both tables.
I.e. because the user id "2" (which we use to join) is not listed in the topic table you will not get any results for this user.
Hope that helps!
use inner join and distinct
select distinct user_table.id
from user_table
inner join topics_table on topic_table.user_id = user_table.id
select u.id
, u.name
, count(b.topicName)
from user u
left join topic t on t.userid = u.id
group by u.id, u.name
You can select topic number per user and then join it with user data. Something like this:
with t as
(
select userid, count(*) as n
from topic
group by userid
)
SELECT user.*, t.n
FROM user
JOIN t ON user.id = t.userid

Selecting a user's friend's events

I have these information :
Table "Users" =>
**Id** **Name**
1 a
2 b
3 c
4 d
5 e
Table "Friends" =>
**SenderId** **ReceiverId** **State**
1 2 x
2 3 ok
3 1 ok
3 4 ok
5 3 ok
5 4 ok
Table "Events" =>
**SenderId** **receiverId** **text**
1 1 3 ssss
2 3 1 dsadsa
3 2 3 safsdf
4 3 5 fgfdgfd
5 4 3 fgfhgfh
6 5 4 sad sad
My question is that how could I get the events of user's friends in one sql statement using JOINS only .
for example :=>
userId : 1
his friends : 3 (state = ok)
friends events : 1, 2, 3, 4, 5 (events from 1 to 5 have the userId 3 which considered as a friend to user 1 )
ANY HELP .. THANKS ;) !!
You didn't specify a name for the first column in your Events table so I've called it row_ID:
SELECT U1.ID AS user_ID, U1.Name AS user_name,
U2.ID AS friend_user_ID, U2.Name AS friend_user_name,
E1.row_ID, E1.text AS event_text
FROM Users AS U1
INNER JOIN Friends AS F1
ON (
U1.ID = F1.ReceiverId
OR U1.ID = F1.SenderId
)
AND F1.State = 'ok'
INNER JOIN Users AS U2
ON U2.ID = F1.SenderId
INNER JOIN Events AS E1
ON (
U2.ID = E1.ReceiverId
OR U2.ID = E1.senderId
)
WHERE U1.ID = 1;
Your table structure is quite confusing with all those Sender and Receiver ID's, but I think the following query will work.
SELECT * FROM
(SELECT Users.Name, Events.text
FROM Users
LEFT JOIN Friends ON Users.Id = Friends.ReceiverId
LEFT JOIN Events ON Friends.SenderId = Events.SenderId
WHERE Users.Id = 1)
UNION ALL
(SELECT Users.Name, Events.text
FROM Users
LEFT JOIN Friends ON Users.Id = Friends.ReceiverId
LEFT JOIN Events ON Friends.SenderId = Events.ReceiverId
WHERE Users.Id = 1)
You can probably simplify this, but I'm not sure how, considering you want to select events 1-5 because the friend ID can be either the Events.SenderId or the Events.ReceiverId.
I don't know if SQL supports this, but maybe put an OR into the LEFT JOIN clause (?):
SELECT Users.Name, Events.text
FROM Users
LEFT JOIN Friends ON Users.Id = Friends.ReceiverId
LEFT JOIN Events ON (Friends.SenderId = Events.SenderId OR Friends.SenderId = Events.ReceiverId)
WHERE Users.Id = 1
It's pretty easy with a sub-query.
Here's the subquery (find user 1's friends)
SELECT
senderID
FROM
friends
WHERE
receiverID = 1
Here's the main query (find user1's friend's events)
SELECT
primary_key,
senderID,
receiverID,
text
FROM
events
WHERE
senderID IN (subquery) --events where user1's friends were the senders
OR receiverID IN (subquery) --events where user1's friend's were the receivers
Put it all together:
SELECT
*
FROM
events
WHERE
senderID IN (SELECT
senderID
FROM
friends
WHERE
senderID = 1) --events where user1's friends were the senders
OR receiverID IN (SELECT
senderID
FROM
friends
WHERE
receiverID = 1) --events where user1's friend's were the receivers
You might consider replacing the explicit '1' with a variable, so you can run this query for every user.
Good luck