SQL inline view subquery - sql

Is it possible to reference an inline view defined in the "FROM" clause from a subquery in the WHERE clause?
SELECT tmp.TeacherName,
tmp.courseid,
tmp.AvgAttendingStudents
FROM (SELECT T.TeacherID AS ID,
T.TeacherName AS Name,
C.CourseID AS CourseID,
avg(L.AttendingStudents) AS AvgAttendingStudents
FROM Teachers AS T
join Courses AS C
ON C.TeacherID = T.TeacherID
join Lessons AS L
ON L.CourseID = C.CourseID
GROUP BY T.TeacherID,
C.CourseID) AS tmp
WHERE tmp.AvgAttendingStudents = (SELECT max(AvgAttendingStudents)
FROM tmp AS tmp2
WHERE tmp2.TeacherID = tmp.TeacherID);
In this example i'm trying to list all the teachers and for each of them I want to show the course having the maximum average of attending students (calculated on all lessons). I tried to use an inline view (tmp) to calculate the average number of attending students for each course, but I don't know if I can reference that view in the subquery SELECT max(...).
I need this to work with Oracle, but unfortunately at the moment I don't have any Oracle database to try it. I tried with MySQL (since I don't think I'm using any Oracle-specific features), but as expected I get the error "Table 'db.tmp' doesn't exist".
Is this somehow possible with Oracle?
Here's my example schema:
CREATE TABLE Courses
(
CourseID INTEGER PRIMARY KEY,
CourseName VARCHAR(32),
TeacherID INTEGER
);
CREATE TABLE Teachers
(
TeacherID INTEGER PRIMARY KEY,
TeacherName VARCHAR(32)
);
CREATE TABLE Lessons
(
LessonDate TIMESTAMP,
CourseID INTEGER,
AttendingStudents INTEGER,
PRIMARY KEY (LessonDate, CourseID)
);
(Sorry for my bad english)

You are right in that you can't reference the derived table ("inline view") that way. You need to rewrite the derived table ("inline view") to a common table expression:
You also have other errors in there. In the derived table you rename TeacherID to ID and TeacherName to Name so you need to use those column names not the "real" ones.
Also Oracle doesn't support AS for a table alias, so you need to get rid of those as well.
So a direct rewrite of the statement would be:
with tmp as (
SELECT T.TeacherID AS ID,
T.TeacherName AS Name,
C.CourseID AS CourseID,
avg(L.AttendingStudents) AS AvgAttendingStudents
FROM Teachers T
join Courses C
ON C.TeacherID = T.TeacherID
join Lessons L
ON L.CourseID = C.CourseID
GROUP BY T.TeacherID,
C.CourseID
)
SELECT tmp.name,
tmp.courseid,
tmp.AvgAttendingStudents
FROM tmp
where tmp.AvgAttendingStudents = (SELECT max(AvgAttendingStudents)
FROM tmp tmp2
WHERE tmp2.id = tmp.id);
However the above will not work in Oracle because of the invalid use of the group by and aggregate function. The above will result in "ORA-00979: not a GROUP BY expression", see this SQLFiddle
For this to work you need to use a window function in the CTE and get rid of the group by:
with tmp as (
SELECT T.TeacherID AS ID,
T.TeacherName AS Name,
C.CourseID AS CourseID,
avg(L.AttendingStudents) over (partition by t.teacherid, c.courseid) AS avgattendingstudents
FROM Teachers T
join Courses C
ON C.TeacherID = T.TeacherID
join Lessons L
ON L.CourseID = C.CourseID
)
SELECT tmp.name,
tmp.courseid,
tmp.AvgAttendingStudents
FROM tmp
where tmp.AvgAttendingStudents = (SELECT max(AvgAttendingStudents)
FROM tmp tmp2
WHERE tmp2.id = tmp.id);
See this SQLFiddle for an example.
Note that you can not test the above queries with MySQL because it does not support modern SQL like common table expressions or window functions.
But you can use the SQLFiddle examples to test it with data.

