Get courses chosen by all students - sql

Now there are two tables student(sid, name) and course(cid, name), which relation is many-to-many. So there is another table student_course(sid, cid) that stores the information about what courses have been chosen by whom.
How to write the sql that can get the courses that have been chosen by all the students?

Standard solution: use the NOT EXISTS ( ... NOT EXISTS (...)) construct:
find all courses in which all students take part
==>> there must not exist a student that does not take part in this course
SELECT * FROM course c
WHERE NOT EXISTS (
SELECT * from student s
WHERE NOT EXISTS (
SELECT * from student_course cs
WHERE cs.sid = s.sid
AND cs.cid = c.cid
)
)
;
This query is often faster (given appropiate indexes) than the count() == count() variant. The reason for this: you do not have to count all the (distinct) records; once you found one student that does not take this course you can omit this course from your list of suspects. Also: ANTI-joins often can make use of indexes [so can a count(), but that still has to count all the (distinct) keyvalues in the index]

Select c.cid, c.name
From course c where
(select count(1) from student) = (select count(1) from student_course sc where sc.cid = c.cid);
See SQL Fiddle
It finds all courses where the count of entries for that course in the student_course table matches the number of students
CID NAME
1 Test Course1
4 Test Course4

Related

How to make sure result pairs are unique - without using distinct?

I have three tables I want to iterate over. The tables are pretty big so I will show a small snippet of the tables. First table is Students:
id
name
address
1
John Smith
New York
2
Rebeka Jens
Miami
3
Amira Sarty
Boston
Second one is TakingCourse. This is the course the students are taking, so student_id is the id of the one in Students.
id
student_id
course_id
20
1
26
19
2
27
18
3
28
Last table is Courses. The id is the same as the course_id in the previous table. These are the courses the students are following and looks like this:
id
type
26
History
27
Maths
28
Science
I want to return a table with the location (address) and the type of courses that are taken there. So the results table should look like this:
address
type
The pairs should be unique, and that is what's going wrong. I tried this:
select S.address, C.type
from Students S, Courses C, TakingCourse TC
where TC.course_id = C.id
and S.id = TC.student_id
And this does work, but the pairs are not all unique. I tried select distinct and it's still the same.
Multiple students can (and will) reside at the same address. So don't expect unique results from this query.
Only an overview is needed, so that's why I don''t want duplicates
So fold duplicates. Simple way with DISTINCT:
SELECT DISTINCT s.address, c.type
FROM students s
JOIN takingcourse t ON s.id = t.student_id
JOIN courses c ON t.course_id = c.id;
Or to avoid DISTINCT (why would you for this task?) and, optionally, get counts, too:
SELECT c.type, s.address, count(*) AS ct
FROM students s
JOIN takingcourse t ON s.id = t.student_id
JOIN courses c ON t.course_id = c.id
GROUP BY c.type, s.address
ORDER BY c.type, s.address;
A missing UNIQUE constraint on takingcourse(student_id, course_id) could be an additional source of duplicates. See:
How to implement a many-to-many relationship in PostgreSQL?

'ALL' concept in SQL queries

Relational Schema:
Students (**sid**, name, age, major)
Courses (**cid**, name)
Enrollment (**sid**, **cid**, year, term, grade)
Write a SQL query that returns the name of the students who took all courses.I'm not sure how I capture the concept of 'ALL' in a SQL query.
EDIT:
I want to be able write it without aggregation as I want to use the same logic for writing the query in relational algebra as well.
Thanks for the help!
One way of writing such queries is to count the number of course and number of courses each student took, and compare them:
SELECT s.*
FROM students s
JOIN (SELECT sid, COUNT(DISTINCT cid) AS student_courses
FROM enrollment
GROUP BY sid) e ON s.sid = e.sid
JOIN (SELECT COUNT(*) AS cnt
FROM courses) c ON cnt = student_cursed
This gives course combinations that are possible but haven't been taken...
SELECT s.sid, c.cid FROM students CROSS JOIN courses
EXCEPT
SELECT sid, cid FROM enrollment
So, you can then do the same with the student list...
SELECT sid FROM students
EXCEPT
(
SELECT DISTINCT
sid
FROM
(
SELECT s.sid, c.cid FROM students CROSS JOIN courses
EXCEPT
SELECT sid, cid FROM enrollment
)
AS not_enrolled
)
AS slacker_students
I don't like it, but it avoids aggregation...
SELECT *
FROM Students
WHERE NOT EXISTS (
SELECT 1 FROM Courses
LEFT OUTER JOIN Enrollment ON Courses.cid = Enrollment.cid
AND Enrollment.sid = Students.sid
WHERE Enrollment.sid IS NULL
)
btw. names of tables should be in singular form, not plural

