Many-to-many relationship to determine if user has liked a post - sql

I have a table that contains all of the posts. I also have a table where a row is added when a user likes a post with foreign keys user_id and post_id.
I want to retrieve a list of ALL of the posts and whether or not a specific user has liked that post. Using an outer join I end up getting some posts twice. Once for user 1 and once for user 2. If I use a WHERE to filter for likes.user_id = 1 AND likes.user_id is NULL I don't get the posts that are only liked by other users.
Ideally I would do this with a single query. SQL isn't my strength, so I'm not even really sure if a sub query is needed or if a join is sufficient.
Apologies for being this vague but I think this is a common enough query that it should make some sense.
EDIT: I have created a DB Fiddle with the two queries that I mentioned. https://www.db-fiddle.com/f/oFM2zWsR9WFKTPJA16U1Tz/4
UPDATE: Figured it out last night. This is what I ended up with:
SELECT
posts.id AS post_id,
posts.title AS post_title,
CASE
WHEN EXISTS (
SELECT *
FROM likes
WHERE posts.id = likes.post_id
AND likes.user_id = 1
) THEN TRUE
ELSE FALSE END
AS liked
FROM posts;
Although I was able to resolve it, thanks to #wildplasser for his answer as well.

Data (I needed to change it a bit, because one should not assign to serials):
CREATE TABLE posts (
id serial,
title varchar
);
CREATE TABLE users (
id serial,
name varchar
);
CREATE TABLE likes (
id serial,
user_id int,
post_id int
);
INSERT INTO posts (title) VALUES ('First Post');
INSERT INTO posts (title) VALUES ('Second Post');
INSERT INTO posts (title) VALUES ('Third Post');
INSERT INTO users (name) VALUES ('Obama');
INSERT INTO users (name) VALUES ('Trump');
INSERT INTO likes (user_id, post_id) VALUES (1, 1);
INSERT INTO likes (user_id, post_id) VALUES (2, 1);
INSERT INTO likes (user_id, post_id) VALUES (2, 2);
-- I want to retrieve a list of ALL of the posts and whether or not a specific user has liked that post
SELECT id, title
, EXISTS(
--EXISTS() yields a boolean value
SELECT *
FROM likes lk
JOIN users u ON u.id = lk.user_id AND lk.post_id=p.id
WHERE u.name ='Obama'
) AS liked_by_Obama
FROM posts p
;
Results:
id | title | liked_by_obama
----+-------------+----------------
1 | First Post | t
2 | Second Post | f
3 | Third Post | f
(3 rows)

As far as I understand, you have two tables such as post table which includes all post from different users and a like table with user.id and post id. if you want to retreive only posts then
select * from posts
if you need user information as well, which is present in user table then you can do below.
select user.user_name, post.postdata from user,post where post.userid=user.userid
in above query, user_name is a column name in user table and postdata is a column in post table.

Related

Nested SQL call

