I am very much a beginner at this, so please pardon my ignorance. I have searched for hours, but don't yet have the experience to have a better idea of precisely what to search for!
I have a data set in SSRS as follows:
|---------------------|------------------|
| Subject | Student |
|---------------------|------------------|
| Biology | Student A |
|---------------------|------------------|
| Chemistry | Student A |
|---------------------|------------------|
| Chemistry | Student B |
|---------------------|------------------|
| Physics | Student B |
|---------------------|------------------|
I want to write a matrix which counts all the students who are doing a combination of subjects (eg. Chemistry and Physics). Currently, my matrix is only counting the number of students in an individual subject, leading to something like this:
|---------------------|------------------|------------------|------------------|
| | Biology | Chemistry | Physics |
|---------------------|------------------|------------------|------------------|
| Biology | 20 | 0 | 0 |
|---------------------|------------------|------------------|------------------|
| Chemistry | 0 | 36 | 0 |
|---------------------|------------------|------------------|------------------|
| Physics | 0 | 0 | 16 |
|---------------------|------------------|------------------|------------------|
What am I missing?
First, your dataset needs to combine subjects as Subject1 and Subject2, for example by joining the table to itself on the Student field.
Then, in the matrix, use a row group that groups on Subject1 and a column group that groups on Subject2.
That's easy:
You inner join the map between student and subject with itselfs, but only if the student is the same, but the subject is different.
Then you do CountDistinct on the studentId in the report, doesn't matter if it's from map1 or map2, since it's always the same.
;WITH subjects AS
(
SELECT 1 AS subject_id, 'Biology' AS subject_name
UNION ALL SELECT 2 AS subject_id, 'Chemistry' AS subject_name
UNION ALL SELECT 3 AS subject_id, 'Physics' AS subject_name
)
,students AS
(
SELECT 1 AS student_id, 'Student A' AS student_name
UNION ALL SELECT 2 AS student_id, 'Student B' AS student_name
UNION ALL SELECT 3 AS student_id, 'Student C' AS student_name
UNION ALL SELECT 4 AS student_id, 'Student D' AS student_name
)
,map_student_subject AS
(
SELECT 1 AS student_id, 1 AS subject_id
UNION ALL SELECT 1 AS student_id, 2 AS subject_id
UNION ALL SELECT 2 AS student_id, 2 AS subject_id
UNION ALL SELECT 2 AS student_id, 3 AS subject_id
UNION ALL SELECT 3 AS student_id, 3 AS subject_id
UNION ALL SELECT 4 AS student_id, 3 AS subject_id
)
SELECT
map1.student_id AS map1_student_id
,map1_student.student_name AS map1_student_name
,map1.subject_id AS map1_subject_id
,map1_subject.subject_name AS map1_subject_name
,map2.student_id AS map2_student_id
,map2_student.student_name AS map2_student_name
,map2.subject_id AS map2_subject_id
,map2_subject.subject_name AS map2_subject_name
FROM map_student_subject AS map1
INNER JOIN map_student_subject AS map2
ON map2.student_id = map1.student_id
AND map2.subject_id <> map1.subject_id
LEFT JOIN students AS map1_student
ON map1_student.student_id = map1.student_id
LEFT JOIN students AS map2_student
ON map2_student.student_id = map2.student_id
LEFT JOIN subjects AS map1_subject
ON map1_subject.subject_id = map1.subject_id
LEFT JOIN subjects AS map2_subject
ON map2_subject.subject_id = map2.subject_id
Result:
And if you want to list the students that have just one subject as well, you'll need to make a UNION, because just removing the <> will make it impossible to calculate the actual number of students in just one subject:
;WITH subjects AS
(
SELECT 1 AS subject_id, 'Biology' AS subject_name
UNION ALL SELECT 2 AS subject_id, 'Chemistry' AS subject_name
UNION ALL SELECT 3 AS subject_id, 'Physics' AS subject_name
)
,students AS
(
SELECT 1 AS student_id, 'Student A' AS student_name
UNION ALL SELECT 2 AS student_id, 'Student B' AS student_name
UNION ALL SELECT 3 AS student_id, 'Student C' AS student_name
UNION ALL SELECT 4 AS student_id, 'Student D' AS student_name
)
,map_student_subject AS
(
SELECT 1 AS student_id, 1 AS subject_id
UNION ALL SELECT 1 AS student_id, 2 AS subject_id
UNION ALL SELECT 2 AS student_id, 2 AS subject_id
UNION ALL SELECT 2 AS student_id, 3 AS subject_id
UNION ALL SELECT 3 AS student_id, 3 AS subject_id
UNION ALL SELECT 4 AS student_id, 3 AS subject_id
)
SELECT
map1.student_id AS map1_student_id
,map1_student.student_name AS map1_student_name
,map1.subject_id AS map1_subject_id
,map1_subject.subject_name AS map1_subject_name
,map2.student_id AS map2_student_id
,map2_student.student_name AS map2_student_name
,map2.subject_id AS map2_subject_id
,map2_subject.subject_name AS map2_subject_name
FROM map_student_subject AS map1
INNER JOIN map_student_subject AS map2
ON map2.student_id = map1.student_id
AND map2.subject_id <> map1.subject_id
LEFT JOIN students AS map1_student
ON map1_student.student_id = map1.student_id
LEFT JOIN students AS map2_student
ON map2_student.student_id = map2.student_id
LEFT JOIN subjects AS map1_subject
ON map1_subject.subject_id = map1.subject_id
LEFT JOIN subjects AS map2_subject
ON map2_subject.subject_id = map2.subject_id
UNION ALL
SELECT
students.student_id AS map1_student_id
,students.student_name AS map1_student_name
,MAX(subjects.subject_id) AS map1_subject_id
,MAX(subjects.subject_name) AS map1_subject_name
,students.student_id AS map2_student_id
,students.student_name AS map2_student_name
,MAX(subjects.subject_id) AS map2_subject_id
,MAX(subjects.subject_name) AS map2_subject_name
FROM map_student_subject
LEFT JOIN students ON students.student_id = map_student_subject.student_id
LEFT JOIN subjects ON subjects.subject_id = map_student_subject.subject_id
GROUP BY
students.student_id
,students.student_name
HAVING COUNT(*) = 1
Related
I have fought my way through various answers and did some progress, however, the final solution has not been discovered.
The DB situation:
Table "clients_a":
userid | name
1 | Steve
2 | John
3 | Paul
Table "clients_b":
userid | name
1 | NULL
3 | Jokename
4 | Jessy
Desired result/output:
userid | name
1 | Steve
2 | John
3 | Paul
4 | Jessy
Description of what is going on:
userid is unique in the result (merged)
a result for name that is not NULL is favored
if two entries, then result from table clients_a is favored
all entries have a groupid (see below), that has to be taken into account
MySql queries I tried (and came close):
Attempt 1: This query works, but it does not regard the name. It takes all names from client_a:
SELECT * FROM
(
SELECT userid, name
FROM `client_a`
WHERE groupid = 123
UNION DISTINCT
SELECT userid, name
FROM `client_b`
WHERE groupid = 123
) AS res
GROUP BY res.userid
Attempt 2: This query creates duplicate entries (one userid can occur twice), but regards the name, as it seems:
SELECT o.*, i.* FROM
(
SELECT userid, name
FROM `client_a`
WHERE groupid = 123
UNION DISTINCT
SELECT userid, realname
FROM `client_b`
WHERE groupid = 123
GROUP BY userid
) AS o
LEFT JOIN `client_a` as i on i.userid = o.userid
I also tried to use MIN(name) without success.
Any help is appreciated.
You can do it with NOT EXISTS:
SELECT a.userid, a.name
FROM clients_a a
WHERE a.groupid = 123
AND (name IS NOT NULL OR NOT EXISTS (SELECT 1 FROM clients_b b WHERE b.userid = a.userid))
UNION
SELECT b.userid, b.name
FROM clients_b b
WHERE b.groupid = 123
AND NOT EXISTS (SELECT 1 FROM clients_a a WHERE a.userid = b.userid AND a.name IS NOT NULL)
See the demo.
Results:
userid
name
1
Steve
2
John
3
Paul
4
Jessy
My solution passed the first test case, but got the wrong answer after final submission. I am thankful for anyone willing to point out my mistakes. Thanks!
The question is as below:
Table: Student
+---------------------+---------+
| Column Name | Type |
+---------------------+---------+
| student_id | int |
| student_name | varchar |
+---------------------+---------+
student_id is the primary key for this table.
student_name is the name of the student.
Table: Exam
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| exam_id | int |
| student_id | int |
| score | int |
+---------------+---------+
(exam_id, student_id) is the primary key for this table.
Student with student_id got score points in exam with id exam_id.
A "quite" student is the one who took at least one exam and didn't score neither the high score nor the low score.
Write an SQL query to report the students (student_id, student_name) being "quiet" in ALL exams.
Don't return the student who has never taken any exam. Return the result table ordered by student_id.
The query result format is in the following example.
Student table:
+-------------+---------------+
| student_id | student_name |
+-------------+---------------+
| 1 | Daniel |
| 2 | Jade |
| 3 | Stella |
| 4 | Jonathan |
| 5 | Will |
+-------------+---------------+
Exam table:
+------------+--------------+-----------+
| exam_id | student_id | score |
+------------+--------------+-----------+
| 10 | 1 | 70 |
| 10 | 2 | 80 |
| 10 | 3 | 90 |
| 20 | 1 | 80 |
| 30 | 1 | 70 |
| 30 | 3 | 80 |
| 30 | 4 | 90 |
| 40 | 1 | 60 |
| 40 | 2 | 70 |
| 40 | 4 | 80 |
+------------+--------------+-----------+
Result table:
+-------------+---------------+
| student_id | student_name |
+-------------+---------------+
| 2 | Jade |
+-------------+---------------+
Explanation in the question:
For exam 1: Student 1 and 3 hold the lowest and high score respectively.
For exam 2: Student 1 hold both highest and lowest score.
For exam 3 and 4: Studnet 1 and 4 hold the lowest and high score respectively.
Student 2 and 5 have never got the highest or lowest in any of the exam.
Since student 5 is not taking any exam, he is excluded from the result.
So, we only return the information of Student 2.
My answer was to create two tables, the one is to list eligible student, with at least one exam.
The other is to find the max(score) and the min(score) of the exam table.
And use <> to locate the quiet students' id and then join with Student table to find
this student_id's name, as below:
-- get eligible student
with eligible_student as
(
select distinct student_id as eligible_id from Exam
group by 1
order by 1
),
-- get the high and low score
high_low as
(select student_id, max(score) as high_score, min(score) as low_score
from Exam),
result as
(select eligible_student.eligible_id as student_id
from eligible_student inner join
high_low
on eligible_student.eligible_id <> high_low.student_id
-- left join Student
-- on eligible_student.eligible_id = Student.student_id
group by student_id
order by student_id
)
select result.student_id, s.student_name as student_name
from result left join Student s
on result.student_id = s.student_id
order by student_id;
I would use window functions and aggregation:
select s.*
from student s
inner join (
select e.*,
rank() over(partition by exam_id order by score) as rn_asc,
rank() over(partition by exam_id order by score desc) as rn_desc
from exam e
) e on e.student_id = s.student_id
group by s.student_id
having min(rn_asc) > 1 and min(rn_desc) > 1
The subquery ranks records having he same exam by ascending and descending score, allowing ties. We can then join that with the student table (which eliminates students that have no exam at all), group by student, and filter on those whose both ranks never reached 1.
This query:
SELECT *,
(MIN(score) OVER (PARTITION BY exam_id) = score) +
(MAX(score) OVER (PARTITION BY exam_id) = score) flag
FROM exam
returns a flag column which has the value 0 when the student's score is neither the min nor the max of of the exam.
You can aggregate on the results of the above query to get all the students that have not a single flag with value different than 0:
WITH cte AS (
SELECT *,
(MIN(score) OVER (PARTITION BY exam_id) = score) +
(MAX(score) OVER (PARTITION BY exam_id) = score) flag
FROM exam
)
SELECT s.student_id, s.student_name
FROM student s INNER JOIN cte c
ON c.student_id = s.student_id
GROUP BY s.student_id, s.student_name
HAVING SUM(c.flag) = 0
Or:
WITH cte AS (
SELECT student_id
FROM (
SELECT *,
(MIN(score) OVER (PARTITION BY exam_id) = score) +
(MAX(score) OVER (PARTITION BY exam_id) = score) flag
FROM exam
) t
GROUP BY student_id
HAVING SUM(flag) = 0
)
SELECT *
FROM student
WHERE student_id IN (SELECT student_id FROM cte)
See the demo.
Results:
> student_id | student_name
> ---------: | :-----------
> 2 | Jade
This part is wrong:
with high_low as
(select student_id, max(score) as high_score, min(score) as low_score
from Exam)
because it outputs:
+------------+------------+-----------+
| student_id | high_score | low_score |
+------------+------------+-----------+
| 1 | 90 | 60 |
+------------+------------+-----------+
and student_id=1 has no relation to the found high_score or low_score.
After this the found (but incorrect) student_id is used in the selection for the cte result.
A solution:
with high_low as
(select max(score) as high_score, min(score) as low_score
from Exam)
select student.student_id, student_name
from (
select exam.student_id
from exam
group by exam.student_id
having max(score) <> (select high_score from high_low)
and min(score) <> (select low_score from high_low)) x
inner join student on student.student_id=x.student_id;
or:
select
student.*
from exam
inner join student on student.student_id=exam.student_id
group by student_id
having max(score) not in (select max(score) from exam)
and min(score) not in (select min(score) from exam);
i have 3 tables as the following:
STUDENT
---------------
id name
---------------
1 Joe
2 mark
3 mike
---------------
SUBJECT
-----------------------------------
id student_id name
-----------------------------------
1 1 math
2 1 english
3 1 french
4 1 history
5 2 math
6 2 english
7 2 french
8 3 math
9 3 english
-----------------------------------
EXAM
-----------------------------------
ID Student_id mark
-----------------------------------
1 1 10
2 1 10
3 1 10
4 1 10
5 2 5
6 2 5
7 2 5
8 3 2
9 3 2
-----------------------------------
SELECT
student.id, student.name AS 'NAME',
COUNT(subject.id) AS 'SUBJECTS',
SUM(exam.mark) AS 'MARKS'
FROM
'student'
INNER JOIN subject
ON subject.student_id = student.id
LEFT JOIN exam
ON exam.student_id = student.id
GROUP BY
student.id
i want to join between STUDENT and SUBJECT, STUDENT and EXAM
i problem is happening when i join the third table, it
duplicates the results.
i succeeded to join the first two tables, but when i join > the third one with them, it duplicates the counts.
the result i need is:
-----------------------------------
ID NAME SUBJECTS MARKS
-----------------------------------
1 joe 4 40
2 mark 3 15
3 mike 2 4
One simple approach here is to do separate aggregations on the subject and exam tables, and then join them:
SELECT
s.id,
s.name AS NAME,
COALESCE(su.cnt, 0) AS SUBJECTs,
COALESCE(e.marks, 0) AS MARKS
FROM student s
LEFT JOIN
(
SELECT student_id, COUNT(*) AS cnt
FROM subject
GROUP BY student_id
) su
ON s.id = su.student_id
LEFT JOIN
(
SELECT student_id, SUM(mark) AS marks
FROM exam
GROUP BY student_id
) e
ON s.id = e.student_id;
Demo
Note that I use left joins above, because perhaps a given student might not have any entry at all in either the subject or exam tables. In this case, we would assign his count/sum to zero by default.
You can get the aggregated columns by using a direct query for each student id:
select
s.id, s.name,
(select count(*) from subject where student_id = s.id) subjects,
(select sum(mark) from exam where student_id = s.id) marks
from student s
See the demo.
My task is to find all those subjects, by their id, that have (at least one, but) the fewest lowest passing grades in the database (the grade being the grade 6). I've managed to write the solution with three queries, however my task is to write it as a single query in MySQL. Thank you in advance.
-- 1. single query "solution"
SELECT subject_id FROM (SELECT subject_id, COUNT(*) AS six_count
FROM exams WHERE grade = 6
GROUP BY subject_id) AS sixes
WHERE subject_id = (SELECT MIN(six_count) FROM sixes);
-- 2. multiple queries solution
CREATE TABLE sixes AS (SELECT subject_id, COUNT(*) AS six_count
FROM exams WHERE grade = 6
GROUP BY subject_id);
SELECT subject_id FROM sixes
WHERE subject_id = (SELECT MIN(six_count) FROM sixes);
DROP TABLE sixes;
EDIT:
Exams table example:
| subject_id | student_id | exam_year | exam_mark | grade | exam_date |
| 1 | 20100022| 2011 | 'apr' | 10 | 2011-04-11 |
| 2 | 20100055| 2011 | 'oct' | 6 | 2011-10-04 |
| 3 | 20110030| 2011 | 'jan1' | 7 | 2011-01-26 |
| 5 | 20110055| 2011 | 'jan2' | 6 | 2011-02-13 |
| 5 | 20110001| 2011 | 'jun1' | 8 | 2011-06-23 |
This should do the trick. The sub query selects the first lowest number of sixes. The main query selects all subjects with that number. The trick is in ORDER BY count(*) LIMIT 1, which makes the sub query return the record with the lowest count.
SELECT
subject_id,
count(*) as six_count
FROM exams
WHERE grade = 6
GROUP BY subject_id
HAVING count(*) =
( SELECT count(*)
FROM exams
WHERE grade = 6
GROUP BY subject_id
ORDER BY count(*)
LIMIT 1
)
This pattern should do the trick. Generalized names.
SELECT subjectID
FROM TEST_DATA
WHERE grade = 6
GROUP
BY SubjectID
HAVING COUNT(1) =
( SELECT count(1) AS minCount
FROM TEST_DATA
WHERE grade = 6
GROUP
BY subjectID
ORDER
BY minCount
LIMIT 1
);
For example - Table name = TABLE
StudentID | SubCode | SubName
-----------------------------
1 M1 Math
1 S1 Science
1 E1 English
2 H1 History
3 G2 Geography
4 M1 Math
Can we display the answer like this ?
studentid | SubCode | SubName
---------------------------------
1 M1 Math
1 S1 Science
1 E1 English
An IN() subquery with a HAVING clause should do it:
SELECT *
FROM table
WHERE StudentID IN (
SELECT
StudentID
FROM table
GROUP BY userid
HAVING COUNT(*) > 1
)
SELECT DISTINCT T1.*
FROM YourTable AS T1
INNER JOIN YourTable AS T2
ON T1.StudentID = T2.StudentID
AND T1.SubCode <> T2.SubCode;