SQL select average grade for students - mysql

I'm learning sql and I'm having a bit of trouble with this,
I need the average grade of every student on each subject.
I made the following tables.
students
|id_student|name|
subjects
|id_subject|name|
grades
|id_grade|value|
And im linking them using these tables:
students_subjects
|id_student|id_subject|
subjects_grades
|id_subject|id_grade|
students_grades
|id_student|id_grade|
Any help appreciated
I'm trying
SELECT students.name, subjects.name, grades.value
FROM students
INNER JOIN students_subjects
ON students.id_student = students_subjects.id_student
INNER JOIN subjects
ON subjects.id_subject = students_subjects.id_subject
INNER JOIN students_grades
ON students_grades.id_student = students.id_student
INNER JOIN grades
ON students_grades.id_grade = grades.id_grade
INNER JOIN subjects_grades
ON grades.id_grade = subjects_grades.id_grade
And i get the following table
| name | name | value |
|----------|---------|-------|
| Nico | class1 | 70 |
| Nico | class1 | 40 |
| Nico | class2 | 70 |
| Nico | class2 | 40 |
| Fadia | class1 | 60 |
| Fadia | class1 | 55 |
| Cristian | class2 | 50 |
| Cristian | class2 | 40 |
But if i do AVG(grades.value) I only get the first row

Just add a group by:
SELECT students.name, subjects.name, AVG(grades.value)
FROM students
INNER JOIN students_subjects
ON students.id_student = students_subjects.id_student
INNER JOIN subjects
ON subjects.id_subject = students_subjects.id_subject
INNER JOIN students_grades
ON students_grades.id_student = students.id_student
INNER JOIN grades
ON students_grades.id_grade = grades.id_grade
INNER JOIN subjects_grades
ON grades.id_grade = subjects_grades.id_grade
GROUP BY students.name, subjects.name

Related

only students who have the best mark

I have a simple relation between student and marks :
Student :
+------------+-------------+
| idStudent | NameStudent |
+------------+-------------+
| 1 | Student A |
| 2 | Student B |
| 3 | Student C |
+------------+-------------+
Marks :
+----------+------+-----------+
| idMarks | mark | idStudent |
+----------+------+-----------+
| 1 | A | 1 |
| 2 | A | 1 |
| 3 | A | 1 |
| 4 | A | 2 |
| 5 | A | 2 |
| 6 | C | 3 |
| 7 | A | 3 |
+----------+------+-----------+
I only want to have students who have a "A" in every exam they passed
SELECT *, COUNT(mark)
FROM student S
INNER JOIN marks M ON S.idStudent = M.idStudent
WHERE M.mark = "A"
GROUP BY S.idStudent
I tried this but I reach deadlock when I try to compare the number of exam they passed and the number of A they have...
I also tried with subqueries but it didn't work
You could use HAVING:
SELECT S.idStudent
FROM student S
INNER JOIN marks M ON S.idStudent = M.idStudent
GROUP BY S.idStudent
HAVING COUNT(mark)=SUM(mark='A');-- compare all marks with marks that are only 'A'
-- if equal then every mark is 'A'
Group by on the student
Find all distinct marks obtained by the student and put them in a comma separated string using Group_concat function
Just filter out (using Having clause) the ones having only one overall distinct mark, that is A
Use the following query:
SELECT S.idStudent,
S.NameStudent,
GROUP_CONCAT(DISTINCT M.mark) AS unique_marks
FROM student S
INNER JOIN marks M ON S.idStudent = M.idStudent
GROUP BY S.idStudent, S.NameStudent
HAVING unique_marks = 'A'
If you just need the student IDs, you can LEFT JOIN students to marks, including a condition in the join clause for the mark not being A. Then, the WHERE clause would only include students with non-matching rows:
SELECT Student.idStudent
FROM Student
LEFT JOIN Marks BelowA ON
BelowA.idStudent = Student.idStudent
AND BelowA.mark <> 'A'
WHERE BelowA.idStudent IS NULL

MYSQL Left Join on multiple table but select only most recent rows

