SQL: tricky join - sql

I'm building software for exam signup and grading:
I need to get data from these two tables:
Exams
|------------------------------------------------------------|
| ExamId | ExamTitle | EducationId | ExamDate |
|------------------------------------------------------------|
ExamAttempts
|-----------------------------------------------------------------------------|
| ExamAttemptId | ExamId | StudentId | Grade | NotPresentCode |
|-----------------------------------------------------------------------------|
Students attends an education
Educations have multiple exams
Students have up to 6 attempts per exam
Every attempt is graded or marked as not present
Students can sign up for an exam if:
- not passed yet (grade below 2)
- has not used all attempts
I want to list every exams that a student can sign up for.
It maybe fairly simple, but I just can't get my head around it and now I'm stuck! I've tried EVERYTHING but haven't got it right yet. This is one of the more hopeless tries I made (!):
CREATE PROCEDURE getExamsOpenForSignUp
#EducationId int,
#StudentId int
AS
SELECT ex.*
FROM Exams ex
LEFT JOIN (
SELECT ExamId, COUNT(ExamId) AS NumAttempts
FROM ExamAttempts
WHERE StudentId = #StudentId AND grade < 2 OR grade IS NULL
GROUP BY ExamId
) exGrouped ON ex.ExamId = exGrouped.ExamId
WHERE educationid = #EducationId and exGrouped.ExamId IS NULL OR exGrouped.NumAttempts < 6;
GO
What am i doing wrong? Please help...

You need to start with a list of all possibilities of exams and students and then weed out the ones that don't meet the requirements.
select driver.StudentId, driver.ExamId
from (select #StudentId as StudentId, e.ExamId
from exams e
where e.EducationId = #EducationId
) driver left outer join
(select ea.ExamId, ea.StudentId
from ExamAttempts ea
group by ea.ExamId, ea.StudentId
having max(grade) >= 2 or -- passed
count(*) >= 6
) NotEligible
on driver.ExamId = NotEligible.ExamId and
driver.StudentId = NotEligible.StudentId
where NotEligible.ExamId is NULL
The structure of this query is quite specific. The driver table contains all possible combinations. In this case, you have only one student and all exams are in the "education". Then the left join determines which are not eligible, based on your two requirements. The final where is selecting the non-matches to the not-eligible -- or the exams that are eligible.

