Update and renew data based on data in other tables - sql

There are 3 tables student, course, and takes as following
CREATE TABLE student
(
ID varchar(5),
name varchar(20) NOT NULL,
dept_name varchar(20),
tot_cred numeric(3,0) CHECK (tot_cred >= 0),
PRIMARY KEY (ID),
FOREIGN KEY (dept_name) REFERENCES department
ON DELETE SET NULL
)
CREATE TABLE takes
(
ID varchar(5),
course_id varchar(8),
sec_id varchar(8),
semester varchar(6),
year numeric(4,0),
grade varchar(2),
PRIMARY KEY (ID, course_id, sec_id, semester, year),
FOREIGN KEY (course_id, sec_id, semester, year) REFERENCES section
ON DELETE CASCADE,
FOREIGN KEY (ID) REFERENCES student
ON DELETE CASCADE
)
CREATE TABLE course
(
course_id varchar(8),
title varchar(50),
dept_name varchar(20),
credits numeric(2,0) CHECK (credits > 0),
PRIMARY KEY (course_id),
FOREIGN KEY (dept_name) REFERENCES department
ON DELETE SET NULL
)
tot_cred column data in the student table now is assigned with random values (not correct), I want to perform the query that updates and renews those data based on the course's grade each student has taken. For those students who received F grade will be excluded and those who didn't take any course will be assigned 0 as tot_cred.
I came up with two approaches, one is
UPDATE student
SET tot_cred = (SELECT SUM(credits)
FROM takes, course
WHERE takes.course_id = course.course_id
AND student.ID = takes.ID
AND takes.grade <> 'F'
AND takes.grade IS NOT NULL)
This query meets all my needs, but for those students who didn't take any course, it does assign NULL value instead of 0.
The second is using case when
UPDATE student
SET tot_cred = (select sum(credits)
case
when sum(credits) IS NOT NULL then sum(credits)
else 0 end
FROM takes as t, course as c
WHERE t.course_id = c.course_id
AND t.grade<>'F' and t.grade IS NOT NULL
)
But it assigned 0 to all students. Is any way to achieve the above requirement?

If the 1st query meets your requirement and the only problem is that it returns NULL for the students that did not take any course then the easiest solution would be to use instead of SUM() aggregate function the function TOTAL() which will return 0 instead of NULL:
UPDATE student AS s
SET tot_cred = (
SELECT TOTAL(c.credits)
FROM takes t INNER JOIN course c
ON t.course_id = c.course_id
WHERE t.ID = s.ID AND t.grade <> 'F' AND t.grade IS NOT NULL
);
The same could be done with COALESCE():
SELECT COALESCE(SUM(credits), 0)...
Also, use a proper join with an ON clause and aliases for the tables to improve readability.

Related

How to join Views with aggregate functions?