select ... not exists not exists query

select r.index, sum(c.points)
from records r join exams e2 on r.index = e2.index
join courses c on c.id_course = e2.id_course
where not exists ( select *
from required_courses rs
where rs.id_studie = r.id_studie
and not exists (select *
from exams e
where e.id_course = rs.id_course
and r.index = e.index
and score>5))
and score>5
group by index;
I have that query. I know what it does, but don't know how.
I have relativly big database with 16 tables, but only use 4 in this query.
Used tables are:
RECORDS (of students) [index (pr. key), name, surname, ..., id_studie(1)(for. key)]
EXAMS [index(of students) (p.k.), id_course(p.k.),..., score(2), application_status]
COURSES [id_course(p.k.), ..., points]
REQUIRED_COURSES [id_studie (f.k.), id_course(f.k), ...]
(1) I don't know better word in English. When on some faculty there are Informatics, Math, Physics, etc. classes. Informatics is one studie.
(2) Scores go from 5 to 10. 10 is best. At least 6 is required for passing.
Query:
- Find all students that passed all required exams on studie thay studie. Print indexes and number of points.
My question: Can sameone explain how does this work in simple words?
I don't understand not exists & not exists part.
Sorry for my bad english.
Always analyze the query from the inside out.
Innermost query:
Select the exams with score higher than 5
Middle one:
Select the required courses that don't have an exam with score higher than 5
Outer one:
Select students that don't have a required course that doesn't have the exam with score higher than 5
The example is a double-nested NOT EXISTS query.
If a subquery returns any rows at all, EXISTS subquery is TRUE, and NOT EXISTS subquery is FALSE.
That is, it has a NOT EXISTS clause within a NOT EXISTS clause.
It is easier to say that a nested NOT EXISTS answers the question “is x TRUE for all y?”
--Part 3
SELECT r.index,
SUM(c.points)
FROM records r
join exams e2
ON r.index = e2.index
join courses c
ON c.id_course = e2.id_course
WHERE NOT EXISTS (
--Part 2 starts
SELECT *
FROM required_courses rs
WHERE rs.id_studie = r.id_studie
AND NOT EXISTS (
--Part 1 starts
SELECT *
FROM exams e
WHERE e.id_course = rs.id_course
AND r.index = e.index
AND score > 5
--Part 1 ends
)
--Part 2 ends
)
AND score > 5
GROUP BY index;
--Part 3 ends
Part 1: Fetch all records of students from EXAMS table who scored more than 5. You should get all pass mark students for all courses here
Part 2: Join Part1 results with REQUIRED_COURSES on student id and fetch all the required courses where the student has not cleared the exams (where student does not have a score more than 5). You should have all the student's courses name where they are not successful
Part 3: Join Part 2 results with RECORDS table on index and also COURSES table on course id to fetch the index and sum of points. You can see this part into two pieces. First is the normal JOIN with two tables and then the NOT EXISTS with the part 2. When you apply a NOT EXISTS on part 2, you are going to get all successful student ids which would provide you the successful entries by adding another SCORE > 5 condition at the end.

cant understand how this view in SQL works