EDIT:
As requested, our table schema is,
posts:
postid (primary key),
post_text
comments:
commentid (primary key) ,
postid (foreign key referencing posts.postid),
comment_text
replies
replyid (primary key)
commentid (foreign key referencing comments.commentid)
reply_text
I have the tables posts, comments, and replies in a SQL database. (Obviously, a post can have comments, and a comment can have replies)
I want to return a post based on its id, postid.
So I would like a database function has the inputs and outputs,
input:
postid
output:
post = {
postid
post_text
comments: [comment, ...]
}
Where the comment and reply are nested in the post,
comment = {
commentid,
text
replies: [reply, ...]
}
reply = {
replyid
reply_text
}
I have tried using joins, but the returned data is highly redundant, and it seems stupid. For instance, fetching the data from two different replies will give,
postid
post_text
commentid
comment_text
replyid
reply_text
1
POST_TEXT
78
COMMENT_TEXT
14
REPLY1_TEXT
1
POST_TEXT
78
COMMENT_TEXT
15
REPLY2_TEXT
It seems instead I want to make 3 separate queries, in sequence (first to the table posts, then to comments, then to replies)
How do I do this?
The “highly redundant” join result is normally the best way, because it is the natural thing in a relational database. Relational databases aim at avoiding redundancy in data storage, but not in query output. Avoiding that redundancy comes at an extra cost: you have to aggregate the data on the server side, and the client probably has to unpack the nested JSON data again.
Here is some sample code that demonstrates how you could aggregate the results:
SELECT postid, post_text,
jsonb_agg(
jsonb_build_object(
'commentid', commentid,
'comment_text', comment_text,
'replies', replies
)
) AS comments
FROM (SELECT postid, post_text, commentid, comment_text,
jsonb_agg(
jsonb_build_object(
'replyid', replyid,
'reply_text', reply_text
)
) AS replies
FROM /* your join */
GROUP BY postid, post_text, commentid, comment_text) AS q
GROUP BY postid, post_text;
The redundant data stems from a cross join of a post's comments and replies. I.e. for each post you join each comment with each reply. Comment 78 does neither relate to reply 14 nor to reply 15, but merely to the same post.
The typical approach to select the data would hence be three queries:
select * from posts;
select * from comments;
select * from replies;
You can also reduce this to two queries and join the posts table to the comments query, the replies query, or both. This again, will lead to selecting redundant data, but may ease data handling in your app.
If you want to avoid joins, but must avoid database round trips, you can glue query results together:
select *
from
(
select postid as id, postid, 'post' as type, post_text as text from posts
union all
select commentid as id, postid, 'comment' as type, comment_text as text from comments
union all
select replyid as id, postid, 'reply' as type, reply_text as text from replies
) glued
order by postid, type, id;
At last you can create JSON in your DBMS. Again, don't cross join comments with replies, but join the aggregated comments object and the aggregated replies object to the post.
select p.postid, p.post_text, c.comments, r.replies
from posts p
left join
(
select
postid,
jsonb_object_agg(jsonb_build_object('commentid', commentid,
'comment_text', comment_text)
) as comments
from comments
group by postid
) c on c.postid = p.postid
left join
(
select
postid,
jsonb_object_agg(jsonb_build_object('replyid', replyid,
'reply_text', reply_text)
) as replies
from replies
group by postid
) r on r.postid = p.postid;
Your idea to store things in JSON is a good one if you have something to parse it down the line.
As an alternative to the previous answers that involve JSON, you can also get a normal SQL result set (table definition and sample data are below the query):
WITH MyFilter(postid) AS (
VALUES (1),(2) /* rest of your filter */
)
SELECT 'Post' AS PublicationType, postid, NULL AS CommentID, NULL As ReplyToID, post_text
FROM Posts
WHERE postID IN (SELECT postid from MyFilter)
UNION ALL
SELECT CASE ReplyToID WHEN NULL THEN 'Comment' ELSE 'Reply' END, postid, commentid, replyToID, comment_text
FROM Comments
WHERE postid IN (SELECT postid from MyFilter)
ORDER BY postid, CommentID NULLS FIRST, ReplyToID NULLS FIRST
Note: the PublicationType column was added for the sake of clarity. You can alternatively inspect CommentID and ReplyToId and see what is null to determine the type of publication.
This should leave you with very little, if any, redundant data to transfer back to the SQL client.
This approach with UNION ALL will work with 3 tables too (you only have to add 1 UNION ALL) but in your case, I would rather go with a 2-table schema:
CREATE TABLE posts (
postid SERIAL primary key,
post_text text NOT NULL
);
CREATE TABLE comments (
commentid SERIAL primary key,
ReplyToID INTEGER NULL REFERENCES Comments(CommentID) /* ON DELETE CASCADE? */,
postid INTEGER NOT NULL references posts(postid) /* ON DELETE CASCADE? */,
comment_text Text NOT NULL
);
INSERT INTO posts(post_text) VALUES ('Post 1'),('Post 2'),('Post 3');
INSERT INTO Comments(postid, comment_text) VALUES (1, 'Comment 1.1'), (1, 'Comment 1.2'), (2, 'Comment 2.1');
INSERT INTO Comments(replytoId, postid, comment_text) VALUES (1, 1, 'Reply to comment 1.1'), (3, 2, 'Reply to comment 2.1');
This makes 1 fewer table and allows to have level 2 replies (replies to replies), or more, rather than just replies to comments. A recursive query (there are plenty of samples of that on SO) can make it so a reply can always be linked back to the original comment if you want.
Edit: I noticed your comment just a bit late. Of course, no matter what solution you take, there is no need to execute a request to get the replies to each and every comment.
Even with 3 tables, even without JSON, the query to get all the replies for all the comments at once is:
SELECT *
FROM replies
WHERE commentid IN (
SELECT commentid
FROM comments
WHERE postid IN (
/* List your post ids here or nest another SELECT postid FROM posts WHERE ... */
)
)