My problem:
In #4, I'm having trouble joining two Views because the other has an aggregate function. Same with #5
Question:
Create a view name it as studentDetails, that would should show the student name, enrollment date, total price per unit and subject description of students who are enrolled on the subject Science or History.
Create a view, name it as BiggestPrice, that will show the subject id and highest total price per unit of all the subjects. The view should show only the highest total price per unit that are greater than 1000.
--4.) Create a view name it as studentDetails, that would should show the student name,
-- enrollment date the total price per unit and subject description of students who are
-- enrolled on the subject Science or History.
CREATE VIEW StudentDetails AS
SELECT StudName, EnrollmentDate
--5.) Create a view, name it as BiggestPrice, that will show the subject id and highest total
-- price per unit of all the subjects. The view should show only the highest total price per unit
-- that are greater than 1000.
CREATE VIEW BiggestPrice AS
SELECT SubjId, SUM(Max(Priceperunit)) FROM Student, Subject
GROUP BY Priceperunit
Here is my table:
CREATE TABLE Student(
StudentId char(5) not null,
StudName varchar2(50) not null,
Age NUMBER(3,0),
CONSTRAINT Student_StudentId PRIMARY KEY (StudentId)
);
CREATE table Enrollment(
EnrollmentId varchar2(10) not null,
EnrollmentDate date not null,
StudentId char(5) not null,
SubjId Number(5) not null,
constraint Enrollment_EnrollmentId primary key (EnrollmentId),
constraint Enrollment_StudentId_FK foreign key (StudentId) references Student(StudentId),
constraint Enrollment_SubjId_Fk foreign key (SubjId) references Subject(SubjId)
);
Create table Subject(
SubjId number(5,0) not null,
SubjDescription varchar2(200) not null,
Units number(3,0) not null,
Priceperunit number(9,0) not null,
Constraint Subject_SubjId_PK primary key (SubjId)
);
Since this appears to be a homework question.
You need to use JOINs. Your current query:
CREATE VIEW StudentDetails AS
SELECT StudName, EnrollmentDate
Does not have a FROM clause and the query you have for question 5 uses the legacy comma join syntax with no WHERE filter; this is the same as a CROSS JOIN and will connect every student to every subject and is not what you want.
Don't use the legacy comma join syntax and use ANSI joins and explicitly state the join condition.
SELECT <expression list>
FROM student s
INNER JOIN enrollment e ON ...
INNER JOIN subject j ON ...
Then you can fill in the ... based on the relationships between the tables (typically the primary key of one table = the foreign key of another table).
Then for the <expression list> you need to include the columns asked for in the question: student name and enrolment date and subject name would just be those columns from the appropriate tables; and total price-per-unit (which I assume is actually total-price-per-subject) would be a calculation.
Then for the last part of question 4.
who are enrolled on the subject Science or History.
Add a WHERE filter to only include rows for those subjects.
For question 5, you do not need any JOINS as the question only asks about details in the SUBJECT table.
You need to add a WHERE filter to show "only the highest total price per unit that are greater than 1000". This is a simple multiplication and then you can filter by comparing if it is > 1000.
Then you need to limit the query to return only the row with the "highest total price per unit of all the subjects". From Oracle 12, this would be done with an ORDER BY clause in descending order of total price and then using FETCH FIRST ROW ONLY or FETCH FIRST ROW WITH TIES.
Not sure if i get it fully, but i think its this :
Notes:
Always use Id's to filter records:
where su.SubjId in (1,2)
You can find max record using max() at subquery and join it with main query like this :
where su2.SubjId = su.SubjId
You cannot use alias as filter so you can filter it like:
( su.Units * su.Priceperunit ) > 1000
CREATE VIEW StudentDetails AS
select s.StudName,
e.EnrollmentDate,
su.SubjDescription,
su.Units * su.Priceperunit TotalPrice
from student s
inner join Enrollment e
on e.StudentId = s.StudentId
inner join Subject su
on su.SubjId = e.SubjId
where su.SubjId in (1,2)
CREATE VIEW BiggestPrice AS
select su.SubjId, ( su.Units * su.Priceperunit ) TotalPrice
from Subject su
where ( su.Units * su.Priceperunit ) =
(
select max(su2.Units * su2.Priceperunit)
from Subject su2
where su2.SubjId = su.SubjId
)
and ( su.Units * su.Priceperunit ) > 1000

For each ‘CptS’ course, find the percentage of the students who failed the course. Assume a passing grade is 2 or above

CREATE TABLE Course (
courseno VARCHAR(7),
credits INTEGER NOT NULL,
enroll_limit INTEGER,
classroom VARCHAR(10),
PRIMARY KEY(courseNo), );
CREATE TABLE Student (
sID CHAR(8),
sName VARCHAR(30),
major VARCHAR(10),
trackcode VARCHAR(10),
PRIMARY KEY(sID),
FOREIGN KEY (major,trackcode) REFERENCES Tracks(major,trackcode) );
CREATE TABLE Enroll (
courseno VARCHAR(7),
sID CHAR(8),
grade FLOAT NOT NULL,
PRIMARY KEY (courseNo, sID),
FOREIGN KEY (courseNo) REFERENCES Course(courseNo),
FOREIGN KEY (sID) REFERENCES Student(sID) );
So far I've been able to create two seperate queries, one that counts the number of people who failes. And the other counts the number of people who passed. I'm having trouble combining these to produce the number of people passed / number of people failed. For each course.
SELECT course.courseno, COUNT(*) FROM course inner join enroll on enroll.courseno = course.courseno
WHERE course.courseno LIKE 'CptS%' and enroll.grade < 2
GROUP BY course.courseno;
SELECT course.courseno, COUNT(*) FROM course inner join enroll on enroll.courseno = course.courseno
WHERE course.courseno LIKE 'CptS%' and enroll.grade > 2
GROUP BY course.courseno;
The end result should look something like
courseno passrate
CptS451 100
CptS323 100
CptS423 66
You can do a conditional average for this:
select
courseno,
avg(case when grade > 2 then 100.0 else 0 end) passrate
from enroll
where courseno like 'CptS%'

Problem in querying specific data based on values present in different rows of the same column