I am having a hard time understand how the create view, TRANSCRIPTVIEW, manages to set the grade of 0 for those who did not take a course. An explanation would help, the solution and question is below. Thanks.
Student(Id,Name)
Transcript(StudId,CourseName,Semester,Grade)
Formulate the following query in SQL:
Create a list of all students (Id, Name) and, for each student, list the average grade for the courses taken in the S2002 semester.
Note that there can be students who did not take any courses in S2002. For these, the average grade should be listed as 0.
Solution:
We first create a view which augments TRANSCRIPT with rows that enroll every student into a NULL course with the grade of 0. Therefore, students who did not take anything in semester ’S2002’ will have the average grade of 0 for that semester.
Below is what confuses me, how does this work and why does it work?
CREATE VIEW TRANSCRIPTVIEW AS (
( SELECT * FROM Transcipt)
UNION
(
SELECT S.Id,NULL,’S2002’,0
FROM Student S)
WHERE S.Id NOT IN (
SELECT T.StudId
FROM Transcript T
WHERE T.Semester = ’S2002’) )
)
Remaining solution:
SELECT S.Id, S.Name, AVG(T.Grade)
FROM Student S, TRANSCRIPTVIEW T
WHERE S.Id = T.StudId AND T.Semester = ’S2002’ GROUP BY S.Id
how the create view, TRANSCRIPTVIEW, manages to set the grade of 0 for those who did not take a course
The set of students who did not take a course in semester S2002 have no record in the transcript table for that semester. Those who did take a course in that semester do have a record in the table for that semester. The query supplies values NULL, 'S2002',0 for students if they are not in the Transcript table for semester S2002:
SELECT S.Id,NULL,’S2002’,0 FROM Student S) -- this parenthesis is wrong
-- this following where conditions looks for students NOT IN the 2002 subset:
WHERE S.Id NOT IN
-- this next part gets a list of studentids for semester 2002
(
SELECT T.StudId FROM Transcript T
WHERE T.Semester = ’S2002’
)
The solution in your qusetion is ridiculous. The better solution is:
SELECT S.Id, S.Name, AVG(case when T.Semester = ’S2002’ then T.Grade end) as AvgS2002Grade
FROM Student S left outer join
TRANSCRIPTVIEW T
on S.Id = T.StudId AND T.Semester = ’S2002’
GROUP BY S.Id
The query in your question is overly complicated. It is using a union (which should really be union all for performance reasons) to be sure all students are included. Gosh, this is what left outer join is for. It is doing filtering in the where clause, when a conditional aggregation is more suitable. It uses archaic join syntax, instead of the ANSI standard.
I hope you are not learning SQL with those shortcomings.

Getting single records back from joined tables that may produce multiple records

I've got a student table and an enrollment table; a student could have multiple enrollment records that can be active or inactive.
I want to get a select that has a single student record and an indicator as to whether that student has active enrollments.
I thought about doing this in an inline UDF that uses the student ID in a join to the enrollment table, but I wonder if there's a better way to do it in a single select statement.
The UDF call might look something like:
Select Student_Name,Student_Email,isEnrolled(Student_ID) from Student
What might the alternative - with one SQL statement - look like?
select Student_Name,
Student_Email,
(select count(*)
from Enrollment e
where e.student_id = s.student_id
) Number_Of_Enrollments
from Student e
will get the number of enrollments, which should help.
Why not join to a secondary select? Unlike other solutions this isn't firing a subquery for every row returned, but gathers the enrollment data for everyone all at once. The syntax may not be quite correct, but you should get the idea.
SELECT
s.student_name,
s.student_email,
IsNull( e.enrollment_count, 0 )
FROM
Students s
LEFT OUTER JOIN (
SELECT
student_id,
count(*) as enrollment_count
FROM
enrollments
WHERE
active = 1
GROUP BY
student_id
) e
ON s.student_id = e.student_id
The select from enrollments could also be redone as a function which returns a table for you to join on.
CREATE FUNCTION getAllEnrollmentsGroupedByStudent()
RETURNS #enrollments TABLE
(
student_id int,
enrollment_count int
) AS BEGIN
INSERT INTO
#enrollments
(
student_id,
enrollment_count
) SELECT
student_id,
count(*) as enrollment_count
FROM
enrollments
WHERE
active = 1
GROUP BY
student_id
RETURN
END
SELECT
s.student_name,
s.student_email,
e.enrollment_count
FROM
Students s
JOIN
dbo.getAllEnrollmentsGroupedByStudent() e
ON s.student_id = e.student_id
Edit:
Renze de Waal corrected my bad SQL!
Try someting like this:
SELECT Student_Name, Student_Email, CAST((SELECT TOP 1 1 FROM Enrollments e WHERE e.student_id=s.student_id) as bit) as enrolled FROM Student s
I think you can also use the exists statement in the select but not positive
try to avoid using udfs or subqueries, they are performance killers. banjolity seems to havea good solution otherwise because it uses a derivd table instead of a UDF or subselect.
select students.name,
decode(count(1), 0, "no enrollments", "has enrollments")
from students, enrollments
where
students.id = enrollments.sutdent_id and
enrollments.is_active = 1 group by students.name
Of course, replace the decode with a function your database uses (or, a case statement).