Check if this works in your SP:
Select EduExams.ExamId
from
(select * from Exams
where Exams.EducationId = #EducationId) EduExams
left outer join
(select * from ExamAttempts
where ExamAttempts.StudentId = #StudentId) StudentAttempts
on EduExams.ExamID = StudentAttempts.ExamId
group by EduExams.ExamId
having count(StudentAttempts.ExamAttemptId) < 6
and ((max(StudentAttempts.Grade) is null) or (max(StudentAttempts.Grade) < 2))

OK thank you both for your help - much appreciated!
Based on #Gordon Linoffs solution. this is what I ended up with:
SELECT driver.ExamId, driver.ExamTitle
FROM (
SELECT #StudentId AS StudentId, e.ExamId, e.ExamTitle
FROM exams e
WHERE e.EducationId = #EducationId
) driver
LEFT JOIN (
SELECT ea.ExamId, ea.StudentId
FROM ExamAttempts ea
WHERE ea.studentId = #StudentId
GROUP BY ea.ExamId, ea.StudentId
HAVING MAX(grade) >= 2 OR COUNT(*) >= 6
) NotEligible
ON driver.ExamId = NotEligible.ExamId AND driver.StudentId = NotEligible.StudentId
WHERE NotEligible.ExamId IS NULL

Related

How do i make this query with two conditions?

database structure
Professor
prof_id number
name string
salary number
building string
Course
name string
prof_id number
room_number number
start_time number
end_time number
Room
room_number number
capacity number
building string
Professors with at least 1 math course:
select distinct Professor.name, count(Course.name) AS numberOfMathCourses
from Course
LEFT JOIN Room
ON Course.Room_id = Room.Room.id
INNER JOIN Professor
ON Professor.id = Course.id
where Course.name = 'math'
group by Professor.name
having numberOfMathCourses > 0
Professors with less than 3 courses :
select distinct Professor.name, count(Course.name) AS numberOfCourses
from Course
LEFT JOIN Room
ON Course.Room_id = Room.Room.id
INNER JOIN Professor
ON Professor.id = Course.id
group by Professor.name
having numberOfCourses < 3
how would do I create a Query that has both of these conditions ? more than one math course course and less than 3 courses.
I tried sub-queries but I wasn't able to make it work. I will try to look into it more. Thanks for the help.
select *
from Professor p
where
(select count(*) from Course c where p.prof_id = c.prof_id and c.name = 'math') > 0
and (select count(*) from Course c where p.prof_id = c.prof_id) < 3

SQL Server : calculating percentage in Derived table

I'm trying to work with 3 tables and create a derived table to get some data together that shows a percentage of completion.
The 3 tables I have that I'm working with are Student, Tests and Results. I'm trying to join the 3 together and create a derived table that shows the student and the progress they have made, in a percentage of tests completed.
As an example, lets assume the 3 students I want to track have all been assigned 3 tests (out of a table with hundreds) and I want to see how far along they are. If they completed all 3 tests the derived table should store the value 100%.
StudentID SName
-----------------
1 Ken
2 Tom
3 Bob
TestID TName
----------------
11 Test 101
22 Test 102
33 Test 103
ResultsID TestID StudentID Passed
--------------------------------------
1 11 Tom 0
2 11 Bob 1
3 22 Bob 1
4 33 Bob 1
Derived table:
StudentID SName %Completed
---------------------------
1 Ken 0%
2 Tom 0%
3 Bob 100%
I have tried a lot of different methods and don't know which one to even show because I feel like all the attempts have been completely wrong. Any ideas? Sorry if the formatting isn't great, it's my first post here :)
Thanks!
From the data, we can't see that Ken has been assigned to the 3 tests, so we must assume that all tracked students have been assigned to all tracked tests.
Here's what I would do:
Build a set of all combinations of tracked students and tests (CROSS JOIN with 2 WHERE conditions)
Determine the results using a LEFT OUTER JOIN (replace NULL with 0)
Calculate the AVG result (casted to a number) while I GROUP BY student
This can be written in a single SELECT statement:
SELECT s.StudentID, s.SName, AVG(CAST(ISNULL(r.Passed, 0) as float)) AS Completed
FROM Students s CROSS JOIN Tests t
LEFT OUTER JOIN Results r ON t.TestID = r.TestID AND s.StudentID = r.StudentID
WHERE s.StudentID IN (1, 2, 3)
AND t.TestID IN (11, 22, 33)
GROUP BY s.StudentID, s.SName
The formatting (as percentage) of the Completed value should be done in the client application.
To get all possible combinations of students with the tests.
One could cross join to a sub-query of the tests the students should pass.
Then all possible combinations of student/test can be left joined to the actual results.
After that, it's a simple GROUP BY with an average.
select
stu.StudentID,
stu.SName,
concat(AVG(coalesce(res.Passed,0)*100),'%') as [%Completed]
from Students as stu
cross join (
select TestID, TName
from Tests
where TestID in (11,22,33)
) as tst
left join Results as res on (res.StudentID = stu.StudentID and res.TestID = tst.TestID)
group by stu.StudentID, stu.SName
Example snippet:
-- Sample data
-- Using table variables for demonstration
declare #Students table (StudentID int identity(1,1) primary key, SName varchar(30));
insert into #Students (Sname) values
('Ken'),
('Tom'),
('Bob'),
('Jane');
declare #Tests table (TestID int primary key, TName varchar(30));
insert into #Tests (TestID, TName) values
(11,'Test 101'),
(22,'Test 102'),
(33,'Test 103');
declare #Results table (ResultsID int identity(1,1) primary key, TestID int, StudentID int, Passed bit);
insert into #Results (TestID, StudentID, Passed) values
(11,2,0),
(11,3,1),(22,3,1),(33,3,1),
(11,4,1);
-- Query
select
stu.StudentID,
stu.SName,
concat(AVG(coalesce(res.Passed,0)*100),'%') as [%Completed]
from #Students as stu
cross join (
select TestID, TName
from #Tests
where TestID in (11,22,33)
) as tst
left join #Results as res on (res.StudentID = stu.StudentID and res.TestID = tst.TestID)
group by stu.StudentID, stu.SName;
Result:
StudentID SName %Completed
--------- ----- ----------
1 Ken 0%
2 Tom 0%
3 Bob 100%
4 Jane 33%
This seems like a join and group by with a twist:
select s.StudentID, s.SName,
sum(case when r.passed = 1 then 1.0 else 0 end) / t.cnt as ratio
from students s left join
results r
on s.studentid = r.studentid cross join
(select count(*) as cnt
from tests
) t
group by s.StudentID, s.SName, t.cnt;

SQL: Find common rows in different record

I have 3 tables:
Teacher Table (t_id, email, ...)
Student Table (s_id, email, ...)
Teaching Table (t_id, s_id, class_time, ...)
I have a task which is, given two t_id, find the common students that these 2 teachers have taught.
Is it possible to accomplish this in strictly SQL? If not I might try to retrieve out the student records individually based on different teacher, and do a search to see which students they have in common. This seems a bit overkill for something that seems possible to write a SQL query for.
You can self join to get students for both teachers.
DECLARE #TeacherID1 INT = 1
DECLARE #TeacherID2 INT = 2
SELECT
StudentID = T1.s_id,
Teacher1 = T1.t_id,
Teacher1ClassTime = T1.class_time ,
Teacher2 = T2.t_id,
Teacher2ClassTime = T2.class_time
FROM
TeachingTable T1
INNER JOIN TeachingTable T2 ON T2.s_id=T1._sid AND T2.t_id=#TeacherID2
WHERE
T1.t_id = #TeacherID1
ORDER BY
T1.ClassTime
select s_id
from student a
inner join teaching b on a.s_id = b.s_id
where t_id = 'First give t_id'
INTERSECT
select s_id
from student a
inner join teaching b on a.s_id = b.s_id
where t_id = 'Second give t_id'
This work with MS DB, but probably not with others.
select s_id
from student a
inner join teaching b on a.s_id = b.s_id
where b.t_id = 'First give t_id'
and s_id in (
select s_id
from student c
inner join teaching d on c.s_id = d.s_id
where d.t_id = 'Second give t_id'
)
the second one should work with any DB.

SQL query using NULL

I have two tables, student and school.
student
stid | stname | schid | status
school
schid | schname
Status can be many things for temporary students, but NULL for permanent students.
How do I list names of schools which has no temporary students?
Using Conditional Aggregate you can count the number of permanent student in each school.
If total count of a school is same as the conditional count of a school then the school does not have any temporary students.
Using JOIN
SELECT sc.schid,
sc.schname
FROM student s
JOIN school sc
ON s.schid = sc.schid
GROUP BY sc.schid,
sc.schname
HAVING( CASE WHEN status IS NULL THEN 1 END ) = Count(*)
Another way using EXISTS
SELECT sc.schid,
sc.schname
FROM school sc
WHERE EXISTS (SELECT 1
FROM student s
WHERE s.schid = sc.schid
HAVING( CASE WHEN status IS NULL THEN 1 END ) = Count(*))
You can use not exists to only select schools that do not have temporary students:
select * from school s
where not exists (
select 1 from student s2
where s2.schid = s.schid
and s2.status is not null
)
You can use a regular join.
SELECT DISTINCT c.schName
FROM Students s
INNER JOIN Schools c ON s.schid = c.schid
WHERE s.status IS NULL

Tricky statistics select

Trying to figure out this for a while, but no luck. I have the following tables (MS-SQL 2008):
students
studentID – email – profileID
courses
courseID – name
studentsCourses
studentID – courseID
profiles
profileID – name
profilesMandatoryCourses
profileID - courseID
studentsCoursesLogs
logID - studentID –courseID – accessDate
Each student, when enrolls, is assigned a profile. For each profile there are a number of mandatory course. Those mandatory courses together with any other courses a user takes are saved in the studentsCourses table.
Whenever a student accesses a course the information is logged in the studentsCoursesLogs.
I am trying to figure out all the students that have taken all the mandatory courses based on their profile.
Any pointers appreciated.
I think this should do it (assuming I've understood your data model and requirements correctly - and that in throwing this sample SQL together I've not made any mistakes - I didn't create your data model in a database). I'm pretty sure it should be close enough though.
select studentRecords.StudentId,
sum(case TakenCourseID when null then 0 else 1 end) as CompleteMandatoryCourses,
sum(case TakenCourseID when null then 1 else 0 end) as IncompleteMandatoryCourses
from (
select mandatoryCourses.StudentID, mandatoryCourses.CourseId as MandatoryCourseID, takenCourses.CourseID as TakenCourseID
from ( -- Courses student should have taken - based on their profile
select p.[Profile], pmc.CourseID, s.StudentID
from profiles p
inner join profilesMandatoryCourses pmc on p.ProfileID = pmc.Profile
inner join students s on p.StudentID = s.StudentID
) mandatoryCourses
left join
(
-- Course students have taken
select s.StudentID, s.ProfileID, sc.CourseID
from students s
inner join studentsCourseLogs scl on s.StudentID = scl.StudentID
) takenCourses on mandatoryCourses.ProfileId = takenCourses.ProfileID
and mandatoryCourses.CourseID = takenCourses.CourseID
) studentRecords
group by mandatoryCourse.StudentId
having sum(case TakenCourseID when null then 1 else 0 end) = 0
It will give a recordset like the following....
+----------------+-----------------------------+-------------------------------+
| StudentID | CompleteMandatoryCourses | IncompleteMandatoryCourses |
+----------------+-----------------------------+-------------------------------+
| 1 | 15 | 3 |
| 2 | 8 | 0 |
+----------------+-----------------------------+-------------------------------+
If you just want a list of students who have taken all mandatory courses, you could wrap all of the above as shown here...
select studentID
from ( /* insert very long sql here */ )
where IncompleteMandatoryCourses = 0`