I am creating a database of a college and the tables are given as:
create table depts(
deptcode char(3) primary key,
deptname char(70) not null);
create table students(
rollno number(2) primary key,
name char(50),
bdate date check(bdate < TO_DATE('2004-01-01','YYYY-MM-DD')),
deptcode char(3) references depts(deptcode)
on delete cascade,
hostel number check(hostel<20),
parent_inc number(8,1));
create table faculty(
fac_code char(2) primary key,
fac_name char(50) not null,
fac_dept char(3) references depts(deptcode)
on delete cascade);
//for courses offered by the college
create table crs_offrd(
crs_code char(5) primary key,
crs_name char(35) not null,
crs_credits number(2,1),
crs_fac_cd char(2) references faculty(fac_code)
on delete cascade);
// for course registered by students*
create table crs_regd(
crs_rollno number(2) references students(rollno),
crs_cd char(5) references crs_offrd(crs_code)
on delete cascade,
marks number(5,2),
primary key(crs_rollno,crs_cd));
I am trying to find out name , subject and marks of students who have marks more than rollno 92005102 for course CS103 and CS106.
I believe the table should look like this:
Name Subject Marks
XYZ CS103 92
XYZ CS106 95
I am confused how to check for marks in both CS103 and CS106 at the same time as the marks for two subjects are present in the same column in a different row, and query processes one row at a time.
I followed the question posted here but it works only for a column and it would list values separated by , but I need to get the corresponding subject name in which that marks was obtained.
If any other information required, please comment.
There are probably many ways to achieve this. One is this:
select
crs_rollno,
max(case when crs_cd = 'CS103' then marks end) as marks103,
max(case when crs_cd = 'CS106' then marks end) as marks106
from crs_regd cr
where crs_cd in ('CS103', 'CS106')
and marks >
(
select marks
from crs_regd cr92005102
where cr92005102.crs_rollno = 92005102 -- student 92005102
and cr92005102.crs_cd = cr.crs_cd -- same class
)
group by crs_rollno
having count(*) = 2 /* both courses better than 92005102 */ ;
You can join to the students table to get their name.
Just for the fun of it another approach:
with cs103 as (select * from crs_regd where crs_cd = 'CS103')
, cs106 as (select * from crs_regd where crs_cd = 'CS106')
select crs_rollno, cs103.marks as cs103_marks, cs106.marks as cs106_marks
from cs103 join cs106 using (crs_rollno)
where cs103.marks > (select marks from cs103 where crs_rollno = 92005102)
and cs106.marks > (select marks from cs106 where crs_rollno = 92005102);
You can use group by with having as following:
-- CTE IS USED TO FETCH THE REQUIRED DATA FROM TABLE USING JOINS
WITH CTE AS (
SELECT
S.NAME,
CO.CRS_NAME,
CO.CRS_CODE,
CR.MARKS,
S.ROLLNO
FROM
STUDENTS S
JOIN CRS_REGD CR ON ( S.ROLLNO = CR.CRS_ROLLNO )
JOIN CRS_OFFRD CO ON ( CR.CRS_CD = CO.CRS_CODE )
WHERE
CO.CRS_NAME IN (
'CS103',
'CS106'
)
)
-- ACTUAL LOGIC START FROM HERE
SELECT
S.NAME,
CO.CRS_NAME,
CR.MARKS
FROM
CTE C3
WHERE
C3.ROLLNO IN (
SELECT
C2.ROLLNO
FROM
CTE C1
JOIN CTE C2 ON ( C1.ROLLNO = 92005102
AND C1.ROLLNO <> C2.ROLLNO
AND C1.CRS_CODE = C2.CRS_CODE )
GROUP BY
C2.ROLLNO
HAVING ( MAX(CASE
WHEN C1.CRS_NAME = 'CS103'
AND C2.MARKS >= C1.MARKS THEN 1
END) = 1
AND MAX(CASE
WHEN C1.CRS_NAME = 'CS106'
AND C2.MARKS >= C1.MARKS THEN 1
END) = 1 )
);
Cheers!!

SQLite, aggregation query as where clause

