sql select with exact outcome - sql

Asking a simple question, just want everyone have fun to solve it.
I got 2 tables.
1. Student
2. Course
Student
+----+--------+
| id | name |
+----+--------+
| 1 | User1 |
| 2 | User2 |
+----+--------+
Course
+----+------------+------------+
| id | student_id | course_name|
+----+------------+------------+
| 1 | 1 | English |
| 2 | 1 | Chinese |
| 3 | 2 | English |
| 4 | 2 | Japanese |
+----+------------+------------+
I would like to get the result all student, who have taken English and Chinese, NOT English or Chinese.
Expected result:
+----+------------+------------+
| id | student_id | course_name|
+----+------------+------------+
| 1 | 1 | English |
| 2 | 1 | Chinese |
+----+------------+------------+
What we normally do is
select * from student join course on (student.id = course.student_id) WHERE course_name = 'English' OR course_name = 'Chinese'
but in this result I can get User2 record which is not my expected result. I want the record only display the User take the course English+Chinese only.

Just join onto course twice: once for English, once for Chinese. That is:
select student.*
from student
join course english_course on student.id = english_course.student_id
join course chinese_course on student.id = chinese_course.student_id
where english_course.course_name = 'English'
and chinese_course.course_name = 'Chinese'
or even
select * from student
where exists (select 1 from course
where course.course_name = 'English' and course.student_id = student.id)
and exists (select 1 from course
where course.course_name = 'Chinese' and course.student_id = student.id);
which will also eliminate duplicate (student_id,course_name) entries from course.
I'm assuming (student_id,course_name) is indexed to drive both these. Your naming is a bit odd: the "course" table doesn't describe a course, it describes the association from a student to a course. Personally, I'd call it "student_course" (or similar, maybe suffix "_map" or "_link") and have it contain a "course_id" referencing a course table with an id and name.
(I also prefer to have primary keys named consistently rather than calling them "id" in their own table, but that's just being picky, and much more subjective)
Just for fun:
select student.*
from student
join course on student.id = course.student_id
where course.course_name = 'English'
intersect
select student.*
from student
join course on student.id = course.student_id
where course.course_name = 'Chinese'
The reality is that using "intersect" to compare two result sets based on the same tables is a bit silly.

You can use a having clause to enforce to matches:
select s.student_id
from student s
join course c
on s.id = c.student_id
and c.course_name in ('English', 'Chinese')
group by
s.student_id
having count(distinct c.course_name) = 2
To retrieve other columns, you could join on this query:
select *
from student s
join course c
on s.id = c.student_id
join (
<query from above here>
) filter on s.id = filter.student_id

You can use the IN operator
select * from student join course on (student.id = course.student_id)
WHERE course_name = 'English' and student.id IN
(select student.id from student join course on (student.id = course.student_id)
WHERE course_name = 'Chinese')

You would probably want to do a self-join:
SELECT left.student_id sid1, right.student_id sid2,
left.course_name cn1, right.course_name cn2
FROM course left, course right
WHERE sid1 = sid2 AND
cn1 = 'English' AND cn2 = 'Chinese'
Joining that to the student table shouldn't be difficult.

Related

Consolidate values from multiple tables without duplicate it

I have tried to do the below example using joins and union but was not successful. I appreciate any assistance.
I have a Student table and 3 other tables of courses Planed to be done, current Enrolled and, Concluded course. For each of those courses tables, I have the FK_Student and the Course name. I do like to "join" all results in a single line for each course name with each course table as a column name. (The same course can be in multiple tables)
See example below:
Table: Student
Id_Student | Student
1 Bob
2 ...
Table: Planed
Id_Planed | Course | Fk_Student
1 History 1
2 English 1
3 Biology 1
4 Geometry 1
5 PE 1
6 Algebra 1
....
Table: Enrolled
Id_Enrolled | Enrollment | Fk_Student
1 History 1
2 Biology 1
3 PE 1
...
Table: Concluded
Id_Concluded | Conclusion | Fk_Student
1 History 1
2 English 1
3 Physics 1
...
Expected Result:
Student | Planed | Enrolled | Concluded
Bob History History History
Bob English NULL English
Bob Biology Biology NULL
Bob Geometry NULL NULL
Bob PE PE NULL
Bob Algebra NULL NULL
Bob NULL NULL Physics
FULL OUTER JOIN is used here because every subject name are not existed in all table. First subquery retrieve student wise Course, Enrollment and Conclusion record. Then INNER JOIN with student table as per expected output. If all student info needed then LEFT JOIN will be better. In Planed/Enrolled/Concluded table same Course/Enrollment/Conclusion can not assigned multiple time for particular student. As student_id and course needed for calculation so use two tables student_id and course inside COALESCE() so that always return NOT NULL value.
-- SQL SERVER (v2014)
SELECT s.Student, r.Course, r.Enrollment, r.Conclusion
FROM Student s
INNER JOIN (SELECT COALESCE(t.student_id, c.Fk_Student) student_id
, t.Course, t.Enrollment, c.Conclusion
FROM (SELECT COALESCE(p.Fk_Student, e.Fk_Student) student_id
, COALESCE(p.Course, e.Enrollment) Course_t
, p.Course
, e.Enrollment
FROM Planed p
FULL OUTER JOIN Enrolled e
ON p.Fk_Student = e.Fk_Student
AND p.Course = e.Enrollment) t
FULL OUTER JOIN Concluded c
ON c.Fk_Student = t.student_id
AND c.Conclusion = t.Course_t) r
ON s.Id_Student = r.student_id;
Please check from url https://dbfiddle.uk/?rdbms=sqlserver_2014&fiddle=09d03c0b64d31c8ae5a3b91145b7b7e5
It looks like your data model is somewhat flawed in its design, I would have expected a table of Courses with each course linked by a Course_Id to each student for each type.
I'm not clear on your desired output but it seems like you want a complete list of courses for each student and which of each are applicable in each case.
You can use a CTE to build a table of truth for all courses which is then cross-joined to Students so each student is presented with the full list of courses and then outer-joined to the 3 tables to indicate which courses are applicable to the student on each case.
with courses as (
select course from planed union
select enrollment from enrolled union
select conclusion from concluded
)
select s.Student, p.Course Planed, e.Enrollment Enrolled, cc.Conclusion Concluded
from courses c
cross join student s
left join planed p on p.course=c.course and p.fk_student=s.id_student
left join enrolled e on e.enrollment=c.course and e.fk_student=s.id_student
left join concluded cc on cc.Conclusion=c.course and cc.fk_student=s.id_student
Try this (no nested query or CTEs):
SELECT
s.Student,
p.Course,
e.Enrollment,
c.Conclusion
FROM Planed AS p
FULL JOIN Enrolled AS e
ON e.Fk_Student = p.Fk_Student AND
e.Enrollment = p.Course
FULL JOIN Concluded AS c
ON (c.Fk_Student = p.Fk_Student AND
c.Conclusion = p.Course) OR
(c.Fk_Student = e.Fk_Student AND
c.Conclusion = e.Enrollment)
RIGHT JOIN Student AS s
ON s.Id_Student IN (
p.Fk_Student,
e.Fk_Student,
c.Fk_Student
);
Result:
+---------+----------+------------+------------+
| Student | Course | Enrollment | Conclusion |
+---------+----------+------------+------------+
| Bob | History | History | History |
| Bob | English | | English |
| Bob | Biology | Biology | |
| Bob | Geometry | | |
| Bob | PE | PE | |
| Bob | Algebra | | |
| Bob | | | Physics |
| Sam | | | |
+---------+----------+------------+------------+
db<>fiddle

SQL Count Number Of Classmates For a Student Across All Courses

I have the following tables:
Student: Student_ID (PK), Student_Name
Course: Course_ID (PK), Course_Name
Enrollment: Student_ID (FK), Course_ID (FK)
I need 2 queries:
A query that computes for each student id in the Student table the total number of different (Unique) classmates that this student has across all courses. If the student is not enrolled in any courses 0 should be returned.
For example if Student_ID 123 is enrolled in 3 courses and he has 10 different classmates in each of those courses, I should get the following result:
Student_ID Num_of_classmates
-----------------------------------
123 30
A SQL query that returns all students with all of their classmates. classmate of a student is another student who is enrolled in at least one same class.
It is unclear which of these you want:
Total students in all the classes (which would include a given student).
Total distinct students, since some classmates might be in multiple classes.
Total distinct students not including the given student ("I am not my own classmate").
In any case, the idea is basically two joins and aggregation:
select s.student_id,
count(ec.student_id) as total_classmates,
count(distinct s.student_id) as distinct_classmates,
count(distinct case when ec.student_id <> s.student_id then ec.student_id end) as distinct_other_classmates
from student s left join
enrollment e
on e.student_id = s.student_id left join
enrollment ec
on ec.class_id = e.class_id;
group by s.student_id;
Here I only give a solution to part 2) as #Gordon Linoff has done part 1) and you have also fixed the -1 problem.
A point: use inner join instead of left join here to avoid NULL in classmate names. Hopefully this also adds a little bit of help :)
Test dataset
if object_id('tempdb..#Student') is not null drop table #Student;
create table #Student (
Student_ID int PRIMARY key,
Student_Name varchar(50)
)
insert into #Student(Student_ID, Student_Name)
values (1,'Alice'), (2,'Bob'),(3,'Claire'),(4,'Danny'),(5,'Eve'),(6,'Frank');
if object_id('tempdb..#Course') is not null drop table #Course;
create table #Course (
Course_ID int PRIMARY key,
Course_Name varchar(50)
)
insert into #Course(Course_ID, Course_Name)
values (1,'Algorithm'), (2,'Bash'),(3,'Compiler'),(4,'Design Pattern'),(5,'Exception Handling');
if object_id('tempdb..#Enrollment') is not null drop table #Enrollment;
create table #Enrollment (
Student_ID int,
Course_ID int
)
insert into #Enrollment(Student_ID, Course_ID)
values (1,1),(1,3),
(2,2),(2,3),
(3,3),(3,4),
(4,1),(4,4),
(5,1),
(6,5); -- This Frank guy has no classmate
-- select * from #Student;
-- select * from #Course;
-- select * from #Enrollment;
Solution to 2)
select distinct
A.Student_Name as Student_Name,
D.Student_Name as Classmate_Name
from #Student as A
-- Student (A) -> Enrolled Courses (B)
inner join #Enrollment as B
on A.Student_ID = B.Student_ID
-- Enrollment Courses (B) -> Enrolled Students in that Course (C)
inner join #Enrollment as C
on B.Course_ID = C.Course_ID
and B.Student_ID <> C.Student_ID -- exclude self
-- Classmate's names
inner join #Student as D
on C.Student_ID = D.Student_ID
order by Student_Name, Classmate_Name;
Output
N.B. Frank has no classmates and do not show a NULL value.
| Student_Name | Classmate_Name |
|--------------|----------------|
| Alice | Bob |
| Alice | Claire |
| Alice | Danny |
| Alice | Eve |
| Bob | Alice |
| Bob | Claire |
| Claire | Alice |
| Claire | Bob |
| Claire | Danny |
| Danny | Alice |
| Danny | Claire |
| Danny | Eve |
| Eve | Alice |
| Eve | Danny |

