Is this SQL (oracle) query correct? - sql

Diagram of Tables:
SELECT DISTINCT s.location, tb.num_students_enrolled, tb2.num_sections
FROM section s
JOIN
(SELECT location, COUNT(DISTINCT student_id) num_students_enrolled
FROM section sec JOIN enrollment e ON(sec.section_id = e.section_id)
GROUP BY location) tb
ON(s.location = tb.location)
JOIN
(SELECT location, COUNT(section_id) num_sections
FROM section sect
GROUP BY location) tb2
ON(s.location = tb2.location)
ORDER BY s.location
Question:
Create a query that lists location, number of sections taught in that location and number of students enrolled in courses at that location. Sort by location.
Enrollment table - many to many relationship between section_id and student_id. A student can be enrolled in multiple sections, and there can be multiple students enrolled in a section.
Section Table
Section table - section_id is unique primary key. There are a total of 12 locations.
Query Results
location nm_st_en nm_sects
H310 1 1
L206 8 1
L210 29 10
L211 10 3
L214 33 15
L500 14 2
L507 33 15
L509 64 25
L511 3 1
M200 1 1
M311 11 3
M500 5 1

Whether the query is correct requires data, and is up to you to figure out. Write some queries to verify your results on a subset of the data.
But I think you might be over-complicating this.
Something like:
select sec.location,
count(distinct sec.section_id) as number_of_sections,
count(distinct enr.student_id) as number_of_students
from Section sec join Enrollment enr on sec.section_id = enr.section_id
group by sec.location
order by sec.location;
As a general pointer, you should avoid joining query results, as it makes things a bit difficult to understand. Also, parenthesis around join clauses are unnecessary clutter.

Related

GROUP BY one column, then GROUP BY another column

I have a database table t with a sales table:
ID
TYPE
AGE
1
B
20
1
BP
20
1
BP
20
1
P
20
2
B
30
2
BP
30
2
BP
30
3
P
40
If a person buys a bundle it appears the bundle sale (TYPE B) and the different bundle products (TYPE BP), all with the same ID. So a bundle with 2 products appears 3 times (1x TYPE B and 2x TYPE BP) and has the same ID.
A person can also buy any other product in that single sale (TYPE P), which has also the same ID.
I need to calculate the average/min/max age of the customers but the multiple entries per sale tamper with the correct calculation.
The real average age is
(20 + 30 + 40) / 3 = 30
and not
(20+20+20+20 + 30+30+30 + 40) / 8 = 26,25
But I don't know how I can reduce the sales to a single row entry AND get the 4 needed values?
Do I need to GROUP BY twice (first by ID, then by AGE?) and if yes, how can I do it?
My code so far:
SELECT
AVERAGE(AGE)
, MIN(AGE)
, MAX(AGE)
, MEDIAN(AGE)
FROM t
but that does count every row.
Assuming the age is the same for all rows with the same ID (which in itself indicates a normalisation problem), you can use nest aggregation:
select avg(min(age)) from sales
group by id
AVG(MIN(AGE))
-------------
30
SQL Fiddle
The example in the documentation is very similar; and is explained as:
This calculation evaluates the inner aggregate (MAX(salary)) for each group defined by the GROUP BY clause (department_id), and aggregates the results again.
So for your version:
This calculation evaluates the inner aggregate (MIN(age)) for each group defined by the GROUP BY clause (id), and aggregates the results again.
It doesn't really matter whether the inner aggregate is min or max - again, assuming they are all the same - it's just to get a single value per ID, which can then be averaged.
You can do the same for the other values in your original query:
select
avg(min(age)) as avg_age,
min(min(age)) as min_age,
max(min(age)) as max_age,
median(min(age)) as med_age
from sales
group by id;
AVG_AGE MIN_AGE MAX_AGE MED_AGE
------- ------- ------- -------
30 20 40 30
Or if you prefer you could get the one-age-per-ID values once ina CTE or subquery and apply the second layer of aggregation to that:
select
avg(age) as avg_age,
min(age) as min_age,
max(age) as max_age,
median(age) as med_age
from (
select min(age) as age
from sales
group by id
);
which gets the same result.
SQL Fiddle

Where statement for exact match on Many to Many SQL tables

I am trying to construct a SQL statement to search in two tables that are in a many to many relation.
Problem : SQL statement to search for products with exact stones.
For example, in the below tables, I need a statement that will search for product with Ruby and Emerald stone ONLY. In all my attempts I get both Ring and Necklace because they both have Ruby and Emerald even though Necklace has one additional stone. It should only give Ring product.
I need a way to implement the AND operator on the stone table so that the result contains products that have the exact stones. Please help.
Table stone
s_id
s_name
1
Ruby
2
Emerald
3
Onyx
Table product
p_id
p_name
1
Ring
2
Necklace
3
Pendent
Relation table - product_stone
p_s_id
p_id
s_id
1
1
1
1
1
2
1
2
1
1
2
2
1
2
3
1
3
3
This is a relational division question. We need to find the cross join of the two tables "divided" by our list, with no remainder i.e. no other stone in product.
We will assume that p_id and s_id are unique:
;WITH StonesToFind AS ( -- we could also use a table variable etc here
SELECT *
FROM stone
WHERE s_name IN ('Ruby','Emerald')
)
SELECT p.p_name
FROM product AS p -- let's get all products...
JOIN product_stone AS ps ON ps.p_id = p.p_id -- ...cross join all their stones
LEFT JOIN StonesToFind AS s ON s.s_id = ps.s_id -- they may have stones in the list
GROUP BY p.p_id, p_name
HAVING COUNT(CASE WHEN s.s_id IS NULL THEN 1 END) = 0
-- the number of non matching stones in product must be zero
AND COUNT(*) = (SELECT COUNT(*) FROM StonesToFind);
-- the total number of stones must be the same as the list