Given the schema:
CREATE TABLE Student (
studentID INT PRIMARY KEY NOT NULL,
studentName TEXT NOT NULL,
major TEXT,
class TEXT CHECK (class IN ("Freshman", "Sophomore", "Junior", "Senior")),
gpa FLOAT CHECK (gpa IS NULL OR (gpa >= 0 AND gpa <= 4)),
FOREIGN KEY (major) REFERENCES Dept(deptID) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE Dept (
deptID TEXT PRIMARY KEY NOT NULL CHECK (LENGTH(deptID) <= 4),
NAME TEXT NOT NULL UNIQUE,
building TEXT
);
CREATE TABLE Course (
courseNum INT NOT NULL,
deptID TEXT NOT NULL,
courseName TEXT NOT NULL,
location TEXT,
meetDay TEXT NOT NULL CHECK (meetDay IN ("MW", "TR", "F")),
meetTime INT NOT NULL CHECK (meetTime >= '07:00' AND meetTime <= '17:00'),
PRIMARY KEY (courseNum, deptID),
FOREIGN KEY (deptID) REFERENCES Dept(deptID) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE Enroll (
courseNum INT NOT NULL,
deptID TEXT NOT NULL,
studentID INT NOT NULL,
PRIMARY KEY (courseNum, deptID, studentID),
FOREIGN KEY (courseNum, deptID) REFERENCES Course ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (studentID) REFERENCES Student(studentID) ON UPDATE CASCADE ON DELETE CASCADE
);
I'm attempting to find the names, IDs, and the number of courses they are taking, for the students who are taking the highest number of courses. The sELECT to retrieve the names and IDs is simple enough, however I'm having trouble figuring out how to select the number of courses each student is taking, and then find the max of that and use it as a WHERE clause.
This is what I have so far:
SELECT Student.studentName, Student.studentID, COUNT(*) AS count
FROM Enroll
INNER JOIN Student ON Enroll.studentID=Student.studentID
GROUP BY Enroll.studentID
So first you get count of all the enrolled classes per student
SELECT COUNT() AS num
FROM Enroll
GROUP BY studentID
You can then check that against your existing query using HAVING to get your final query.
SELECT Student.studentName,Student.studentID,COUNT(*) AS count
FROM Enroll
INNER JOIN Student ON Enroll.studentID=Student.studentID
GROUP BY Enroll.studentID
HAVING COUNT()=(SELECT COUNT() AS num FROM Enroll GROUP BY studentID);
So to recap this basically gets the number which represents the highest number of enrollments for any student, then gets all students where that number is their count of enrollments, thus all students which have the highest, or equal highest number of enrollments.
We use HAVING because it is applied after the GROUP BY, meaning you can't use aggregate functions such as COUNT() in a WHERE clause.

SQL normalization

right now, i have a table:
Id - CollegeName - CourseName
this table is not normalized so i have many Courses for every 1 College
I need to normalize this into two tables:
Colleges: CollegeID - CollegeName
Courses: CourseID - CollegeID - CourseName
Is there an easy way to do this?
Thank you
CREATE TABLE dbo.College
(
CollegeId int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
CollegeName nvarchar(100) NOT NULL
)
CREATE TABLE dbo.Course
(
CourseId int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
CollegeId int NOT NULL,
CourseName nvarchar(100) NOT NULL
)
ALTER TABLE dbo.Course
ADD CONSTRAINT FK_Course_College FOREIGN KEY (CollegeId)
REFERENCES dbo.College (CollegeId)
--- add colleges
INSERT INTO dbo.College (CollegeName)
SELECT DISTINCT CollegeName FROM SourceTable
--- add courses
INSERT INTO dbo.Course (CollegeId, CourseName)
SELECT
College.CollegeId,
SourceTable.CourseName
FROM
SourceTable
INNER JOIN
dbo.College ON SourceTable.CollegeName = College.CollegeName
If you create the 2 new tables with Colleges.CollegeID and Courses.CourseID as auto numbered fields, you can go with :
INSERT INTO Colleges (CollegeName)
SELECT DISTINCT CollegeName
FROM OLdTable ;
INSERT INTO Courses (CollegeID, CourseName)
SELECT Colleges.CollegeID, OldTable.CourseName
FROM OldTable
JOIN Colleges
ON OldTable.CollegeName = Colleges.CollegeName ;
I agreed with #Andomar's first comment: remove the seemingly redundant Id column and your CollegeName, CourseName table is already in 5NF.
What I suspect you need is a further table to give courses attributes so that you can model the fact that, say, Durham University's B.Sc. in Computing Science is comparable with Harvard's A.B. in Computer Science (via attributes 'computing major', 'undergraduate', 'country=US, 'country=UK', etc).
Sure.
Create a College table with a college_id (primary key) column, and a college_name column which is used as a unique index column.
Just refer to the college_id column, not college_name, in the Course table.