Optimizing WHERE clause SQL query

I'm using SQL Server. I find myself doing complex queries in the WHERE clause with the following syntax:
SELECT ..
WHERE StudentID IS NULL OR StudentID NOT IN (SELECT StudentID from Students)
was wondering if there's a better approach/more cleaner way to replace it with because this is a small example of the bigger query I'm doing which includes multiple conditions like that.
As you can see I'm trying to filter for a specific column the rows which its column value is null or not valid id.
EDIT
Courses:
|CourseID | StudentID | StudentID2|
|-----------------------------------|
| 1 | 100 | NULL |
| 2 | NULL | 200 |
| 3 | 1 | 1 |
Students
|StudentID | Name |
|--------------------
| 1 | A |
| 2 | B |
| 3 | C |
Query:
SELECT CourseID
FROM Courses
WHERE
StudentID IS NULL OR StudentID NOT IN (SELECT * FROM Students)
OR StudentID2 IS NULL OR StudentID2 NOT IN (SELECT * FROM Students)
Result:
| CourseID |
|-----------|
| 1 |
| 2 |
As you can see, course 1 and 2 has invalid students.
Alain was close, except the studentID2 column is associated with the courses table. Additionally, this is joining each studentID column to an instance of the students table and the final WHERE is testing if EITHER of the student ID's fail, so even if Student1 is valid and still fails on Student2, it will capture the course as you are intending.
SELECT
C.CourseID
FROM
Courses C
LEFT JOIN Students S
ON C.StudentId = S.StudentId
LEFT JOIN Students S2
OR C.StudentId2 = S2.StudentId
WHERE
S.StudentId IS NULL
OR S2.StudentID IS NULL
this is not a sure shot but i have had experience that this is better performer than the question one:
SELECT CourseID from Courses WHERE
Courses.StudentID NOT exists (SELECT 1 FROM Students where Students.StudentID=nvl(Courses.StudentID,-1));
Also Create an index on StudentId in the Students Table.
And if your data model supports create a primary key foreign key relationship between the 2 tables. That way u definitely avoid invalid values in the courses table.
After your update:
SELECT CourseID from Courses WHERE
Courses.StudentID NOT exists (SELECT 1 FROM Students where Students.StudentID=nvl(Courses.StudentID1,-1) or Students.StudentID=nvl(Courses.StudentID2,-1));
The NOT EXISTS pattern is fine, however, you have several ways to do that.
You should check here and here
For example with LEFT JOIN (two left joins since two variables are checked)
SELECT *
from Courses
LEFT JOIN Students Student1
on Courses.StudentId = Student1.StudentId
LEFT JOIN Students Student2
on Courses.StudentId2 = Student2.StudentId
WHERE
-- No matching Student
student1.StudentId IS NULL
and student2.StudentId IS NULL