I have three tables (form_completions, customer_sessions and conversion_sessions) which are design like the below, these are heavily striped down for the purpose of this example:
Form_completions:
id | name | email
------------------------
101 | Tom | tom#website.com
102 | Ben | ben#website.com
Customer_sessions:
id | customer_id | session_id | session_source
---------------------------------
1 | 900 | 9kc73bsf | twitter
2 | 901 | 15jvuw83 | google
3 | 901 | 45h73bgf | twitter
Conversion_sessions:
id | customer_id | session_id | form_completion_id
------------------------------------
1 | 900 | 9kc73bsf | 101
2 | 901 | 45h73bgf | 102
The query that i currently have is:
SELECT custsess.session_source,
c.id as form_conversion_id,
c.name,
FROM conversion_sessions convsess
LEFT JOIN form_completions fc
ON convsess.`form_completion_id` = fc.`id`
LEFT JOIN customer_sessions custsess
ON custsess.`customer_id` = convsess.`customer_id`
This will give me the following:
session_source | form_conversion_id | name
------------------------------------------
twitter | 101 | Tom
google | 102 | Ben
twitter | 102 | Ben
But what i need is to avoid the duplication of form_conversion_id and only include the most recent so it would be...
session_source | form_conversion_id | name
------------------------------------------
twitter | 101 | Tom
twitter | 102 | Ben
Hopefully this makes sense.
Solution to your problem:
SELECT custsess.session_source,
fc.id as form_conversion_id,
fc.name
FROM conversion_sessions AS convsess
INNER JOIN form_completions AS fc
ON convsess.form_completion_id = fc.id
INNER JOIN customer_sessions AS custsess
ON custsess.customer_id = convsess.customer_id
AND custsess.session_id = convsess.session_id
While joining you forgot to join customer_sessions and coversion_sessions ON session_id.
OUTPUT:
session_source form_conversion_id name
twitter 01 Tom
twitter 102 Ben
For demo follow the below link:
https://www.db-fiddle.com/f/pQG4VCWAnoGtRJa6MkvST6/0
You need a Subquery to get the most recent, change your JOIN to something like this
LEFT JOIN customer_sessions custsess
ON custsess.id =
(
SELECT MAX(id)
FROM Customer_sessions
WHERE c.id = custsess.customer_id
)
Looking to your data You should just use inner join
You should use inner join on a subquery for max session_id
SELECT custsess.session_source,
c.id as form_conversion_id,
c.name,
FROM conversion_sessions convsess
customer_sessions custsess ON custsess.`customer_id` = convsess.`customer_id`
INNER JOIN (
select customer_id, max(session_id) as max_sess
from Customer_sessions
group by customer_id
) t on t.customer_id = custsess.customer_id and t.max_sess = custsess.session_id
INNER JOIN form_completions fc ON convsess.`form_completion_id` = fc.`id`

Fastest way to select min row with join

