Problem constructing where clause to include null/0 values - sql

I have an application where users can take tests (which are composed of questions and answers).
I'm trying to construct a query that returns a count of answers grouped by question, for a specific teacher and test.
The problem is I would like the query to return 0 count for questions with no answers.
answers
id
question_id
test_id
student_id
questions
id
teacher_students
student_id
teacher_id
tests
id
Query
SELECT COUNT(answers.id) AS rcount,
questions.id
FROM "questions"
LEFT JOIN answers ON answers.question_id = questions.id
LEFT JOIN teacher_students ON teacher_students.student_id = answers.student_id
WHERE (questions.test_id = 1)
AND (teacher_students.teacher_id = 1)
GROUP BY questions.id
ORDER BY questions.id
Output
rcount | question_id
--------+----
4 | 1
2 | 3
Desired Output
rcount | question_id
--------+----
4 | 1
0 | 2
2 | 3
0 | 4
If I remove teacher_students.teacher_id = 1, the number of questions returned is correct, but the count is not.

Move the teacher_students.teacher_id = 1 check from the WHERE clause to the joining ON clause.
When a condition that refers to a table in the right side of a LEFT JOIN is put in the WHERE clause, the LEFT JOIN is cancelled and it acts as an INNER JOIN.
SELECT count(teacher_students.student_id) AS rcount <--- changed
, questions.id
FROM "questions"
LEFT JOIN answers
ON answers.question_id = questions.id
LEFT JOIN teacher_students
ON teacher_students.student_id = answers.student_id
AND teacher_students.teacher_id = 1
WHERE questions.test_id = 1
GROUP BY questions.id
ORDER BY questions.id

Related

Dynamically select the table to join in Postgres with case statements

My notifications table has a column called action_id and trigger_type. I want to INNER JOIN action_id with another table (Like users or posts) depending on the trigger_type. I wrote the following query but it throws an error.
Table structure
users
display_name
username
id
John
Doe
1
Larry
Doe
2
posts
post_title
post_body
id
user_id
Hello
Hello world
1
2
comments
comment_text
post_id
id
user_id
Hello
1
1
1
notifications
read
trigger_id
id
target_id
action_id
trigger_type
false
1
1
2
1
0
false
1
2
2
1
1
trigger_type = 0 means its a like 1 means its a comment
SELECT notifications.*, users.display_name, users.username, users.profile_pic, posts.title
FROM notifications
INNER JOIN users ON users.id = notifications.trigger_id
(
CASE notifications.trigger_type
WHEN 0 THEN INNER JOIN users ON users.id = notifications.action_id
WHEN 1 THEN INNER JOIN posts ON posts.id = notifications.trigger_id
)
You cannot conditionally join like that. Instead, use left join like this:
SELECT n.*,
-- whatever columns you want from the trigger user go here
un.display_name, un.username, un.profile_pic, p.title
FROM notifications n JOIN
users u
ON u.id = n.trigger_id LEFT JOIN
users un
ON un.id = n.action_id AND n.trigger_type = 0 LEFT JOIN
posts p
ON p.id = n.action_id AND n.trigger_type = 1;

Multiple rows get only specific values