How to SQL in many-to-many relationship

I want to display how many hobbies does john have. Could you tell me how to write SQL statement?
PERSON table
ID | NAME
1 | John
HOBBY table
ID | NAME
1 | music
2 | sport
PERSON_HOBBY_COMBINATION table
ID | PERSON_ID | HOBBY_ID
1 | 1 | 1
expected result
HOBBY_NAME | HOBBY_EXIST
music | YES
sport | NO
This might work for you:
SELECT h.name,
CASE WHEN ph.id IS NULL THEN 'No' ELSE 'Yes' END AS hobby_exist
FROM hobby h
CROSS JOIN person p
LEFT JOIN person_hobby_conbination ph ON (ph.p_id = p.id AND ph.h_id = h.id)
WHERE (p.name = 'John')
Select NAME, count(*) AS NoHobbies from Person p
inner join PERSON_HOBBY_COMBINATION phc
on p.ID = phc.PERSON_ID
group by p.ID, p.NAME
Note that you should group on both ID and NAME of the person.
You need to group on NAME because you have it in the output, but if you have duplicate names grouping on NAME will sum several persons hobbies together, that is why you need to group on ID too.
Edit
When inspection your expected result you don't want how many hobbies John has, but which hobbies. Then you need to write
Select p.NAME as PersonName, h.Name as HobbyName, case when phc.ID is null then 'No' else 'Yes' end as HasHobby from Person p
inner join Hobby h
on 1 = 1
left outer join dbo.PersonHobbyCombination phc
on p.ID = phc.PersonID and h.ID = phc.HobbyID
If you already have John's ID and just want a raw count, then this should work.
select count(*) from person_hobby_combination where person_id=?

