user8574635
user8574635

Reputation:

SQL - EXIST OR ALL?

I have two different table student and grades; grade table has an attribute student_id which references student_id from student table. How do I find which student has every grade that exists?

If this is not clear,

Student     ID     Name
   1         1       John        
   2         2       Paul
   3         3       George
   4         4       Mike
   5         5       Lisa

Grade     Student_Id   Course   Grade
   1          1         Math       A
   2          1         English    B
   3          1         Physics    C
   4          2         Math       A
   5          2         English    A
   6          2         Physics    B
   7          3         Economics  A
   8          4         Art        C
   9          5         Biology    A

Assume there is only grade a,b,c (no d, e or fail) 

I want to find only John because He has grade a,b,c while other student like Paul(2) should not be selected because he does not have grade c. It does not matter which course he took, I just need to find if he has all the grades out there available.

Feel like I should something like exist or all function in sql but not sure. Please help. Thank you in advance.

Upvotes: 2

Views: 134

Answers (4)

JohnLBevan
JohnLBevan

Reputation: 24470

Before delving into the answer, here's a working SQL Fiddle Example so you can see this in action.

As Gordon Linoff points out in his excellent answer, you should use GroupBy and Having Count(Distinct ... ) ... as an easy way to check.

However, I'd recommend changing your design to ensure that you have tables for each concern.
Currently your Grade table holds each student's grade per course. So it's more of a StudentCourse table (i.e. it's the combination of student and course that's unique / gives you that table's natural key). You should have an actual Grade table to give you the list of available grades; e.g.

create table Grade
(
    Code char(1) not null constraint PK_Grade primary key clustered
)

insert Grade (Code) values ('A'),('B'),('C')

This then allows you to ensure that your query would still work if you decided to include grades D and E, without having to amend any code. It also ensures that you only have to query a small table to get the complete list of grades, rather than a potentially huge table; so will give better performance. Finally, it will also help you maintain good data; i.e. so you don't accidentally end up with students with grade X due to a typo; i.e. since the validation/constraints exist in the database.

select Name from Student s
where s.Id in 
(
    select sc.StudentId
    from StudentCourse sc
    group by sc.StudentId
    having count(distinct sc.Grade) = (select count(Code) from Grade)
)
order by s.Name

Likewise, it's sensible to create a Course table. In this case holding Ids for each course; since holding the full course name in your StudentCourse table (as we're now calling it) uses up a lot more space and again lacks validation / constraints. As such, I'd propose amending your database schema to look like this:

create table Grade
(
    Code char(1) not null constraint PK_Grade primary key clustered
)

insert Grade (Code) values ('A'),('B'),('C')

create table Course
(
    Id bigint not null identity(1,1) constraint PK_Course primary key clustered
    , Name nvarchar(128) not null constraint UK_Course_Name unique  
)

insert Course (Name) values ('Math'),('English'),('Physics'),('Economics'),('Art'),('Biology')

create table Student
(
    Id bigint not null identity(1,1) constraint PK_Student primary key clustered
    ,Name nvarchar(128) not null constraint UK_Student_Name unique  
)

set identity_insert Student on --inserting with IDs to ensure the ids of these students match data from your question

insert Student (Id, Name)
values (1, 'John')        
    ,  (2, 'Paul')
    ,  (3, 'George')
    ,  (4, 'Mike')
    ,  (5, 'Lisa')

set identity_insert Student off

create table StudentCourse
(
    Id bigint not null identity(1,1) constraint PK_StudentCourse primary key
    , StudentId bigint not null constraint FK_StudentCourse_StudentId foreign key references Student(Id)
    , CourseId bigint not null constraint FK_StudentCourse_CourseId foreign key references Course(Id)
    , Grade char /* allow null in case we use this table for pre-results; otherwise make non-null */ constraint FK_StudentCourse_Grade foreign key references Grade(Code)
    , Constraint UK_StudentCourse_StudentAndCourse unique clustered (StudentId, CourseId)
)

insert StudentCourse (StudentId, CourseId, Grade)
select s.Id, c.Id, x.Grade
from (values
   ('John',       'Math',     'A')
  ,('John',       'English',  'B')
  ,('John',       'Physics',  'C')
  ,('Paul',       'Math',     'A')
  ,('Paul',       'English',  'A')
  ,('Paul',       'Physics',  'B')
  ,('George',     'Economics','A')
  ,('Mike',       'Art',      'C')
  ,('Lisa',       'Biology',  'A')
) x(Student, Course, Grade)
inner join Student s on s.Name = x.Student
inner join Course c on c.Name = x.Course

Upvotes: 0

Zaynul Abadin Tuhin
Zaynul Abadin Tuhin

Reputation: 31991

By using exists

 select * from student s
  where exists ( select 1
                       from grades g where g.Student_Id=s.ID 
                        group by g.Student_Id
                        having count(distinct Grade)=3
               )  

Example

with Student as
(
select 1 as  id,'John' as person
union all
select 2 as  id,'Paul' as person
union all
select 3 as  id,'jorge'
),
Grades as
(
select 1 as Graden, 1 as Student_Id, 'Math' as Course, 'A' as Grade
union all
select 2 as Graden, 1 as Student_Id, 'English' as Course, 'B' as Grade 
union all
select 3 as Graden, 1 as Student_Id, 'Physics' as Course, 'C' as Grade
union all
select 4 as Graden, 2 as Student_Id, 'Math' as Course, 'A' as Grade 
union all
select 5 as Graden, 2 as Student_Id, 'English' as Course, 'A' as Grade 
union all
select 6 as Graden, 2 as Student_Id, 'Physics' as Course, 'B' as Grade 
)
select * from Student s
  where exists ( select 1
                       from Grades g where g.Student_Id=s.ID 
                        group by g.Student_Id
                        having count(distinct Grade)=3
               )

Note having count(distinct Grade)=3 i used this as in your sample data grade type is 3

Upvotes: 0

Gordon Linoff
Gordon Linoff

Reputation: 1270391

I would use GROUP BY and HAVING, but like this:

SELECT s.Name
FROM Student s JOIN
     Grade g
     ON s.ID = g.Student_Id
GROUP BY s.id, s.Name
HAVING COUNT(DISTINCT g.Grade) = (SELECT COUNT(DISTINCT g2.grade) FROM grade g2);

You say "all the grades out there", so the query should not use a constant for that.

Upvotes: 5

Aaron Dietz
Aaron Dietz

Reputation: 10277

You can use HAVING COUNT(DISTINCT Grade) = 3 to check that the student has all 3 grades:

SELECT Name
FROM Student S
JOIN Grade G ON S.ID = G.Student_Id
GROUP BY Name
HAVING COUNT(DISTINCT Grade) = 3

Guessing at S.ID vs S.Student on the join. Not sure what the difference is there.

Upvotes: 0

Related Questions