Feel like this should be a rather simple problem yet, I'm struggling to find the solution.
We have three tables to create a Question Answer system. One is the question, other is answer and then the third is finally where we store the user's selection.
Question table
QuestionID Question
1 What is your favorite color?
2 Where were you born?
Answer table
AnswerID QuestionID Answer
1 1 Blue
2 1 Green
3 1 Yellow
4 2 USA
5 2 Africa
Answer stored table
AnswerStoreID QuestionID AnswerID UserID
1 1 1 1
2 1 2 1
3 2 4 2
4 2 5 2
5 1 1 3
I want to find the UserID that answered QuestionID 1 as AnswerID 1 AND QuestionID 2 as AnswerID 4.
Thought it would be simple like this
SELECT UserID
FROM Question Q
INNER JOIN Answer A ON A.QuestionID = A.QuestionID
INNER JOIN AnswerStore AS ON AS.AnswerID = A.AnswerID
WHERE (AS.AnswerID = 1 AND AS.QuestionID = 1)
AND (AS.AnswerID = 2 AND AS.QuestionID = 4)
That renders nothing though. When replacing the AND between the two where statements with an OR gets results that don't have both those answers though which is not desired either. I want only those users who answered both of these questions.
I then did a query with some various joins to do a query per question but feel that is too complicated and heavy for this problem and I'm overthinking it. Is there an easier solution to this problem?
---- Edit ----
Actually, you don't even need the JOINs in your original query:
SELECT t.UserID
FROM AnswerStore AS t
WHERE (t.AnswerID = 1 AND t.QuestionID = 1)
OR (t.AnswerID = 2 AND t.QuestionID = 4)
GROUP BY t.UserID
HAVING COUNT(*) = 2
---- Original Full Answer ----
This is actually a fairly common question, that appears a couple times a week. Unfortunately, it is really hard to formulate a repeatable/searchable question to reference for it.
SELECT UserID
FROM Question Q
INNER JOIN Answer A ON A.QuestionID = A.QuestionID
INNER JOIN AnswerStore AS ON AS.AnswerID = A.AnswerID
WHERE (AS.AnswerID = 1 AND AS.QuestionID = 1)
OR (AS.AnswerID = 2 AND AS.QuestionID = 4)
GROUP BY UserID
HAVING COUNT(*) = 2
The general form is:
SELECT A.a_id
FROM A
INNER JOIN B ON A.a_id = B.a_id
WHERE B.something IN ([list])
GROUP BY a_id
HAVING COUNT(*) = [length of list]
-- or in cases where B matches may be non-unique
-- HAVING COUNT(DISTINCT B.something) = [length of list]
You are really looking at two sets of data, UserIDs that answered QuestionID 1 as AnswerID 1, and UserIDs that answered QuestionID 2 as AnswerID 4. So you can join the sets together to find UserIDs that are in both sets of data:
SELECT UserID
FROM AnswerStore as1 INNER JOIN AnswerStore as2 ON as1.UserID = as2.UserID
AND as1.QuestionID = 1 AND as1.AnswerID = 1
AND as2.QuestionID = 2 AND as2.AnswerID = 4

SQL join adding rows with null values

I'm doing a clone of Twitter using PostgreSQL. The users can write posts and they can share them. There is a timeline for each user, where it shows the posts and the shares of the people he follows. I also have to show the difference between a shared post (retweet) and a regular one by the original author. I have the following problem when joining the tables:
Post
post_id | user_id | text
1 1 text
Shares
post_id | user_id
1 2
Join_result
Post.post_id | Post.user_id | Post.text | Shares.post_id | Shares.user_id
1 1 text 1 2
Then I filter by Post.user_id or Shares.user_id. However, with this result, I don't know if I'm showing the post because it is a user 2 sharing or user 1 post. A good solution to me, it would be this join table:
Join_result
Post.post_id | Post.user_id | Post.text | Shares.post_id | Shares.user_id
1 1 text null null
1 1 text 1 2
Now I could filter correctly:
(Post.user_id in following and Share.user_id is NULL) or Share.user_id in followings
In this example, if the user follows user 1, it returns the first row; if he follows user 2 I get the second one and if he follows the two users, returns the two rows.
I've already solved this problem using a UNION query but I'd like to know if there is another solution (and better).
EDIT the query:
SELECT p0* f1.*, FROM "posts" AS p0
LEFT OUTER JOIN "forwards" AS f1 ON f1."post_id" = p0."id"
INNER JOIN (SELECT id FROM users AS u0
INNER JOIN follows AS f1 ON (f1.follower_id = $1) AND (f1.following_id = u0.id) UNION
SELECT id FROM users AS u1 WHERE (u1.id = $1)
) AS f3 ON (p0."author_id" = f3."id") OR (f1."user_id" = f3."id") ORDER BY p0."inserted_at" DESC
You are using a LEFT OUTER JOIN, in that case if no forward exists it will set null values in the results.
See this post for more information:
What is the difference between "INNER JOIN" and "OUTER JOIN"?