sql query related

Hi I have an "Student" table with below records
INSERT into Student(studId,name)
Values(1,A)
values(2,B)
values(3,C)
values(4,D)
I have a "Department" table with below records
INSERT into dept(deptId,Deptname,Emp Id)
Values(D1,Phy,1)
values(D2,Maths,2)
values(D3,Geo,3)
How can i find the student who does not belong to any department i.e. in this case the result should be "D".
I know the left outer join would return all the records from the student table but i am only interested to get 1 record i.e.: of student "D".
SELECT *
FROM Student
WHERE studId NOT IN (SELECT EmpId FROM dept)
Find all Student.studId entries that do not already exist in the dept.EmptId column as an entry. (Assuming I read your table correctly)
Ideally though, you should probably break out the "Student" and "Department" and creating a joining table (maybe called) "Student_Department" that links the keys of each table.
+--------------+ +--------------------+ +--------------+
| Student | | Student_Department | | Dept |
|--------------| |--------------------| |--------------|
| studId | <-----| studId | .-> | deptId |
| name | | deptId | --' | name |
| ... | +--------------------+ | ... |
+--------------+ +--------------+
This allows you to only have to define a student and department once, but can assign one student to multiple departments, one department to multiple students, or any combination therein.
Pseudo-code:
select *
from students
where id not in(select a.id from students a
inner join department b where b.id in('D1','D2','D3'))
While I agree with some commenters that this sounds like a homework problem, I'll answer with a question...
I know the left outer join would return all the records from the student table but i am only interested to get 1 record i.e.: of student "D".
OK, let's say you run the following query:
SELECT * FROM Student
LEFT OUTER JOIN dept ON Student.studId = dept.EmpId
You'd get the results:
studId name deptId deptName EmpId
1 A D1 Phy 1
2 B D2 Maths 2
3 C D3 Geo 3
4 D NULL NULL NULL
Can you add a WHERE clause to this query that will filter out only the data you want? :)
SELECT s.name
FROM Student s
LEFT JOIN Dept d ON d.empId = s.studId
WHERE d.empId IS NULL