In this example, I have a listing of users (main_data), a pass list (pass_list) and a corresponding priority to each pass code type (pass_code). The query I am constructing is looking for a list of users and the corresponding pass code type with the lowest priority. The query below works but it just seems like there may be a faster way to construct it I am missing. SQL Fiddle: http://sqlfiddle.com/#!2/2ec8d/2/0 or see below for table details.
SELECT md.first_name, md.last_name, pl.*
FROM main_data md
JOIN pass_list pl on pl.main_data_id = md.id
AND
pl.id =
(
SELECT pl2.id
FROM pass_list pl2
JOIN pass_code pc2 on pl2.pass_code_type = pc2.type
WHERE pl2.main_data_id = md.id
ORDER BY pc2.priority
LIMIT 1
)
Results:
+------------+-----------+----+--------------+----------------+
| first_name | last_name | id | main_data_id | pass_code_type |
+------------+-----------+----+--------------+----------------+
| Bob | Smith | 1 | 1 | S |
| Mary | Vance | 8 | 2 | M |
| Margret | Cough | 5 | 3 | H |
| Mark | Johnson | 9 | 4 | H |
| Tim | Allen | 13 | 5 | M |
+------------+-----------+----+--------------+----------------+
users (main_data)
+----+------------+-----------+
| id | first_name | last_name |
+----+------------+-----------+
| 1 | Bob | Smith |
| 2 | Mary | Vance |
| 3 | Margret | Cough |
| 4 | Mark | Johnson |
| 5 | Tim | Allen |
+----+------------+-----------+
pass list (pass_list)
+----+--------------+----------------+
| id | main_data_id | pass_code_type |
+----+--------------+----------------+
| 1 | 1 | S |
| 3 | 2 | E |
| 4 | 2 | H |
| 5 | 3 | H |
| 7 | 4 | E |
| 8 | 2 | M |
| 9 | 4 | H |
| 10 | 4 | H |
| 11 | 5 | S |
| 12 | 3 | S |
| 13 | 5 | M |
| 14 | 1 | E |
+----+--------------+----------------+
Table which specifies priority (pass_code)
+----+------+----------+
| id | type | priority |
+----+------+----------+
| 1 | M | 1 |
| 2 | H | 2 |
| 3 | S | 3 |
| 4 | E | 4 |
+----+------+----------+
Due to mysql's unique extension to its GROUP BY, it's simple:
SELECT * FROM
(SELECT md.first_name, md.last_name, pl.*
FROM main_data md
JOIN pass_list pl on pl.main_data_id = md.id
ORDER BY pc2.priority) x
GROUP BY md.id
This returns only the first row encountered for each unique value of md.id, so by using an inner query to order the rows before applying the group by you get only the rows you want.
A version that will get the details as required, and should also work across different flavours of SQL
SELECT md.first_name, md.last_name, MinId, pl.main_data_id, pl.pass_code_type
FROM main_data md
INNER JOIN pass_list pl
ON md.id = pl.main_data_id
INNER JOIN pass_code pc
ON pl.pass_code_type = pc.type
INNER JOIN
(
SELECT pl.main_data_id, pl.pass_code_type, Sub0.MinPriority, MIN(pl.id) AS MinId
FROM pass_list pl
INNER JOIN pass_code pc
ON pl.pass_code_type = pc.type
INNER JOIN
(
SELECT main_data_id, MIN(priority) AS MinPriority
FROM pass_list a
INNER JOIN pass_code b
ON a.pass_code_type = b.type
GROUP BY main_data_id
) Sub0
ON pl.main_data_id = Sub0.main_data_id
AND pc.priority = Sub0.MinPriority
GROUP BY pl.main_data_id, pl.pass_code_type, Sub0.MinPriority
) Sub1
ON pl.main_data_id = Sub1.main_data_id
AND pl.id = Sub1.MinId
AND pc.priority = Sub1.MinPriority
ORDER BY pl.main_data_id
This does not rely on the flexibility of MySQLs GROUP BY functionality.
I'm not familiar with the special behavior of MySQL's group by, but my solution for these types of problems is to simply express as where there doesn't exist a row with a lower priority. This is standard SQL so should work on any DB.
select distinct u.id, u.first_name, u.last_name, pl.pass_code_type, pc.id, pc.priority
from main_data u
inner join pass_list pl on pl.main_data_id = u.id
inner join pass_code pc on pc.type = pl.pass_code_type
where not exists (select 1
from pass_list pl2
inner join pass_code pc2 on pc2.type = pl2.pass_code_type
where pl2.main_data_id = u.id and pc2.priority < pc.priority);
How well this performs is going to depend on having the proper indexes (assuming that main_data and pass_list are somewhat large). In this case indexes on the primary (should be automatically created) and foreign keys should be sufficient. There may be other queries that are faster, I would start by comparing this to your query.
Also, I had to add distinct because you have duplicate rows in pass_list (id 9 & 10), but if you ensure that duplicates can't exist (unique index on main_data_id, pass_code_type) then you will save some time by removing the distinct which forces a final sort of the result set. This savings would be more noticeable the larger the result set is.

Mysql join on 3 tables output