postgres insert into multiple tables after each other and return everything

Given postgres database with 3 tables:
users(user_id: uuid, ...)
urls(slug_id:int8 pkey, slug:text unique not null, long_url:text not null)
userlinks(user_id:fkey users.user_id, slug_id:fkey urls.slug_id)
pkey(user_id, slug_id)
The userlinks table exists as a cross reference to associate url slugs to one or more users.
When a new slug is created by a user I'd like to INSERT into the urls table, take the slug_id that was created there, INSERT into userlinks with current users ID and slug_id
Then if possible return both results as a table of records.
Current users id is accessible with auth.uid()
I'm doing this with a stored procedure in supabase
I've gotten this far but I'm stuck:
WITH urls_row as (
INSERT INTO urls(slug, long_url)
VALUES ('testslug2', 'testlong_url2')
RETURNING slug_id
)
INSERT INTO userlinks(user_id, slug_id)
VALUES (auth.uid(), urls_row.slug_id)
--RETURNING *
--RETURNING (urls_record, userlinks_record)
Try this :
WITH urls_row as (
INSERT INTO urls(slug, long_url)
VALUES ('testslug2', 'testlong_url2')
RETURNING slug_id
), userlink_row AS (
INSERT INTO userlinks(user_id, slug_id)
SELECT auth.uid(), urls_row.slug_id
FROM urls_row
RETURNING *
)
SELECT *
FROM urls_row AS ur
INNER JOIN userlink_row AS us
ON ur.slug_id = us.slug_id

Delete from a SQL Table using a couple JOINs