List of questions comparison

I have a profile that looks like this:
profile_id | answer_id
----------------------
1 1
1 4
1 10
I have a table which contains a list of responses by poll respondents with structure like this:
user_id | answer_id
-------------------
1 1
1 9
2 1
2 4
2 10
3 14
3 29
How do I select a list of users that gave all of the answers in the profile? In this case only user 2.
You can use the following:
select user_id
from response r
where answer_id in (select distinct answer_id -- get the list of distinct answer_id
from profile
where profile_id = 1) -- add filter if needed
group by user_id -- group by each user
having count(distinct answer_id) = (select count(distinct answer_id) -- verify the user has the distinct count
from profile
where profile_id = 1) -- add filter if needed
See SQL Fiddle with Demo
Or another way to write this is:
select user_id
from response r
where answer_id in (1, 4, 10)
group by user_id
having count(distinct answer_id) = 3
See SQL Fiddle with Demo
This is an example of a join query with an aggregation:
select a.user_id
from profile p full outer join
answers a
on p.answer_id = p.answer_id and
p.profile_id = 1
group by a.user_id
having count(p.profileid) = count(*) and
count(a.user_id) = count(*)
The full outer join matches all the profiles to all the answers. If the two sets completely match, then there are no "null"s in the ids of the other set. The having clause checks for jsut this condition.
SELECT user_id
FROM user_answer
WHERE user_id in (SELECT user_id FROM profile WHERE answer_id = 1) AND
user_id in (SELECT user_id FROM profile WHERE answer_id = 4) AND
user_id in (SELECT user_id FROM profile WHERE answer_id = 10)
SELECT *
FROM table1
INNER JOIN table2
ON table1.answer_id = table2.answer_id
WHERE table2.user_id = 2
i think this might be what you're looking for.

How to count from a table based on another table

I have 4 Tables, I don't need the 'Questions' Table, but I just put it there so you'd know where i got the Question ID in the other tables.
Questions
--------
ID
Question
Question_Options
--------
ID
Question_ID
Option_Label
Session
--------------
ID
GENDER
Session_Answers
-----------------
ID
Session_ID
Option_ID
Question_ID
I calculated the following: the number of votes for each option from a certain question, like so
SELECT Q.Option_Label as Choice, COALESCE((SELECT COUNT(*) FROM Session_Answers S WHERE S.Option_ID = Q.ID),0) as Votes
FROM Question_Options Q
INNER JOIN Session_Answers S
ON Q.Question_ID = S.Question_ID
WHERE Q.Question_ID = 10114<---the Question ID
GROUP BY Q.ID,Q.option_label
What I want to do, is add a new column to the query that calculates the number of males who have chosen each option based on the Session Table.
You can do that :
SELECT QO.Question_ID, QO.Option_Label as Choice, COUNT(*) as VotesMale
FROM Question_Options QO
LEFT JOIN Session_Answers SA ON QO.ID = SA.Option_ID
JOIN [Session] S ON S.ID = SA.Session_ID AND S.Gender = 'M'
WHERE QO.Question_ID = 10114<---the Question ID
GROUP BY QO.Question_ID, QO.Option_label
You can simply add extra count from the Session table filtered with Gender.
SELECT Q.Option_Label as Choice, COALESCE((SELECT COUNT(*) FROM Session_Answers SA WHERE
SA.Option_ID = Q.ID),0) as Votes,
COALESCE((SELECT COUNT(*) FROM Session SM WHERE
S.Session_ID = SM.Session_ID AND Gender='M'),0) as MalesSessions
FROM Question_Options Q
INNER JOIN Session_Answers S
ON Q.Question_ID = S.Question_ID
WHERE Q.Question_ID = 10114 ---the Question ID
GROUP BY Q.ID,Q.option_label