Finding all instances where a foreign key appears multiple times grouped by month

I am not too familiar with SQL, and I have been tasked with something that I quite frankly have no clue how to go about it.
I am just going to simplify the tables to the point where only the necessary fields are taken into consideration.
The tables look as follows.
Submission(course(string), student(foreign_key), date-submitted)
Student(id)
What I need to do is produce a table of active students per month, per course with a total. An active student being anyone who has more than 4 submissions in the month. I am only looking at specific courses, so I will need to hard code the values that I need, for the sake of the example "CourseA" and "CourseB"
The result should be as follows
month | courseA | CourseB | Total
------------------------------------------
03/2020 50 27 77
02/2020 25 12 37
01/2020 43 20 63
Any help would be greatly apreciated
You can do this with two levels of aggregation: first by month, course and student (while filtering on students having more than 4 submissions), then by month (while pivoting the dataset):
select
month_submitted,
count(*) filter(where course = 'courseA') active_students_in_courseA,
count(*) filter(where course = 'courseB') active_students_in_courseB,
count(*) total
from (
select
date_trunc('month', date_submitted) month_submitted,
course,
student_id,
count(*) no_submissions
from submission
where course in ('courseA', 'courseB')
group by 1, 2, 3
having count(*) > 4
) t
group by 1
You could do subqueries using the WITH keyword like this:
WITH monthsA AS (
SELECT to_char(date-submitted, "MM/YYYY") as month, course, COUNT(*) as students
FROM Submission
WHERE course = 'courseA'
GROUP BY 1, 2
), monthsB AS (
SELECT to_char(date-submitted, "MM/YYYY") as month, course, COUNT(*) AS students
FROM Submission
WHERE course = 'courseB'
GROUP BY 1, 2
)
SELECT ma.month,
COALESE(ma.students, 0) AS courseA,
COALESCE(mb.students) AS courseB,
COALESCE(ma.students, 0) + COALESCE(mb.students, 0) AS Total
FROM monthsA ma
LEFT JOIN monthsB mb ON ma.month = mb.month
ORDER BY 1 DESC

Join results of two queries in SQL and produce a result given some condition

I've never used SQL until now, so please bear with me. I have a table of departments:
I have written two queries as follows:
-- nbr of staff associated with each dept.
SELECT count(departmentId) as freq
FROM staff
GROUP BY departmentId
-- nbr of students associated with each dept.
SELECT count(departmentId) as freq
FROM StudentAssignment
GROUP BY departmentId
These produce the following two tables:
For each department id 1 to 5, I need to divide the studentFreq by the staffFreq and show the department id and description if the result is greater than 2.
If the staffFreq i.e. number of staff, for a department id is zero then I need to show that department id and description too.
So for example, in this case I want to produce a table with the department ids of 1, 4 and 5 and their corresponding descriptions: Computing, Classics and Mechanical Engineering.
Computing because 7 / 2 > 2. Classics and ME because 0 staff are assigned to those depts.
One method is a left join, starting with the departments table:
SELECT d.*,
s.freq as as num_staff, sa.freq as num_students,
sa.freq * 1.0 / s.freq as student_staff_ratio
FROM deptartments d LEFT JOIN
(SELECT departmentId, count(*) as freq
FROM staff
GROUP BY departmentId
) s
ON s.departmentId = d.department_id LEFT JOIN
(SELECT departmentId, count(*) as freq
FROM StudentAssignment
GROUP BY departmentId
) sa
ON sa.departmentId = d.departmentId;
Notes:
This should missing values as NULL rather than 0. You can assign 0 instead using COALESCE(): COALESCE(s.freq, 0) as num_staff.
SQL Server does integer division, so 7 / 2 = 3, not 3.5. I think you would typically want the fractional component.

how to avoid duplicate on Joining two tables

Student Table
SID Name
1 A
2 B
3 C
Marks Table
id mark subject
1 50 physics
2 40 biology
1 50 chemistry
3 30 mathematics
SELECT distinct(std.id),std.name,m.mark, row_number() over() as rownum FROM
student std JOIN marks m ON std.id=m.id AND m.mark=50
This result is 2 times A even after using disticnt . My expected result will have only one A. if i remove row_number() over() as rownum its working fine. Why this is happening ? how to resolve. AM using DB2!!
There are two rows in marks Table with id = 1 and mark = 50.. So you will get two rows in the output for each row in student table...
If you only want one, you have to do a group By
SELECT std.id, std.name, m.mark, row_number()
over() as rownum
FROM student std
JOIN marks m
ON m.id=std.id AND m.mark=50
Group By std.id, std.name, m.mark
Now that you've clarified your question as:
I want to find all students with a mark of 50 in at least one subject. I would use the query:
SELECT student.id, '50'
FROM student
WHERE EXISTS (SELECT 1 FROM marks WHERE marks.id = student.id AND marks.mark = 50)
This also gives you flexibility to change the criteria, e.g. at least one mark of 50 or less.
Similar to Charles answer, but you always want to put the predicate (mark=50) in the WHERE clause, so you're filtering before joining. If this is just homework it might not matter but you'll want to remember this if you ever hit any real data.
SELECT std.sid,
std.name,
m.mark,
row_number() over() AS rownum
FROM student std
JOIN marks m
ON std.sid=m.id
WHERE m.mark=50
GROUP BY std.sid, std.name, m.mark