You may use having clause, which can provide you a way to constraint an agregate function.
Here an example :
SELECT T.TeacherID AS ID,
T.TeacherName AS Name,
C.CourseID AS CourseID,
avg(L.AttendingStudents) AS AvgAttendingStudents
FROM Teachers AS T
join Courses AS C
ON C.TeacherID = T.TeacherID
join Lessons AS L
ON L.CourseID = C.CourseID
GROUP BY T.TeacherID,
T.TeacherName
C.CoursesID
HAVING avg(L.AttendingStudents) = (SELECT max(AvgAttendingStudents)
FROM Teachers AS tmp2
WHERE tmp2.TeacherID = T.TeacherID);
I just remove your first nested query and change AvgAttendingStudents to avg(L.AttendingStudents) (beacause you can't work with variable onto Having clause) and add selected attributs in Group clause, I don't test but here the way to do the trick.
Don't forget to add your not agregated var selected in group clause.
Here a documentation on having clause.

Related

'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

Which Join for SQL plus query

I have 4 tables, I would like to select one column from each table, but only if the department has both 'Mick' and 'Dave working in it (must have both names, not one or the other). But it does not seem to be working properly:
SELECT SCHOOL_NAME, TOWN, COUNTY
FROM STUDENTS
NATURAL JOIN SCHOOLS NATURAL JOIN TOWNS NATURAL JOIN
COUNTIES
WHERE FIRST_NAME IN ('Mick','Dave)
/
I'm going wrong somewhere (probably lots of places :( ). Any help would be great
Don't use NATURAL JOIN. It is an abomination, because it does not take properly declared foreign key relationships into account. It only looks at the names of columns. This can introduce really hard to find errors.
Second, what you want is aggregation:
select sc.SCHOOL_NAME, t.TOWN, c.COUNTY
from STUDENTS st join
SCHOOLS sc
on st.? = sc.? join
TOWNS t
on t.? = ? join
COUNTIES c
on c.? = t.?
where FIRST_NAME in ('Mick', 'Dave')
group by sc.SCHOOL_NAME, t.TOWN, c.COUNTY
having count(distinct st.first_name) = 2;
The ? are placeholders for table and column names. If you are learning SQL, it is all the more important that you understand how columns line up for joins in different tables.
A where clause can only check the values in a single row. There is a separate row for each student, so there is no way -- with just a where -- to find both students. That is where the aggregation comes in.
You need at least three Join conditions, and properly end the string Dave with quote :
SELECT SCHOOL_NAME, TOWN, COUNTY
FROM SCHOOLS h
JOIN TOWNS t ON (t.id=h.town_id)
JOIN COUNTIES c ON (t.county_id=c.id)
WHERE EXISTS ( SELECT school_id
FROM STUDENTS s
WHERE s.first_name in ('Mick','Dave')
AND school_id = h.id
GROUP BY school_id
HAVING count(1)>1
);
SQL Fiddle Demo
You can use an analytic function in a sub-query to count the students who have the name Mick or Dave for each school_id (assuming that is your identifier for a school):
SELECT SCHOOL_NAME, TOWN, COUNTY
FROM ( SELECT *
FROM (
SELECT d.*,
COUNT(
DISTINCT
CASE WHEN FIRST_NAME IN ( 'Mick', 'Dave' ) THEN FIRST_NAME END
) OVER( PARTITION BY school_id )
AS num_matched
FROM STUDENTS d
)
WHERE num_matched = 2
)
NATURAL JOIN SCHOOLS
NATURAL JOIN TOWNS
NATURAL JOIN COUNTIES;
SQLFiddle
You would also be better to use an INNER JOIN and explicitly specify the join condition rather than relying on NATURAL JOIN.

sql - combining columns from different tables

I have four different tables
class(classID, className)
person(personID, name)
schedule(personID, classID)
enrollment(personID, grade)
What is the easiest way to get each distinct column in one table?
I understand that I would start with enrollment, get the personID and grade, add them to the result table, then use the personID to get the name as well as the classID, and then use the classID to get the className. I just don't know exactly how to do that.
It seems that you need something like this:
SELECT p.name personName, e.grade, c.className
FROM person p
JOIN enrollment e ON e.personId = p.personId
JOIN schedule s ON s.personId = p.personId
JOIN class c ON c.classId = s.classId
WHERE p.personId = 1;

Multiple joins onto same table

i have the following tables:
TABLE: teachers:
teacherID
teacherName
TABLE: students:
studentID
studentName
teacherID
advisorID
so, usually, i know i can get a single row per student, with their teachers name using an INNER JOIN.
but in this case - the advisor and tacher - are from the same teachers table. so how can i join onto the teachers table twice - once getting the teacher name, and then again to get the advisor name?
hope this is clear
thanks!
This lists students with the names of their teachers and advisors if any, in alpha order of student, without either (a) the teacher or (b) the advisor having to exist. If you want only where those names exist, change the respective join to an INNER join.
SELECT s.studentname as [Student], t.teachername as [Teacher], a.teachername as [Advisor]
FROM Students s
LEFT JOIN Teachers t ON s.TeacherID = t.TeacherID
LEFT JOIN Teachers a ON s.AdvisorID = a.TeacherID
ORDER BY 1, 2
You can join to the same table more than once, just give it a different alias for each join, and name your fields in a descriptive enough way. Use a left join if there might not be a link, but if a student always has both a teacher and an advisor, a straight join should be fine.
Something like this:
select s.studentname student
, t.teachername teacher
, a.teachername advisor
from students s
join teacher t
on t.teacherID = s.teacherID
join teacher a
on a.teacherID = s.teacherID
Why not try something like the following. Its been a while since I've done SQL so this may not work.
SELECT s.studentName AS Student, t.teacherName AS Teacher, a.teacherName AS Advisor
FROM teachers t, teachers a, students s
WHERE t.teacherID = s.teacherID AND a.teacherID = s.advisorID

LEFT JOIN confusion -- I need to retrieve a student count

I have four tables:
students
classes
teachers
teacher_assignments
classes and teachers has a many-to-many relationship and so teacher_assignments acts as the xref table (with fields teacher_id and class_id).
Each student in students has a class_id (many-to-one -- many students to one class).
I should also mention that teacher_assignments has an active column (BOOL) which indicates whether that assignment is currently active
What I want to do:
I want to retrieve the following:
class_name -- a concat of its level and sub_level, e.g. 3 and A
teacher_names -- the names of the teachers currently assigned to that class
student_count -- a count of the students in each class
At first, I tried retrieving just the class_name and teacher_names, like so:
SELECT
CONCAT(CONVERT(classes.level, CHAR(8)), classes.sub_level) AS class_name,
GROUP_CONCAT(DISTINCT teachers.name SEPARATOR ',') AS teacher_names
FROM
teacher_assignments
LEFT JOIN teachers
ON teachers.id = teacher_assignments.teacher_id
AND teacher_assignments.active = TRUE
LEFT JOIN classes
ON classes.id = teacher_assignments.class_id
GROUP BY classes.id
This works fine and outputs:
class_name | teacher_names
--------------------------------------
1A | NULL
2A | John, Sam
3B | Sam, Sarah
(Class 1A has no teachers currently, and so the NULL is expected)
... BUT, now I have no idea how to work the student_count into this.
My question:
How exactly should the students table be joined with the others in the above query so I can produce a student_count column?
Use:
SELECT CONCAT(CONVERT(c.level, CHAR(8)), c.sub_level) AS class_name,
GROUP_CONCAT(DISTINCT teachers.name SEPARATOR ',') AS teacher_names,
COUNT(s.id) AS studentCount
FROM CLASSES c
LEFT JOIN TEACHER_ASSIGNMENTS ta ON ta.class_id = c.id
AND ta.active = TRUE
LEFT JOIN TEACHERS t ON t.id = ta.teacher_id
LEFT JOIN STUDENTS s ON s.class_id = c.id
GROUP BY class_name
Column aliases can be referenced in the GROUP BY when using MySQL, otherwise you'd have to duplicate the logic that produces the class_name column value. This is also the column to GROUP on, as GROUP_CONCAT and COUNT are aggregate functions.
To get zero as the count value, you might need to use:
SELECT CONCAT(CONVERT(c.level, CHAR(8)), c.sub_level) AS class_name,
GROUP_CONCAT(DISTINCT teachers.name SEPARATOR ',') AS teacher_names,
COALESCE(COUNT(s.id), 0) AS studentCount
FROM CLASSES c
LEFT JOIN TEACHER_ASSIGNMENTS ta ON ta.class_id = c.id
AND ta.active = TRUE
LEFT JOIN TEACHERS t ON t.id = ta.teacher_id
LEFT JOIN STUDENTS s ON s.class_id = c.id
GROUP BY class_name
Just thinking off the top of my head...
Join classes and students tables to get the student count...
Instead of doing a left join on classes in your above query, you will do a left join with the result from #1 (essentially an inner join between classes and students tables) that allows you to pull the student count.
I dont think I would use a join but instead would use an inline column select on student like this:
SELECT
CONCAT(CONVERT(classes.level, CHAR(8)), classes.sub_level) AS class_name,
GROUP_CONCAT(DISTINCT teachers.name SEPARATOR ',') AS teacher_names,
( SELECT COUNT(*) FROM students WHERE students.class_id = classes.id ) AS student_count
FROM ...