SQL statement that shows at least 16 courses not taken - mysql

I have four schemas:
takes(ID,course_id,sec_id,semester,year)
student(ID,name,dept_name,tot_credit)
course(course_id,title,dept_name,credits)
department(dept_name,building,budget)
I want to create a query that finds the name and id of each Astronomy student whose name begins with the letter ’T’ and who has not taken at least 16 Astronomy courses.
What's the easiest way I could do this?
I already wrote this beginning bit
SELECT name, id
FROM student
WHERE dept_name='Astronomy' AND name LIKE '%T%'
I'm not quite sure how to finish this off.
Any help would be greatly appreciated :)
Here's the result
NAME ID CLASS_TAKEN
-------------------- ----- -----------
Tolle 38279 12
Teo 62268 13
Tolle 93223 13
Tsukamoto 17707 5
Titi 11576 9
Teo 91772 12
Toraichi 50387 11
Tewari 80754 14
Tiroz 64091 14
9 rows selected
I need Teo with the id 91772 and Tewari 80754 to be gone

Given my reading of the requirements and the comments it's pretty clear that the question is not very clear. :-) What you're looking for are students where (total # of courses given by the Astronomy department) - (# of Astronomy courses taken by student) >= 16. So, how do we find these values? First, let's start with the total number of courses given by the Astronomy department. This is pretty simple:
SELECT COUNT(*) AS ASTRONOMY_COURSE_COUNT
FROM COURSE
WHERE DEPT_NAME = 'ASTRONOMY'
Now, the second part is to determine how many courses given by the Astronomy department each student has taken. To do this we need to start with the student, join to the courses the student has taken (the TAKES table), then join to the COURSES table to find out which department each course is part of. Something like the following should do it:
SELECT s.ID, s.NAME, COUNT(*) AS STUDENT_ASTRO_COUNT
FROM STUDENT s
INNER JOIN TAKES t
ON t.ID = s.ID
INNER JOIN COURSE c
ON c.COURSE_ID = t.COURSE_ID
WHERE c.DEPT_NAME = 'ASTRONOMY' AND
s.NAME LIKE 'T%'
GROUP BY s.ID, s.NAME;
OK, now we need to put this together. You've tagged this question for both Oracle and MySQL so I'm going to guess you'll accept valid syntax for either database; thus I'll use Oracle Common Table Expression syntax to pull everything together:
WITH ASTRONOMY_COURSES AS (SELECT COUNT(*) AS ASTRONOMY_COURSE_COUNT
FROM COURSE
WHERE DEPT_NAME = 'ASTRONOMY'),
STUDENT_ASTRO_COURSES AS (SELECT s.ID,
s.NAME,
COUNT(*) AS STUDENT_ASTRO_COUNT
FROM STUDENT s
INNER JOIN TAKES t
ON t.ID = s.ID
INNER JOIN COURSE c
ON c.COURSE_ID = t.COURSE_ID
WHERE c.DEPT_NAME = 'ASTRONOMY' AND
s.NAME LIKE 'T%'
GROUP BY ID)
SELECT s.ID,
s.NAME,
s.STUDENT_ASTRO_COUNT,
a.ASTRONOMY_COURSE_COUNT - s.STUDENT_ASTRO_COUNT AS UNTAKEN_COUNT
FROM STUDENT_ASTRO_COURSES s
CROSS JOIN ASTRONOMY_COURSES a
WHERE a.ASTRONOMY_COURSE_COUNT - s.STUDENT_ASTRO_COUNT >= 16;
Note here that a CROSS JOIN is used to put together the subqueries. This means that all the rows of each subquery are joined to all the rows of the other subquery - but since in this case the ASTRONOMY_COURSES subquery will only return a single row what we're doing is appending the ASTRONOMY_COURSE_COUNT value onto each row returned by the STUDENT_ASTRO_COURSES subquery.
That should at least get you pretty close. Amend as needed.
Not tested on animals - you'll be first! :-)
Share and enjoy.

Do you need to use all tables?
Table department has no links with the student,
Table takes has no links with the student,
Table coursehas no links with the student.
If student lists total credits that are all Astronomy I think this can be used:
select name, id, MAX(tot_credit) as credits
from student
where dept_name='Astronomy' and name like 'T%'
group by name, id
having MAX(tot_credit)<=16
PS - you schema is not good; PK-FK references are missing

Your query will need to reference more tables than just the student table.
Your tables seem be missing some important information, which student has taken which course. There's a table named takes, but there doesn't appear to be any relationship between takes and student.
So first, figure out how to list the students along with the Astronomy courses they have taken. Each row will identify the student and a course.
SELECT s.id AS student_id
, s.name AS student_name
, t.???
FROM student s
JOIN ??? t
ON t.student_id = s.id
WHERE ...
You may also need to include another "join" to an additional table, in order to identify which course a student has taken is an Astronomy course.
To also include students that have not take any Astronomy courses, you can use an outer join, rather than an inner join. (That would mean including the LEFT keyword before JOIN, and relocating predicates from the WHERE clause to the ON clause. (A predicate in the WHERE clause that can only be satisfied by non-NULL values will negate the outer-ness of the join.)
Once you have a query that returns that set (students along with any astronomy courses they've taken), you can then add a GROUP BY clause to "collapse" a set of rows into a single row. (Looks like you want the rows "grouped" by student.)
And then an aggregate function like COUNT() or SUM() can be used to get a count of rows for each group. (If you don't want to count any re-takes of a course (a "duplicate" course for a student) you may be able to make use of the COUNT(DISTINCT t.foo) form.
And then a HAVING clause can be added to the query, to compare the value returned from the aggregate expression to a constant value, to return only rows that satisfy a specific condition.
FOLLOWUP
Assuming:
CREATE TABLE course
( id INT UNSIGNED NOT NULL COMMENT 'PK'
, title VARCHAR(30) NOT NULL COMMENT 'course title'
, dept_name VARCHAR(30) NOT NULL COMMENT 'FK ref dept.name'
, credits DECIMAL(5,2) COMMENT 'credit hours'
, PRIMARY KEY (id)
);
CREATE TABLE student
( id INT UNSIGNED NOT NULL COMMENT 'PK'
, name VARCHAR(30) NOT NULL COMMENT 'student name'
, dept_name VARCHAR(30) NOT NULL COMMENT 'FK ref dept.name'
, tot_credit INT COMMENT '?'
, PRIMARY KEY (id)
);
CREATE TABLE takes
( student_id INT UNSIGNED NOT NULL COMMENT 'FK ref student.id'
, course_id INT UNSIGNED NOT NULL COMMENT 'FK ref course.id'
, sec_id INT UNSIGNED NOT NULL COMMENT '?'
, semester INT UNSIGNED NOT NULL COMMENT '?'
, year INT UNSIGNED NOT NULL COMMENT '?'
, PRIMARY KEY (student_id, course_id, sec_id, semester, year)
, CONSTRAINT FK_takes_course FOREIGN KEY (course_id) REFERENCES course (id)
, CONSTRAINT FK_takes_student FOREIGN KEY (student_id) REFERENCES student (id)
);
Query to get a list of students...
SELECT s.id
, s.name
FROM student s
WHERE s.name LIKE 'T%'
AND s.dept_name = 'ASTRONOMY'
Get list of students along with the courses they've taken, returning the id of the ASTRONOMY courses they've taken...
SELECT s.id AS student_id
, s.name AS student_name
, c.id AS course_id
FROM student s
LEFT
JOIN takes t
ON t.student_id = t.id
LEFT
JOIN course c
ON c.id = t.course_id
AND c.dept_name = 'ASTRONOMY'
WHERE s.name LIKE 'T%'
AND s.dept_name = 'ASTRONOMY'
Collapse the rows to one per student using a GROUP BY, and use aggregate functions to get counts or totals.
SELECT s.id AS student_id
, s.name AS student_name
, SUM(c.credits) AS total_astronomy_credits_taken
, COUNT(c.id) AS count_astronomy_courses_taken
, COUNT(DISTINCT c.id) AS count_distinct_astronomy_courses_taken
FROM student s
LEFT
JOIN takes t
ON t.student_id = t.id
LEFT
JOIN course c
ON c.id = t.course_id
AND c.dept_name = 'ASTRONOMY'
WHERE s.name LIKE 'T%'
AND s.dept_name = 'ASTRONOMY'
GROUP
BY s.id
, s.name
To omit rows from this resultset, add a HAVING clause. For example, to exclude rows where total_astronomy_credits_taken is greater than or equal to 12...
HAVING total_astronomy_credits_taken >= 12
If you want the rows returned in a certain sequence, specify that in an ORDER BY clause
ORDER BY s.id
If you want to replace NULL values from the aggregates with zeroes, you can warp the aggregate expression in an IFNULL(foo,0) function, e.g.
, IFNULL(COUNT(c.id),0) AS count_astronomy_courses_taken

Try this :
select a.name, a.id, count(b.ID) as class_taken
from student a inner join takes b
on a.ID = b.ID
inner join course c
on b.course_id = c.course_id
where a.dept_name='Astronomy' and substring(a.name,1,1) = 'T'
group by a.name, a.id
having count(b.ID) < 17

Related

How do I write an SQL query that gives me the amount of times a value is repeated in a table?

So I have two tables which I have created:
CREATE TABLE IF NOT EXISTS `Advertising_Campaign` (
`CampaignID` VARCHAR(10) NOT NULL,
`AdvertName` varchar(45) NOT NULL,
`ProjectLead` VARCHAR(10) NULL,
`CostEstimate` decimal NULL,
`CampaignCost` decimal NULL,
`EndDateEst` date NULL,
`StartDate` date NULL,
`EndDate` date NULL,
`Theme` VARCHAR(45) NOT NULL,
`AdvertType` VARCHAR(45) NOT NULL,
PRIMARY KEY (`CampaignID`))
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `staff_works_campaign` (
`CampaignID` VARCHAR(10) NOT NULL,
`StaffID` VARCHAR(10) NOT NULL,
`SalaryGrade` Integer NOT NULL,
`isSup` VARCHAR(3) NOT NULL,
PRIMARY KEY (`StaffID`, `CampaignID`),
CONSTRAINT `FK_StaffID3` FOREIGN KEY (`StaffID`) REFERENCES `Staff` (`StaffID`),
CONSTRAINT `FK_CampaignID2` FOREIGN KEY (`CampaignID`) REFERENCES `Advertising_Campaign` (`CampaignID`))
ENGINE = InnoDB;
which gives the tables:
Basically, I want to write a query that will return me a list of the advertising_campaign.AdvertName with more than 2 staff members working on them and a count of the number of staff members whose staff_works_campaign.SalaryGrade is greater than 2.
I have tried:
select a.advertname, count(*) as 'Greater Than 2'
from advertising_campaign a inner join staff_works_campaign
where staff_works_campaign.SalaryGrade > 2;
Which isn't exactly what I want, it returns:
I am a bit unsure at what this is returning exactly because I would thought it would have returned a count of 2(because of the fact that there are 2 entries with a SalaryGrade of 4 in the table), might be because of the way inner join works?
I am also a bit confused as to how to filter for 'more than 2 staff members', My idea is to see the amount of times the staff_works_campaign.CampaignID has appeared in the staff_works_campaign table to see how many staff members are apart of the same campaign.
I'm not sure how to structure it to count the amount of times campaignID is repeated and to return the names of the adverts that have a campaignID that has 2 or more staff members working on it.
So in this case I would want it to return a table with AdvertName of only those campaigns with two or more people working on them and a count of those staff members who have a salary grade greater than 2.
SELECT
a.CampaignID
,a.AdvertName
,COUNT(DISTINCT s.StaffID) AS [Count of staff]
,SUM(
--Use this to get a total of the staff who are
--in a SalaryGrade greater than 2
CASE WHEN s.SalaryGrade > 2
THEN 1
ELSE 0 --anyone who is under this level will be a 0 and not count
END
) as [Count of staff above salary grade]
FROM
advertising_campaign AS a
INNER JOIN staff_works_campaign AS s
--dont forget the join condition
ON a.CampaignID = s.CampaignID
--Dont want a where here, we want to include ALL staff.
--WHERE
-- staff_works_campaign.SalaryGrade > 2
GROUP BY
a.CampaignID
,a.AdvertName
HAVING
--more than two members of staff working on the same campaign.
COUNT(DISTINCT s.StaffID) > 2
Firstly you need a condition to join the two tables on. Secondly you can use a Group By and Having clause to put the filter on aggregation. Finally you need to count the number of staff with a salary grade > 2, which you can SUM a conditional for. Something like this:
select a.advertname, Sum(CASE WHEN c.SalaryGrade > 2 THEN 1 ELSE 0 END) as 'Greater Than 2'
from advertising_campaign a inner join staff_works_campaign c
on a.CampaignId = c.CampaignId
Group By a.advertname Having count(*) >= 2;
You can do something like following if you want both conditions together,
2 people working on camp, whose salary_grade >2
SELECT AdvertName
FROM Advertising_Campaign
WHERE CampaignID IN
(
SELECT
CampaignID
FROM
staff_works_campaign
WHERE
SalaryGrade > 2
GROUP BY
CampaignID
HAVING
COUNT(DISTINCT StaffID) >= 2
)
What you have received as a result from your query is the count of all staff members across all campaigns that have a salary grade greater than 2. It returns "Star Wars 3" as the advert name simply because it's the first name it came across in all the results that the COUNT operates over. (Some other SQL technologies such as Microsoft SQL Server actually won't allow you to do this kind of query to avoid this confusion.)
In order to get the results to be split by the campaign, you have to use the GROUP BY clause as suggested in the other answers. This will tell SQL to calculate any aggregate functions (i.e. COUNT) over groups of records that all match for one or more fields. In your case, you want to group by the campaignID, since you want the COUNT to be calculated for each campaign individually. You could do this on the advert name as well, but better to do it on the ID in case you have two with the same name. Modifying your query to do that, we get:
select a.campaignID, count(*) as 'Greater Than 2'
from advertising_campaign a inner join staff_works_campaign
where staff_works_campaign.SalaryGrade > 2
group by a.campaignID;
This still isn't quite going to work though, because the salary grade condition is applied before the COUNT. We need to move that part out into a new query that wraps around this one. We also need to limit the campaigns down to those with two staff - thankfully, we don't need yet another outer query for that. The HAVING keyword allows a condition to be applied after a GROUP BY, so we can do:
select a.campaignID, count(*) as 'staff_amount'
from advertising_campaign a inner join staff_works_campaign
group by a.campaignID
having staff_amount > 2;
Now, adding the staff salary condition and another select from advertising_campaign to get the advert name in an outer query, we finally get:
select advertising_campaign.advertname
from advertising_campaign
inner join staff_works_campaign on advertising_campaign.campaignid = staff_works_campaign.campaignid
inner join
(
select a.campaignID, count(*) as 'staff_amount'
from advertising_campaign a inner join staff_works_campaign
group by a.campaignID
having staff_amount > 2
) large_campaigns on advertising_campaign.campaignid = large_campaigns.campaignid
where staff_works_campaign.salarygrade > 2

how does the count function in mysql work?

First and foremost, this is part of an assignment, but I'm trying to get more clarification on how the count() function works when querying a db.
The question is: List the name, SSN and the number of courses the student has taken (courses with the same CourseNumber taken in different quarters are counted as different courses).
I've been querying using this:
SELECT S.Name, S.SSN, COUNT(*)
FROM Student S, Enrollment E
WHERE S.SSN = E.SSN
GROUP BY S.SSN
which seems to return the correct answer, but I'm not sure why. As a result, I can't seem to get the next questions (assuming courses with the same CourseNumber taken in different quarters are considered as one course) correct.
Here are the create table commands so you can see which table holds what info:
CREATE TABLE Student(
SSN INT(9),
Name VARCHAR(20),
Major VARCHAR(30),
PRIMARY KEY (SSN)
);
CREATE TABLE Course(
CourseNumber INT(5),
PrerequisiteCourseNumber INT(10),
CourseTitle VARCHAR(10),
NumberUnits INT(2),
PRIMARY KEY (CourseNumber)
);
CREATE TABLE Section(
CourseNumber INT(5),
Quarter VARCHAR(10),
RoomNumber INT(5),
DayTime VARCHAR(20),
PRIMARY KEY (CourseNumber,Quarter),
FOREIGN KEY (CourseNumber) REFERENCES Course(CourseNumber)
);
CREATE TABLE Enrollment(
SSN INT(9),
CourseNumber INT(5),
Quarter VARCHAR(10),
Grade VARCHAR(1),
PRIMARY KEY (SSN,CourseNumber,Quarter),
FOREIGN KEY (SSN) REFERENCES Student(SSN),
FOREIGN KEY (CourseNumber) REFERENCES Course(CourseNumber),
FOREIGN KEY (Quarter) REFERENCES Section(Quarter)
);
Any pointers?
what you are doing is using old join syntax (pre ansi syntax) where you do a cross join of tables and use the where to turn it into a join. an equivalent query would be this.
SELECT S.Name, S.SSN, COUNT(*)
FROM Student S
JOIN Enrollment E ON S.SSN = E.SSN
GROUP BY S.SSN
Now to answer your question about what the count is doing and stuff..
COUNT() returns a count of every row that is returned.
GROUP BY groups all of the records by a common ground aka your SSN field. so if a student has 5 SSN rows in the table then his count will be 5.
Now for the part you are looking for with course numbers.. you need to JOIN those tables on appropriate fields and add a field to your group by for each quarter
aka add to the previous code..
WHERE E.quarter = whatever_quarter_you_want
you can add more to this query if you need to. but add data to your question if you want a more full answer.
Your current query is:-
SELECT S.Name, S.SSN, COUNT(*)
FROM Student S
INNER JOIN Enrollment E
ON S.SSN = E.SSN
GROUP BY S.SSN
What this is doing is joining student to enrollment, so giving multiple rows for each student, one for each course / quarter they are enrolled on. It is then grouped by SSN (student number?) to count up the number of course / quarters they are enrolled on. You should really group by S.Name as well (while MySQL won't object, most flavours of SQL would).
Note that COUNT(*) counts the number of rows. You could use COUNT(E.CourseNumber) which would count the number of rows where course number is not null. Not really useful here, but can be useful with LEFT OUTER JOINs. You can also use COUNT(DISTINCT CourseNumber) to count the number of unique non null course numbers for the student.
A LEFT OUTER JOIN might also be better as this would enable you to return 0 as the counts for students who exist but who are not enrolled in any courses:-
SELECT S.Name, S.SSN, COUNT(DISTINCT E.CourseNumber)
FROM Student S
LEFT OUTER JOIN Enrollment E
ON S.SSN = E.SSN
GROUP BY S.Name, S.SSN
SELECT S.Name, S.SSN, COUNT(*)
FROM Student S, Enrollment E, Section Sec, Course C
WHERE S.SSN = E.SSN AND E.CourseNumber = Sec.CourseNumber
AND Sec.CourseNumber = C.CourseNumber AND Sec.Quarter like E.Quarter
GROUP BY S.Name, S.SSN

Write a query to find the students with the same first name AND last name?

So here is what I have tried:
SELECT A.STUDENT_ID
FROM STUDENT A, STUDENT B
WHERE A.FNAME=B.FNAME
AND A.LNAME=B.LNAME
AND A.STUDENT_ID!=B.STUDENT_ID;
Here is the STUDENT table columns:
STUDENT_ID primary key
FNAME
LNAME
It seems to work. But according to the practice exam, it is worth 15 points so I am not sure if it is totally right. What if there more that two students have the same fname and lname? How would I write it or would this work for that too?
To avoid the Cartesian product (which is what you get with those multiple FROM clauses) and the duplicate rows that would create, I'd use an EXISTS clause
SELECT a.STUDENT_ID, a.FNAME, a.LNAME
FROM STUDENT a WHERE EXISTS (
SELECT 1 FROM STUDENT b
WHERE a.FNAME = b.FNAME
AND a.LNAME = b.LNAME
AND a.STUDENT_ID <> b.STUDENT_ID
);
http://sqlfiddle.com/#!9/75fd6/3
You would also benefit greatly by having an index on both FNAME and LNAME but I doubt that's going to be relevant to your practice exam.
What you have will technically work. However, the number of rows returned per matched student will equal n - 1, where n is the number of students who have the same first and last name as the matched student.
To address this, use a DISTINCT clause in your query.
SELECT DISTINCT A.STUDENT_ID
FROM STUDENT A, STUDENT B
WHERE A.FNAME=B.FNAME
AND A.LNAME=B.LNAME
AND A.STUDENT_ID!=B.STUDENT_ID;

mysql update table column from a select statement with count

I am working on some MySQL and am running into a bit of a problem.
I'm trying to update a column with the correct amount of credits taken by a student, but currently when I count the credits taken I get the total number of credits taken by all students. I
student table
ID varchar(5)
name varchar(20)
dept_name varchar(20)
tot_cred decimal(3,0)
takes table
ID varchar(5)
course_id varchar(8)
sec_id varchar(8)
semester varchar(6)
year decimal(4,0)
grade varchar(2)
course table
course_id varchar(8)
title varchar(50)
dept_name varchar(20)
credits decimal(2,0)
This is my statement that I'm currently using. I also have to add if they have an f then they don't get credit for the class that they took
update studentCopy set tot_cred = (
select sum(course.credits)
from student
left join takes on student.ID = takes.ID
left join course on takes.course_id = course.course_id
where student.ID = student.ID
group by studentCopy.ID);
Any help would be greatly appreciated. Thanks in advance!
Here are some simplifications to your query:
The left joins are unnecessary. You can just use join.
The group by is unnecessary. In fact, it is misleading, because it suggests that the subquery could return more than one row (which would generate an error).
The Student table is unnecessary; you can write the correlation clause directory to takes.
Table aliases make the query easier to write and to read.
So, I would write your query as:
update studentCopy sc
set tot_cred = (select sum(c.credits)
from takes t join
course c
on t.course_id = c.course_id
where sc.ID = t.ID
);
You can add the condition on grades in the where clause:
update studentCopy sc
set tot_cred = (select sum(c.credits)
from takes t join
course c
on t.course_id = c.course_id
where sc.ID = t.ID and grade <> 'f'
);
I actually got part of my answer just now. I changed
where student.ID = student.ID
to
where studentCopy.ID = student.ID
to give me the results of their own credits taken. Just working on getting the if grade = 'F' then don't count the grade.

Using Count and Max in SQL Query

I have two tables that I am trying to query from, Enrollment and Course. In the course Table, there is just one entry for each course, but in the Enrollment table, there is an entry for each student that is enrolled in any course, so there may be 30 entries for one course. My task is to find The course that has the most enrollments, and print out that course name, as well as the number of enrollments for that course. Here is my query so far
select c.CourseCode ,(SELECT count( * ) FROM Enrollment WHERE CourseCode = c.CourseCode) as test from Course c ;
this gives me the results:
CS227 - 29
CS228 - 34
CS309 - 31
CS311 - 25
, ect, which is good, but NOW, how do I print out only the class that has the most enrollments(in this case, CS228). I have tried using the max(), but I can't get anything to work.
Here is the table structure
create table Course(
CourseCode char(50),
CourseName char(50),
PreReq char (6));
create table Enrollment (
CourseCode char(6) NOT NULL,
SectionNo int NOT NULL,
StudentID char(9) NOT NULL references Student,
Grade char(4) NOT NULL,
primary key (CourseCode,StudentID),
foreign key (CourseCode, SectionNo) references Offering);
Just take the top 1 after ordering by the count.
That is:
Select Top 1 A.CourseCode, Count(*) From Course A inner join Enrollment B on (A.CourseCode=B.CourseCode)
Group By A.CourseCode
Order By Count(*) DESC
Also - use an inner join as I've shown here rather than a subquery. I do tend to like SubQueries and this one will work but it is just not appropriate in this kind of query!
Based on your comment, I think the blow query is what you want, although it is untested and I am not entirely sure on the HAVING clause being valid. From the documentation on MySQL's page, it seems it should work.
SELECT A.CourseCode, COUNT(*) AS count FROM Course A
JOIN Enrollment B ON A.CourseCode = B.CourseCode
GROUP BY A.CourseCode
HAVING count = MAX(count)
As for performance, I cannot tell if it's a good idea to run a MAX on an aggregate function (probably not).
Otherwise, just use the other query to return the top X and simply run through comparing to the previous number.
SELECT
c.CourseCode,
c.CourseName,
COUNT(*) AS cnt
FROM
Course AS c
INNER JOIN
Enrollment AS e
ON c.CourseCode = e.CourseCode
GROUP BY
c.CourseCode
HAVING
COUNT(*) =
( SELECT
COUNT(*) AS cnt
FROM
Enrollment AS e
GROUP BY
e.CourseCode
ORDER BY
cnt DESC
LIMIT 1
)