There are 4 Tables:
Users: id,name
Albums: id,user_id
Pictures: id,picture_name,album_id
Tags: picture_id , user_id
I need to write 2 commands:
- a command which deletes a picture by its name -
which means, removing it from the "Tags" table and from the "Pictures" table.
- a command which adds a tag to a picture of a User.
I have tried so far those two commands (which don't seem to work):
Deleting from the tags table:
DELETE FROM Tags
JOIN Albums
ON Users.id=Albums.user_id
JOIN PICTURES
ON Pictures.album_id = Albums.id
WHERE Pictures.name LIKE "pic_name.png" ;
Deleting from the pictures table:
DELETE FROM Pictures
WHERE Pictures.name LIKE "pic_name.png" ;
Adding a tag of " user1 " on picture "pic_name2.png" to the Tags table:
INSERT INTO Tags (user_id,picture_id) SELECT Users.id, Pictures.id FROM Albums JOIN Users ON Users.id=Albums.user_id JOIN PICTURES ON Pictures.album_id = Albums.id WHERE Users.name LIKE "user1" and Pictures.name= "pic_name2.png";
please help me write those queries correctly ...
try like below
delete from Tags
where user_id in
( select user_id from Albums
JOIN PICTURES
ON PICTURES.album_id = Albums.id
WHERE Pictures.name LIKE '%pic_name.png%'
)
2nd query
DELETE FROM Pictures
WHERE Pictures.name LIKE '%pic_name.png%'
and 3rd query only need change where
WHERE Users.name ='user1' and Pictures.name= 'pic_name2.png'
You don't need to join any tables to delete the rows:
delete from tags
where picture_id = (
select id from pictures
where picture_name = 'pic_name.png'
);
delete from pictures
where picture_name = 'pic_name.png';
The tables users and pictures cannot and do not need to be joined.
You can insert a new row like this:
insert into tags (user_id, picture_id) values
(
(select id from users where name = 'user1'),
(select id from pictures where picture_name = 'pic_name2.png')
);
As you can see I changed all like to = because this is what you need in this case.
See more here

Inserting multiple records in database table using PK from another table

I have DB2 table "organization" which holds organizations data including the following columns
organization_id (PK), name, description
Some organizations are deleted so lot of "organization_id" (i.e. rows) doesn't exist anymore so it is not continuous like 1,2,3,4,5... but more like 1, 2, 5, 7, 11,12,21....
Then there is another table "title" with some other data, and there is organization_id from organization table in it as FK.
Now there is some data which I have to insert for all organizations, some title it is going to be shown for all of them in web app.
In total there is approximately 3000 records to be added.
If I would do it one by one it would look like this:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
VALUES
(
'This is new title',
XXXX,
CURRENT TIMESTAMP,
1,
1,
1
);
where XXXX represent "organization_id" which I should get from table "organization" so that insert do it only for existing organization_id.
So only "organization_id" is changing matching to "organization_id" from table "organization".
What would be best way to do it?
I checked several similar qustions but none of them seems to be equal to this?
SQL Server 2008 Insert with WHILE LOOP
While loop answer interates over continuous IDs, other answer also assumes that ID is autoincremented.
Same here:
How to use a SQL for loop to insert rows into database?
Not sure about this one (as question itself is not quite clear)
Inserting a multiple records in a table with while loop
Any advice on this? How should I do it?
If you seriously want a row for every organization record in Title with the exact same data something like this should work:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
;
you shouldn't need the column aliases in the select but I am including for readability and good measure.
https://www.ibm.com/support/knowledgecenter/ssw_i5_54/sqlp/rbafymultrow.htm
and for good measure in case you process errors out or whatever... you can also do something like this to only insert a record in title if that organization_id and title does not exist.
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
LEFT JOIN Title t
ON o.organization_id = t.organization_id
AND t.name = 'This is new title'
WHERE
t.organization_id IS NULL
;

Copy column to another table, but keep equal ID's

I've searched all over, but can't find an answer to this.. I have two tables user_setting and fibruser which both contains ID's to users. fibruser contains all possible ID's, but user_setting only contain a few. I need to copy all fibruser.id to user_setting.user_id where user_id is NULL and make sure none of the existing ones get overridden.
I do this Query:
SELECT user_setting.user_id, fibruser.id
FROM user_setting
FULL JOIN fibruser
ON user_id = fibruser.id;
to compare ID's and find all the ID's, but I simply cannot get UPDATE user_setting working like I want to...
UPDATE user_setting
SET user_id = fibruser.id
FROM fibruser
WHERE user_id IS null;
This only gives me `UPDATE user_setting
SET user_id = fibruser.id
FROM fibruser
WHERE user_id = id
AND user_id IS null;
This only outputs 0 row(s) affected.. What am I doing wrong and/or missing?
EDIT: Forgot to mention datatypes.
id: INT(auto increment)
user_id: INT
When you do a FULL JOIN, all those null values you are seeing represent cases where there is a row in fibruser with the given ID value, but there is NOT any row with that ID value in user_setting. Thus, when you do an UPDATE on user_setting, by definition you are going to have 0 rows affected; you have specifically told it to only update rows that you know do not exist!
What I think you want to do is to INSERT those rows, something like this:
INSERT INTO user_setting (user_id)
SELECT f.id
FROM fibruser f
WHERE f.id NOT IN (SELECT user_id FROM user_setting);
Alternatively, you could write it with NOT EXISTS:
INSERT INTO user_setting (user_id)
SELECT f.id
FROM fibruser f
WHERE NOT EXISTS (SELECT 1 FROM user_setting u WHERE f.id = u.user_id);
You may find that one way or the other will run faster, or they may be identical; however, since I'm assuming you're only doing this one time, you probably don't care too much about performance, so you can just pick whichever syntax floats your boat.