I am learning joins and have the following tables.
Student
| ID | NAME |
-------------
| 1 | A |
| 2 | B |
| 3 | C |
| 4 | D |
Pass
| ID | MARKS |
--------------
| 2 | 80 |
| 3 | 75 |
Fail
| ID | MARKS |
--------------
| 1 | 25 |
| 4 | 20 |
The output I want is this:
| NAME | MARKS |
----------------
| B | 80 |
| C | 75 |
| A | 25 |
| D | 20 |
I wrote a query like this:
select s.id,s.name,p.marks from student s
left join pass p on s.id=p.id
left join (select f.marks,f.id from fail f ) as nn on s.id=nn.id
order by marks desc;
The output I got is this:
| id | name | Marks|
--------------------
| 1 | B | 80 |
| 2 | C | 75 |
| 3 | A | Null |
| 4 | D | NUll |
Cant figure out why Null is coming. Any pointers?
You can use CASE statement for that:
SELECT Name,
CASE WHEN P.Marks IS NULL THEN f.Marks ELSE P.Marks END AS Marks
FROM Student s
LEFT JOIN Pass p ON s.ID = p.ID
LEFT JOIN Fail f ON s.ID = f.ID
ORDER BY Marks DESC;
Or you can also use IF statement:
SELECT Name,
IF(P.Marks IS NULL, F.Marks, P.Marks) AS Marks
FROM Student s
LEFT JOIN Pass p ON s.ID = p.ID
LEFT JOIN Fail f ON s.ID = f.ID
ORDER BY Marks DESC;
Output
| NAME | MARKS |
----------------
| B | 80 |
| C | 75 |
| A | 25 |
| D | 20 |
See this SQLFiddle
To learn more about JOINs see: A Visual Explanation of SQL Joins
You select only the passed marks, this is the reason of null-s appears near falied results.
If you want to select the failed marks you can use IF condition
select s.id,s.name,IF(p.marks = null, nn.marks, p.marks) as markss
from student s
left join pass p on s.id=p.id
left join fail nn on s.id=nn.id
order by markss desc;
Or you can use union of the passed and failed results.
select s.id,s.name, u.marks
from student s
left join ( (SELECT * FROM pass) UNION (SELECT * FROM fail) ) as n ON n.id = s.id
order by marks desc;
You need to understand how the different joins work to understand why you receive NULL for the marks column.
Take a look here:A Visual Explanation of SQL Joins
The relevant example for you is:
LEFT OUTER JOIN:
SELECT * FROM TableA
LEFT OUTER JOIN TableB
ON TableA.name = TableB.name
id name id name
-- ---- -- ----
1 Pirate 2 Pirate
2 Monkey null null
3 Ninja 4 Ninja
4 Spaghetti null null
The Null values you received for the marks column are rows that have no match in the left joined tables.
(the left part of the Venn diagram) the values that does have a value are the cross section between the tow groups of the Venn Diagram.
specifics for your example:
select s.id,s.name,p.marks
from student s
left join pass p on s.id=p.id
left join (select f.marks,f.id from fail f ) as nn on s.id=nn.id
order by marks desc;
The output i got is this:
id | name | Marks
-------------------
1 | B | 80
2 | C | 75
3 | A | Null
4 | D | NUll
This will return all student rows when.
students that have a passing gtade will display the grade and thous who don't will display null.
Try the below Query, use COALESCE
select s.id,s.name,COALESCE(p.marks , nn.marks) as marks
from student s
left join pass p on s.id=p.id
left join fail nn on s.id=nn.id
order by marks desc;
SQL Fiddle

JOIN 4 Tables with meta_table

I have this database structure:
sites
id | name
1 | Site 1
2 | Site 2
locations
id | city
23 | Baltimore
24 | Annapolis
people
id | name
45 | John
46 | Sue
sites_meta
id | site_id | meta_name | meta_value
1 | 1 | local | 23
2 | 1 | person | 45
3 | 2 | local | 24
4 | 2 | person | 46
So, as you can see, Site 1 (id 1) is in Baltimore and is associated with John, Site 2 (id 2) is in Annapolis and associated with Sue.
I need to figure out a clever sql statement that can return
id | name | id | city | id | name
1 | Site 1 | 23 | Baltimore | 45 | John
2 | Site 2 | 24 | Annapolis | 46 | Sue
I would be super appreciative if anyone can help me out. I've tried a few combinations of a select statement, but I keep getting stuck with using two values from the sites_meta table.
select
s.id as siteId,
s.name as siteName,
max(l.id) as locationId,
max(l.city) as city,
max(p.id) as personId,
max(p.name) as personName
from
sites_meta sm
join sites s on s.id = sm.site_id
left join locations l on l.id = sm.meta_value and sm.meta_name = 'local'
left join people p on p.id = sm.meta_value and sm.meta_name = 'person'
group by
s.id,
s.name
You can probably imagine how this kind of "meta" table might become a pain... especially as more items are added to it.
Instead, you might consider replacing it with two new tables, sites_locations and sites_people.
SELECT
s.id,s.name,l.id,l.city,p.id,p.name
FROM
sites s
INNER JOIN sites_meta sm1 ON s.id = sm1.site_id
INNER JOIN sites_meta sm2 ON s.id = sm2.site_id
INNER JOIN locations l ON sm1.meta_value = l.id AND sm1.meta_name = 'local'
INNER JOIN people p ON sm2.meta_value = p.id AND sm2.meta_name = 'person'
;