Continue Learning SQL By Examples_ One Hundred Examples of SQL Queries, Stored Procedures, and Functions for MySQL
Continue Learning SQL By Examples_ One Hundred Examples of SQL Queries, Stored Procedures, and Functions for MySQL
SERGEY SKUDAEV
Copyright © 2025 Sergey Skudaev
All rights reserved.
Table of Contents
Introduction
Simple Select Statement
Complex Select Statements
Aggregate Functions
Window Functions
The RANK and DENSE_RANK Functions
Conditional Logic with CASE
Stored Procedures
Stored Functions
Homework
Create Stored Procedures
Create Stored Functions
Homework Solutions
Queries From The Homework
Stored Procedures From The Homework
Functions From The Homework
Resources
Author's Books On Amazon.com
About The Author
INTRODUCTION
This book is for readers familiar with SQL and have read my previous book,
Learn SQL by Examples. In the new book, we will create more complex SQL
queries. For example, the queries that join more than three tables. You will learn
to use new functions, regular expressions, and more.
To refresh your memory, and for those who did not read my first book, I will
repeat the information about the database structure used in my previous book
and this book.
SQL stands for Structured Query Language. SQL is designed to manage data
in a relational database management system (RDBMS). The relational database is
based on the relational model. In the relational database, each record in one
table is linked to the corresponding record(s) in the other table(s). The records´
IDs are used to link related records
SQL is a standard way to manipulate relational database data, and any
computer language programmer should know SQL. Even though it is standard,
there are slightly different SQL "dialects" for various databases. This book
provides examples of SQL queries that I tested on MySQL.
A database is an application that stores data in organized data sets and manages
data storage, retrieval, and modification. In the relational database, data is stored
in tables. Each table holds many records. Each record contains many fields and
is identified with an ID that is the record's primary key. For example, this is a
computer courses table. The primary key is the courseid field.
In our tutorial, we will use a computer school database. Tables in the
database are linked. There are three types of relationships between related tables:
One to One,
One to Many,
Many to Many.
When one record in one table is related to only one record in another, it is a
“One to One” relationship. For example, one order record in the ORDER table
is related to one order details record in the OrderDetails table.
When one record in one table relates to many records in another, it is a
“ONE to Many” relationship. I created a ‘teachers’ table and a ‘cars’ table in our
school database. One teacher may have a few cars, but each car can belong to
only one teacher.
The ‘cars’ table has the following fields: cid, teacherid, cmake, and cyear.
The ‘cars’ table has a primary key cid and a foreign key teacherid. The
teacherid field links a ‘cars’ table to a ‘teachers’ table record.
The ‘teachers’ table has the following fields:
teacherid, lastname, firstname, email, phone, hiredate, trate.
A student may take many computer courses, and many students can take
the same computer course. The relationship between students and courses is
MANY TO MANY. To implement a "MANY TO MANY" relationship, we
must create the student_course table. It will hold studentid and courseid as
foreign keys and link student records with course records.
The ‘students’ table has the following fields:
studentid, lastname, firstname, email, phone, age, gender, startdate.
How do we enter the address for each student? Entering an address for each
student at a time will take time. Let’s do it at once. First, we can enter the
city, state, and zip code for all students. We can do it if all students live in
one city.
The students cannot live in one house, so the house number and street
must be unique for each student. We can achieve that using SQL
functions.
Explanation:
The FLOOR function rounds a decimal number to the nearest lower
integer.
The RAND() function in SQL generates a random floating-point
number between 0 (inclusive) and 1 (exclusive).
Output 1:
street city state zip
28 15th Avenue St.Petersburg FL 33711
28 7th Avenue St.Petersburg FL 33711
28 12th Avenue St.Petersburg FL 33711
13 4th Avenue St.Petersburg FL 33711
15 18th Avenue St.Petersburg FL 33711
20 19th Avenue St.Petersburg FL 33711
12 18th Avenue St.Petersburg FL 33711
30 7th Avenue St.Petersburg FL 33711
23 4th Avenue St.Petersburg FL 33711
200 West Ave N Clearwater FL 33587
100 West Ave N Clearwater FL 33587
106 Main street Dunedin FL 34697
How do you enter random birthdays for all students at once? Assuming
they were born about 18-25 years ago.
UPDATE students SET dob =
DATE_SUB(CURDATE(),
INTERVAL (18 + FLOOR(RAND() * 8)) YEAR;
Explanation:
Output 2:
dob
2003-07-13
1999-05-26
1999-11-11
2001-10-31
2001-01-25
2002-11-04
1999-06-22
2001-06-05
1999-11-20
2005-03-21
2006-03-11
2000-02-02
The student_course table has the following fields: studentid, courseid, paid,
score.
Explanation:
Output 3:
Let’s add values to the start date and end date. For the start date, you can add
values like we did for the student’s birthday. We can assume that each course
lasts one year to add value to the end date. Then, to get the end date, we can
add one year to the start date.
UPDATE schedule SET enddate =DATE_ADD(startdate, INTERVAL 1 YEAR);
startdate enddate
2020-01-11 2021-01-11
2020-01-11 2021-01-11
2020-01-12 2021-01-12
2020-01-12 2021-01-12
2020-01-12 2021-01-12
2020-01-12 2021-01-12
2020-01-12 2021-01-12
2020-01-11 2021-01-11
2020-01-12 2021-01-12
2020-01-12 2021-01-12
2024-01-05 2024-12-20
2022-09-01 2023-09-01
2022-09-24 2023-09-24
2022-09-01 2023-09-01
2022-09-01 2023-09-01
2022-09-03 2023-09-03
2022-09-03 2023-09-03
2024-01-05 2024-12-20
2024-01-05 2024-12-20
Then, we may update the ‘teachers’ table and enter the random age at once.
UPDATE teachers SET age = FLOOR(40 + RAND() * 10);
Output 5:
firstname lastname age
John Smith 49
John Jefferson 49
James Barry 49
Michael Murphy 48
Julia Williams 43
John Niven 43
Christine Merry 45
Greg Gerald 45
Michael Ross 44
Michael Kelly 42
Norm Karon 41
The relationship between the ‘teachers’ table and the courses table is
"MANY TO MANY" because one teacher may teach many courses, and the
same course may be taught by many ‘teachers’. The ‘schedule’ table has a foreign
key for a teacher and course records and links the ‘teachers’ table and courses
table.
To practice SQL, you need to use MySQL database version 9.
First, you need to create tables and insert data to use these tables to learn SQL
with this book later.
I provided Create Table statements so that you can create tables in MySQL.
You can download a txt file from the link at the end of the book.
You can copy and paste these “create table” statements. When you create the
database and tables, you can copy and paste insert statements with the data to
fill all the tables.
If you do not want to recreate the tables on your PC, you can use my web
page for practice.
www.learn-coding.today/continue_learn_sql.php
My website server has all the tables and data used for this book, but my host
database version is not high enough, and some examples will not work.
You will get the same result if you list all table fields in the SELECT clause.
SELECT courseid, coursename, hours, cost FROM courses.
List only those you need if you do not want to display values from all fields.
Besides, you can choose by which field or fields you want to sort records. For
that, you should add an ORDER BY clause.
SELECT courseid, coursename, hours, cost
FROM courses
ORDER BY cost, coursename
Output 7:
Output 8:
Output 9:
lastname coursename
Smith C++
Ross Visual Basic
Murphy HTML
Murphy CSS
Murphy Unix
Williams Pearl
Williams Assembly
Barry Visual Basic
Niven Python
Gerald JavaScript
Williams Assembly
Jefferson Java
Jefferson C++
Williams Python
Williams Unix
Kelly PHP
Karon PHP
Jefferson Java
Gerald JavaScript
Initially, we join the ‘teachers’ table with the ‘schedule’ table. To emphasize
this, I enclosed these tables in parentheses. Next, we join the result (in
parentheses) with the third table. Parentheses are optional in MySQL and Oracle
but mandatory in MS Access.
The following query is almost identical to the previous one and returns the
same result.
SELECT
t.lastname,
c.coursename
FROM (teachers t
INNER JOIN schedule s ON s.teacherid=t.teacherid)
INNER JOIN courses c ON c.courseid=s.courseid;
Output 10:
coursename
lastname
Smith C++
Ross Visual Basic
Murphy HTML
Murphy CSS
Murphy Unix
Williams Pearl
Williams Assembly
Barry Visual Basic
Niven Python
Gerald JavaScript
Williams Assembly
Jefferson Java
Jefferson C++
Williams Python
Williams Unix
Kelly PHP
Karon PHP
Jefferson Java
Gerald JavaScript
In the ON statements of the first query, the sequence of IDs matches the
sequence of tables. In the second query, the sequence of IDs in the ON
statements is reversed compared to the sequence of tables.
The first query is preferable for readability and convention. It follows the
more conventional order of specifying the join conditions, i.e., ON clauses
match the order of tables being joined.
It presents a more logical and sequential flow of joins, making it easier for
someone reading the query to understand which tables are being joined and on
what conditions.
Which teachers teach Java?
SELECT DISTINCT t.firstname,
t.lastname,
c.coursename
FROM (teachers t
INNER JOIN schedule s ON t.teacherid=s.teacherid)
INNER JOIN courses c ON s.courseid=c.courseid
AND c.coursename='Java'
ORDER BY t.lastname;
Output 11:
Output 12:
Output 13:
Explanation:
The query effectively retrieves the desired information. Here's a breakdown
of how it works:
It joins the ‘teachers’, schedule, and courses tables based on the teacherid and
courseid foreign key relationships.
The WHERE clause filters the results only to include rows where the
coursename is 'C++'.
The SELECT clause specifies the columns to be retrieved: firstname,
lastname, and coursename.
The resulting output will be a list of teachers who teach the "C++" course,
along with their first, last, and course names.
What if we do not want to select all records a query returns but limit it to
a certain number? The following query demonstrates how to do that.
Output 15:
SELECT * FROM (
SELECT DISTINCT s.firstname,
s.lastname,
c.coursename
FROM students s
INNER JOIN student_course sc ON s.studentid=sc.studentid
INNER JOIN courses c ON sc.courseid=c.courseid
ORDER BY s.lastname )
WHERE ROWNUM <= 3
Explanation:
The objective is to find students with at least one score greater than or equal to
90.
The SELECT Clause lists all fields that need to be selected.
The FROM Clause specifies the primary table from which to select data,
aliasing the ‘students’ table as s.
The INNER JOIN joins the ‘students’ table with the student_course table,
matching records where the studentid in the ‘students’ table equals the studentid
in the student_course table.
The INNER JOIN courses clause joins the student_course table with the
courses table, matching records where the courseid in the student_course table
equals the courseid in the courses table.
The WHERE clause filters the results only to include records with scores
greater than or equal to 90 in the student_course table.
This query retrieves the first name and last name of students, along with their
course names and scores, but only for records where the student's score is 90 or
higher. The query uses inner joins to combine data from the students,
student_course, and courses tables based on matching studentid and courseid
Find the student with the highest score in C++.
SELECT
s.firstname,
s.lastname,
sc.score
FROM
students s
INNER JOIN student_course sc ON s.studentid = sc.studentid
INNER JOIN courses c ON sc.courseid = c.courseid
WHERE
c.coursename = 'C++'
ORDER BY
sc.score DESC
LIMIT 1;
Output 17:
Output 18:
Explanation:
The SELECT DISTINCT selects the distinct first and last names of students
and the schedule start time to avoid duplicates.
The FROM clause specifies the ‘students’ table and gives it an alias ‘s’ for easier
referencing.
The JOIN student_course sc ON s.studentid = sc.studentid clause joins the
‘students’ table with the ‘student_course’ table based on the studentid.
The JOIN schedule sch ON sc.courseid = sch.courseid clause joins the
student_course table with the ‘schedule’ table based on the courseid.
The WHERE sch.starttime < '12:00:00' clause filters the results to include only
schedules where the starttime is before 12:00 PM (noon), indicating morning
courses.
This query will effectively retrieve the names of students who attend morning
courses by joining the relevant tables and applying the appropriate filter.
Find students who attend only afternoon classes and not morning classes.
SELECT DISTINCT s.studentid, s.firstname, s.lastname
FROM students s
JOIN student_course sc ON s.studentid = sc.studentid
JOIN schedule sch ON sc.courseid = sch.courseid
WHERE s.studentid NOT IN (
SELECT s.studentid
FROM students s
JOIN student_course sc ON s.studentid = sc.studentid
JOIN schedule sch ON sc.courseid = sch.courseid
WHERE sch.starttime < '12:00:00 PM'
);
Output 19:
studentid firstname lastname
12 John Clifford
13 Ann Williams
The subquery fetches student IDs from morning courses.
SELECT s.studentid
FROM students s
JOIN student_course sc ON s.studentid = sc.studentid
JOIN schedule sch ON sc.courseid = sch.courseid
WHERE sch.starttime < '12:00:00 PM'
Then, in the main query, we select only IDs that are not included in the
subquery result.
Find students who attend only morning classes and not afternoon classes.
SELECT DISTINCT s.studentid, s.firstname, s.lastname
FROM students s
JOIN student_course sc ON s.studentid = sc.studentid
JOIN schedule sch ON sc.courseid = sch.courseid
WHERE s.studentid NOT IN (
SELECT s.studentid
FROM students s
JOIN student_course sc ON s.studentid = sc.studentid
JOIN schedule sch ON sc.courseid = sch.courseid
WHERE sch.starttime >= '12:00:00'
);
Output 20:
The subquery of the main query retrieves the student IDs of those who attend
afternoon classes.
Then, in the main query, we select IDs that are not included in the subquery
result.
AGGREGATE FUNCTIONS
How do we understand aggregated functions? Let’s review a simple
example with the COUNT function. Suppose you need to count how
many courses each student attends.
Output 21:
lastname courseid coursename
Barklay 2 Java
Barklay 9 JavaScript
Barklay 6 Pearl
Brown 5 HTML
Brown 2 Java
Brown 2 Java
Brown 6 Pearl
Brown 6 Pearl
Brown 10 Python
Brown 1 Visual Basic
Brown 1 Visual Basic
Clifford 12 Java
Clifford 9 JavaScript
Cremette 5 HTML
Cremette 11 Unix
Cremette 1 Visual Basic
Folkner 7 CSS
Folkner 6 Pearl
Folkner 1 Visual Basic
Holden 6 Pearl
Holden 4 PHP
Holden 1 Visual Basic
James 8 Assembly
James 3 C++
James 9 JavaScript
James 1 Visual Basic
Johnson 3 C++
Johnson 2 Java
Johnson 6 Pearl
Michaels 6 Pearl
Michaels 14 Visual Basic
Williams 13 C++
To count courses for each student you will group records per student.
Let’s start from the bottom.
Lastname Courseid Coursename
Williams 13 C++
Michaels 14 Visual Basic
6 Pearl
Johnson 6 Pearl
2 Java
3 C++
James 1 Visual Basic
9 JavaScript
3 C++
8 Assembly
And so on.
You can do the same with your query using the GROUP BY clause.
Find out how many courses each student attends.
SELECT
s.firstname,
s.lastname,
COUNT(DISTINCT sc.courseid) AS enrolled_courses
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.firstname, s.lastname
ORDER BY
s.lastname;
Output 22:
Output 23:
courseid coursename lastname
8 Assembly James
3 C++ James
3 C++ Johnson
13 C++ Williams
7 CSS Folkner
You must group records per course to calculate the number of students for each
course. Then, you will see that one student attends the Assembly language
course, three students attend C++, and one student attends CSS.
.
Courseid Coursename lastname
8 Assembly James
3 C++ James
Johnson
Williams
7 CSS Folkner
You will get the same result with the query using GROUP BY courseid.
SELECT
c.coursename,
COUNT(DISTINCT s.studentid) as students_number
FROM
students s
JOIN
student_course sc ON s.studentid=sc.studentid
JOIN
courses c ON sc.courseid=c.courseid
GROUP BY
c.coursename
ORDER BY c.coursename;
Output 24:
coursename students_number
Assembly 1
C++ 3
CSS 1
HTML 2
Java 4
JavaScript 3
Pearl 7
PHP 1
Python 1
Unix 1
Visual Basic 7
One student attends the Assembly language course, three attend C++, and one
attend CSS.
Find the student who has enrolled in the most courses.
SELECT
s.studentid,
s.firstname,
s.lastname,
COUNT(sc.courseid) AS enrolled_courses
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.studentid, s.firstname, s.lastname
ORDER BY
enrolled_courses DESC
LIMIT 1;
Output 25:
studentid firstname lastname enrolled_courses
6 Cindy Brown 5
How does this query work? First, it gets student information and course
enrollments by JOIN the ‘students’ table with the ‘student_course’ table. Then it
groups the results by studentid, firstname, and lastname to count the number of
courses for each student.
Then, the results are ordered in descending order based on the
enrolled_courses count.
Then, the result is limited to the first row, representing the student with the
most enrolled courses.
Find the average score for each student.
SELECT
s.firstname,
s.lastname,
AVG(sc.score) AS average_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.firstname, s.lastname
Order by s.lastname;
Output 26:
firstname lastname average_score
Julia Barklay 93.3333
Ryan Brown 73.6667
Cindy Brown 78.6000
John Clifford 70.0000
Alison Cremette 64.0000
James Folkner 89.3333
Michael Holden 58.0000
Molly James 66.7500
Michael Johnson 78.3333
Holly Michaels 55.5000
Ann Williams 100.0000
This query selects each student's first and last name and the average score.
Since scores are stored in the student_course table, the query joins the
‘students’ table with the ‘student_course’ table to get student information along
with their scores.
In the GROUP BY clause, the query groups the results by first name and last
name to calculate each student's average score.
The AVG(sc.score) function calculates each student's average score based on
their scores in the student_course table.
This query will effectively calculate the average score for each student in the
database.
Find a student with the highest average score.
SELECT
s.firstname, s.lastname,
ROUND( AVG(sc.score)) AS average_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.firstname, s.lastname
ORDER BY
average_score DESC
LIMIT 1;
Output 27:
firstname lastname average_score
Ann Williams 100
Explanation:
The SELECT clause selects the firstname and lastname and average scores
for each student.
The FROM clause joins the ‘students’ table with the ‘student_course’ table to
get student information along with their scores.
The GROUP BY clause groups the results by firstname and lastname to
calculate the average score for each student.
The AVG function calculates the average score for the student based on their
scores in the student_course table.
The ROUND function rounds values to the whole number.
The ORDER BY average_score DESC clause orders the results in
descending order based on the average_score.
The LIMIT 1 clause limits the result to the first row, representing the student
with the highest average score.
This query will effectively identify the student with the highest average score
across all their courses.
Find a course average score.
Output 28:
coursename avg_score
Assembly 76
C++ 76
CSS 77
HTML 82
Java 83
JavaScript 78
Pearl 72
PHP 55
Python 81
Unix 63
Visual Basic 73
Find a course with the highest average score.
Output 29:
coursename avg_score
Java 83
In the above query, we sort records by the score in descending order and take
only the first record representing a student with the highest score.
In the same way, we can find a student with the lowest score if we sort records
in ascending order.
Find a course with the lowest average score.
Output 30:
coursename avg_score
PHP 55
How to find a teacher who has the most cars
SELECT
t.firstname,
t.lastname,
COUNT(c.cid) AS num_cars
FROM
teachers t
INNER JOIN cars c ON t.teacherid = c.teacherid
GROUP BY
t.firstname, t.lastname
HAVING
COUNT(c.cid) = (
SELECT MAX(car_count)
FROM (
SELECT
COUNT(c2.cid) AS car_count
FROM teachers t2
INNER JOIN cars c2 on c2.teacherid=t2.teacherid
GROUP BY t2.firstname, t2.lastname
) AS MaxCarCounts
);
Output 31:
Explanation:
The innermost Query returns the car count for each teacher.
SELECT
COUNT(c2.cid) AS car_count
FROM
teachers t2
INNER JOIN
cars c2 ON c2.teacherid = t2.teacherid
GROUP BY
t2.firstname, t2.lastname
The middle query selects the maximum car count from the result of the
innermost query.
SELECT MAX(car_count)
FROM (
SELECT
COUNT(c2.cid) AS car_count
FROM
teachers t2
INNER JOIN
cars c2 ON c2.teacherid = t2.teacherid
GROUP BY
t2.firstname, t2.lastname
) AS MaxCarCounts
The outer query retrieves the teachers' first name, last name, and car count and
selects the records matching the maximum car count.
SELECT
t.firstname,
t.lastname,
COUNT(c.cid) AS num_cars
FROM
teachers t
INNER JOIN
cars c ON t.teacherid = c.teacherid
GROUP BY
t.firstname, t.lastname
HAVING
COUNT(c.cid) = (
SELECT MAX(car_count)
FROM (
SELECT
COUNT(c2.cid) AS car_count
FROM
teachers t2
INNER JOIN
cars c2 ON c2.teacherid = t2.teacherid
GROUP BY
t2.firstname, t2.lastname
)
SELECT
t.firstname,
t.lastname,
COUNT(c.cid) AS num_cars
FROM
teachers t
INNER JOIN
cars c ON t.teacherid = c.teacherid
GROUP BY
t.firstname, t.lastname
HAVING
COUNT(c.cid) = (
SELECT COUNT(c2.cid)
FROM teachers t2
INNER JOIN cars c2 ON t2.teacherid = c2.teacherid
GROUP BY t2.firstname, t2.lastname
ORDER BY COUNT(c2.cid) DESC
LIMIT 1
);
Output 32:
firstname lastname num_cars
John Smith 2
John Jefferson 2
This query achieves the same goal by directly comparing the count of cars for
each teacher to the highest car count found in the inner query.
Explanation:
The innermost query returns the car count for each teacher.
SELECT
COUNT(c2.cid) AS car_count
FROM
teachers t2
INNER JOIN
cars c2 ON c2.teacherid = t2.teacherid
GROUP BY
t2.firstname, t2.lastname
The middle query selects the maximum car count from the result of the
innermost query, ordering them in descending order and limiting the result to
the highest value.
SELECT COUNT(c2.cid)
FROM
teachers t2
INNER JOIN
cars c2 ON t2.teacherid = c2.teacherid
GROUP BY
t2.firstname, t2.lastname
ORDER BY
COUNT(c2.cid) DESC
LIMIT 1
The outer query retrieves the first name, last name, and car count for all teachers
and selects the records that match the maximum car count obtained from the
middle query.
SELECT
t.firstname,
t.lastname,
COUNT(c.cid) AS num_cars
FROM
teachers t
INNER JOIN
cars c ON t.teacherid = c.teacherid
GROUP BY
t.firstname, t.lastname
HAVING
COUNT(c.cid) = (
SELECT COUNT(c2.cid)
FROM teachers t2
INNER JOIN cars c2 ON t2.teacherid = c2.teacherid
GROUP BY t2.firstname, t2.lastname
ORDER BY COUNT(c2.cid) DESC
LIMIT 1
);
Output 33:
coursename num_students
Visual Basic 8
This query selects the number of students in each course, sorts the records by
the number of students in descending order, and retrieves the first record.
Find average scores for male and female students.
SELECT
s.gender,
ROUND(AVG(sc.score)) AS average_score
FROM
students s
INNER JOIN student_course sc ON s.studentid = sc.studentid
GROUP BY
s.gender;
Output 34:
gender average_score
male 74
female 75
You can also use a more complex query to compare the average scores for
specific courses. Still, the basic approach involves grouping the data by gender
and calculating the average score for each group.
SELECT
s.gender, c.coursename,
ROUND(AVG(sc.score)) AS average_score
FROM
students s
INNER JOIN student_course sc ON s.studentid = sc.studentid
INNER JOIN courses c ON c.courseid = sc.courseid
GROUP BY
s.gender, c.coursename order by c.coursename;
Output 35:
Output 36:
Output 37:
Output 38:
This version of the query only includes the cost of courses for which payment
has been made.
Output 39:
The outer query retrieves the student ID, first name, last name, and the total
cost of courses for students whose total cost matches the maximum total cost
found in the middle query.
To check if the student actually paid, we use a CASE statement.
SELECT
s.studentid,
s.firstname,
s.lastname,
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) AS
total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid, s.firstname, s.lastname
HAVING
SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END) = (SELECT
MAX(total_paid)
FROM
(SELECT SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END)
AS total_paid
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid) paid_most);
Output 40:
studentid firstname lastname total_spent
6 Cindy Brown 8699.8
Now we can see that all of Cindy’s courses cost $10,400, but she paid only
$8,700
Try to write a query to find a student who paid the least without looking at the
query below.
SELECT
s.studentid,
s.firstname,
s.lastname,
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) AS
total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid, s.firstname, s.lastname
HAVING
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END)) = (SELECT
MIN(total_paid)
FROM
(SELECT ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE
0 END)) AS total_paid
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid) paid_least);
Output 41:
studentid firstname lastname total_spent
5 Holly Michaels 0
13 Ann Williams 0
This query returned students who have not paid. To exclude students who have
not paid at all and find those who have paid but paid the least, we can modify
our query by ensuring that only students with a positive
total_spent are considered. Here's the updated query:
SELECT
s.studentid,
s.firstname,
s.lastname,
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) AS
total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid, s.firstname, s.lastname
HAVING
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) > 0 AND
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) = (
SELECT MIN(total_paid)
FROM (
SELECT
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) AS
total_paid
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid
HAVING
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) >
0
) AS paid_least
);
Output 42:
studentid firstname lastname total_spent
12 John Clifford 999.95
Explanation:
The main query aims to find the students who have paid the least, excluding
those who haven't paid anything at all.
The main query retrieves the student ID, first name, last name, and the total
amount spent by each student, rounded to two decimal places.
The JOIN clauses combine the students, student_course, and courses tables
based on their relationships.
The GROUP BY clause groups the results by student ID, first name, and last
name.
The HAVING clause filters the results to include only students who have spent
a positive amount and whose total spent matches the minimum amount spent
by any student who has paid.
HAVING
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) > 0 AND
The subquery calculates the minimum amount spent by any student who has
paid.
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) = (
SELECT MIN(total_paid)
FROM (
SELECT
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) AS
total_paid
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid
HAVING
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) > 0
) AS paid_least
);
The innermost query calculates the total amount spent by each student who has
paid.
SELECT
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) AS
total_paid
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid
HAVING
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) > 0
The middle query selects the minimum total amount paid from the results of the
innermost query.
SELECT MIN(total_paid)
FROM (
SELECT
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) AS
total_paid
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.studentid
HAVING
ROUND(SUM(CASE WHEN sc.paid = 1 THEN c.cost ELSE 0 END), 2) > 0
) AS paid_least
Output 42:
lastname coursename youngest_age
Clifford JavaScript 18
This query selects all students' records, sorts them in ascending order, and
returns the first record with the youngest age.
Find which course is attended by the oldest student.
SELECT
s.lastname,
c.coursename,
MAX(s.age) AS oldest_age
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.lastname, c.coursename
ORDER BY
oldest_age DESC
LIMIT 1;
Output 43:
SELECT
t.lastname,
c.coursename,
MIN(t.age) AS youngest_teacher
FROM
teachers t
JOIN
schedule s ON t.teacherid = s.teacherid
JOIN
courses c ON s.courseid = c.courseid
GROUP BY
t.lastname, c.coursename
ORDER BY
youngest_teacher
LIMIT 1;
Output 44:
Output 45:
lastname coursename oldest_teacher
Smith C++ 49
Find a male student who has the lowest average score.
SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'male'
GROUP BY
s.lastname
ORDER BY
avg_score ASC
LIMIT 1;
Output 46:
lastname avg_score
Holden 58.0000
Explanation:
The Join clause joins the students and student_course tables to get
student information and their scores.
The WHERE s.gender = 'male' clause filters the results to include only male
students.
The GROUP BY clause groups the results by s.lastname to calculate the average
score for each male student.
The ORDER BY clause sorts the average score in ascending order.
The Limit to one-row clause selects only the first-row student, who will be the
male student with the lowest average score.
Find a female student with the lowest average score.
SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'female'
GROUP BY
s.lastname
ORDER BY
avg_score ASC
LIMIT 1;
Output 47:
lastname avg_score
Michaels 55.5000
Let’s build a query that finds the male and the female student with the lowest
average score.
SELECT
s.gender,
s.firstname,
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.gender, s.firstname, s.lastname
HAVING
(s.gender = 'male' AND AVG(sc.score) = (SELECT MIN(avg_score)
FROM
(SELECT
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'male'
GROUP BY
s.lastname) AS male_subquery))
OR
(s.gender = 'female' AND AVG(sc.score) = (SELECT MIN(avg_score)
FROM
(SELECT
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'female'
GROUP BY
s.lastname) AS female_subquery));
Output 48:
gender firstname lastname avg_score
male Michael Holden 58.0000
female Holly Michaels 55.5000
Explanation:
The main query retrieves the gender, first name, last name, and average score of
students.
The JOIN clause combines the students and student_course tables based on the
student ID.
The GROUP BY clause groups the results by gender, first name, and last name.
The HAVING clause filters the results based on the conditions specified for
each gender.
The HAVING clause checks if the student's gender is 'male' and if their average
score equals the minimum average score among all male students.
Alternatively, it checks if the student's gender is 'female' and if their average
score equals the minimum average score among all female students.
The HAVING clause has two subqueries, one for each gender (male and
female).
The innermost subqueries calculate the average score for each student within
the respective gender group.
1. SELECT
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'male'
GROUP BY
s.lastname
2. SELECT
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'female'
GROUP BY
s.lastname
The middle subqueries find the minimum average score from the results of the
innermost subqueries for each gender.
1. SELECT MIN(avg_score)
FROM
(SELECT
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'male'
GROUP BY
s.lastname) AS male_subquery
2. SELECT MIN(avg_score)
FROM
(SELECT
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'female'
GROUP BY
s.lastname) AS female_subquery
The outer query retrieves the gender, first name, last name, and average score of
students whose average score matches the minimum average score obtained
from the respective middle subqueries for each gender.
We can get the same result with the query that uses UNION.
SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'male'
GROUP BY
s.lastname
UNION ALL
SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'female'
GROUP BY
s.lastname
ORDER BY
avg_score ASC
LIMIT 2;
Output 49:
gender firstname lastname avg_score
male Michael Holden 58.0000
female Holly Michaels 55.5000
When using the UNION, ensure each query returns the same number of
columns.
Let’s create one more query with UNION.
Find the male and the female student with the highest average score.
SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'male'
GROUP BY
s.lastname
UNION ALL
SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'female'
GROUP BY
s.lastname
ORDER BY
avg_score DESC
LIMIT 2;
Output 50:
lastname avg_score
Williams 100.0000
Barklay 93.3333
Find Students with Scores Above or Below Average.
SELECT
s.studentid,
s.firstname,
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.studentid, s.firstname, s.lastname
HAVING
AVG(sc.score) > (SELECT AVG(score) FROM student_course)
UNION ALL
SELECT
s.studentid,
s.firstname,
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.studentid, s.firstname, s.lastname
HAVING
AVG(sc.score) < (SELECT AVG(score) FROM student_course);
Output 51:
The query aims to find students whose average scores are either above or below
the overall average score of all students.
The first subquery retrieves students whose average scores are above average.
SELECT
s.studentid,
s.firstname,
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.studentid, s.firstname, s.lastname
HAVING
AVG(sc.score) > (SELECT AVG(score) FROM student_course)
The second subquery retrieves students whose average scores are below average.
SELECT
s.studentid,
s.firstname,
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.studentid, s.firstname, s.lastname
HAVING
AVG(sc.score) < (SELECT AVG(score) FROM student_course)
The UNION ALL operator combines the results of the two subqueries,
ensuring that both sets of students (those with above-average scores and those
with below-average scores) are included in the final result.
We can find the average score to check if our result is correct.
SELECT AVG(score) FROM student_course
Output 52:
AVG(score)
75.2941
Our query is correct. All students' average scores are above or below the overall
average score.
Let’s find students who enrolled in C++ or Java classes using the UNION
operator.
SELECT
s.studentid,
s.firstname,
s.lastname,
c.coursename
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
c.coursename = 'C++'
UNION ALL
SELECT
s.studentid,
s.firstname,
s.lastname,
c.coursename
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
c.coursename = 'Java';
Output 53:
Output 54:
lastname1 lastname2 avg_score1 avg_score2 score_diff
Johnson Brown 78.3333 76.7500 1.5833
Brown Johnson 76.7500 78.3333 1.5833
Explanation:
The query aims to find and display pairs of students whose average scores differ
by less than 2 points.
The first subquery, s1, calculates the average score for each student based on
their last name.
The JOIN clause combines the students and student_course tables based on the
student ID.
The GROUP BY clause groups the results by the student's last name.
The subquery returns the student's last name and average score as avg_score1.
(SELECT
s.lastname,
AVG(sc.score) AS avg_score1
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.lastname) AS s1
The second subquery, s2, is similar to s1 and calculates the average score for
each student based on their last name. It returns the students' last names and
average scores as avg_score2.
(SELECT
s.lastname,
AVG(sc.score) AS avg_score2
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.lastname) AS s2
The main query joins the results of the two subqueries, s1 and s2, to compare
the average scores of different students.
The ON clause ensures that the average scores differ by less than 2 points
(BETWEEN -2 AND 2).
It also ensures that the last names of the students being compared are different
(s1.lastname != s2.lastname).
The ABS() function calculates the absolute difference between the two students'
average scores.
The SELECT statement retrieves the two students' last names, their respective
average scores, and the absolute difference in their scores.
Let's modify this query so that one student is male and the other female.
SELECT
s1.lastname AS lastname1,
s2.lastname AS lastname2,
s1.avg_score as avg_score1,
s2.avg_score as avg_score2,
ABS(s1.avg_score - s2.avg_score) AS score_difference
FROM
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'male'
GROUP BY
s.firstname, s.lastname) AS s1
JOIN
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
s.gender = 'female'
GROUP BY
s.lastname) AS s2 ON ABS(s1.avg_score - s2.avg_score) <= 2;
Output 55:
Output 56:
Output 57:
Lastname1 Avg_Score1 Lastname2 Avg_Score2 Score_Diff
Williams 100.0000 Michaels 55.5000 44.5000
Two subqueries calculate the average score for each student.
The subqueries are joined on the condition s1.lastname != s2.lastname to ensure
we compare different students.
The function ABS(s1.avg_score - s2.avg_score) calculates the absolute
difference between the two students' average scores.
The ORDER BY clause sorts the results in descending order based on the
absolute difference in average scores.
The LIMIT 1 clause selects only the first row, representing the pair of students
with the greatest difference in average scores.
This query will find the two students with the largest difference in their average
scores.
This query assumes no duplicate first and last names are in the ‘students’ table.
If duplicate names are possible, you might need to include the studentid in the
join condition to ensure that the comparison is made between truly distinct
students.
Let’s see what our query returns without limit.
SELECT
s1.lastname AS Lastname1,
s1.avg_score AS Avg_Score1,
s2.lastname AS Lastname2,
s2.avg_score AS Avg_Score2,
ABS(s1.avg_score - s2.avg_score) AS Score_Diff
FROM
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.firstname, s.lastname) AS s1
JOIN
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.lastname) AS s2 ON s1.lastname != s2.lastname
ORDER BY
ABS(s1.avg_score - s2.avg_score) DESC
Output 58:
99 rows in set (0.02 sec)
We have eleven students in total. Thus, 11 people can form 55 unique pairs.
We got 99 records.
The result shows that this query shows the difference between a pair of
students' scores twice. One record shows the difference between Williams and
Michaels, and the second record shows the difference between Michael and
Williams.
For instance, if you have students with last names A and B, your query will
generate both pairs (A, B) and (B, A), while you only need one of those pairs.
To avoid this redundancy and get unique pairs, you can modify your query by
ensuring that each pair is only created once. One way to do this is by adding a
condition to ensure the second student's last name is lexicographically greater
than the first student's last name. Here's the updated query:
SELECT
s1.lastname AS Lastname1,
s1.avg_score AS Avg_Score1,
s2.lastname AS Lastname2,
s2.avg_score AS Avg_Score2,
ABS(s1.avg_score - s2.avg_score) AS Score_Diff
FROM
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.firstname, s.lastname) AS s1
JOIN
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.lastname) AS s2 ON s1.lastname < s2.lastname
ORDER BY
ABS(s1.avg_score - s2.avg_score) DESC
Output 59:
Lastname1 Avg_Score1 Lastname2 Avg_Score2 Score_Diff
Michaels 55.5000 Williams 100.0000 44.5000
Holden 58.0000 Williams 100.0000 42.0000
Barklay 93.3333 Michaels 55.5000 37.8333
Cremette 64.0000 Williams 100.0000 36.0000
Barklay 93.3333 Holden 58.0000 35.3333
Folkner 89.3333 Michaels 55.5000 33.8333
James 66.7500 Williams 100.0000 33.2500
Folkner 89.3333 Holden 58.0000 31.3333
Clifford 70.0000 Williams 100.0000 30.0000
Barklay 93.3333 Cremette 64.0000 29.3333
Barklay 93.3333 James 66.7500 26.5833
Brown 73.6667 Williams 100.0000 26.3333
Cremette 64.0000 Folkner 89.3333 25.3333
Barklay 93.3333 Clifford 70.0000 23.3333
Brown 78.6000 Michaels 55.5000 23.1000
Johnson 78.3333 Michaels 55.5000 22.8333
Folkner 89.3333 James 66.7500 22.5833
Johnson 78.3333 Williams 100.0000 21.6667
Brown 78.6000 Williams 100.0000 21.4000
Brown 78.6000 Holden 58.0000 20.6000
Holden 58.0000 Johnson 78.3333 20.3333
Clifford 70.0000 Folkner 89.3333 19.3333
Brown 73.6667 Michaels 55.5000 18.1667
Barklay 93.3333 Brown 76.7500 16.5833
Brown 73.6667 Holden 58.0000 15.6667
Brown 73.6667 Folkner 89.3333 15.6666
Barklay 93.3333 Johnson 78.3333 15.0000
Brown 78.6000 Cremette 64.0000 14.6000
Clifford 70.0000 Michaels 55.5000 14.5000
Cremette 64.0000 Johnson 78.3333 14.3333
Clifford 70.0000 Holden 58.0000 12.0000
Brown 78.6000 James 66.7500 11.8500
James 66.7500 Johnson 78.3333 11.5833
James 66.7500 Michaels 55.5000 11.2500
Folkner 89.3333 Johnson 78.3333 11.0000
Brown 78.6000 Folkner 89.3333 10.7333
Folkner 89.3333 Williams 100.0000 10.6667
Brown 73.6667 Cremette 64.0000 9.6667
Holden 58.0000 James 66.7500 8.7500
Brown 78.6000 Clifford 70.0000 8.6000
Cremette 64.0000 Michaels 55.5000 8.5000
Clifford 70.0000 Johnson 78.3333 8.3333
Brown 73.6667 James 66.7500 6.9167
Barklay 93.3333 Williams 100.0000 6.6667
Clifford 70.0000 Cremette 64.0000 6.0000
Cremette 64.0000 Holden 58.0000 6.0000
Brown 73.6667 Johnson 78.3333 4.6666
Barklay 93.3333 Folkner 89.3333 4.0000
Brown 73.6667 Clifford 70.0000 3.6667
Clifford 70.0000 James 66.7500 3.2500
Cremette 64.0000 James 66.7500 2.7500
Holden 58.0000 Michaels 55.5000 2.5000
Brown 78.6000 Johnson 78.3333 0.2667
This subquery s2 is similar to s1 and calculates the average score for each
student based on their last name.
It returns the student's last name and average score, aliased as avg_score.
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.lastname) AS s2
The main query joins the results of the two subqueries, s1 and s2, to compare
the average scores of different students.
The ON clause ensures that the last names of the students being compared are
different (s1.lastname < s2.lastname). This condition ensures that the last names
are different and that the second student's last name is lexicographically greater
than the first student's last name. It prevents the same pair of students from
being compared twice (e.g., comparing student A with student B and then
student B with student A).
The ORDER BY clause sorts the results by the absolute difference in average
scores in descending order (ABS(s1.avg_score - s2.avg_score) DESC).
The SELECT statement retrieves the last names of the two students, their
respective average scores, and the absolute difference in their scores.
Let’s add LIMIT1 and see if we get the same result.
SELECT
s1.lastname AS Lastname1,
s1.avg_score AS Avg_Score1,
s2.lastname AS Lastname2,
s2.avg_score AS Avg_Score2,
ABS(s1.avg_score - s2.avg_score) AS Score_Diff
FROM
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.firstname, s.lastname) AS s1
JOIN
(SELECT
s.lastname,
AVG(sc.score) AS avg_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
GROUP BY
s.lastname) AS s2 ON s1.lastname < s2.lastname
ORDER BY
ABS(s1.avg_score - s2.avg_score) DESC
LIMIT 1;
Output 60:
Output 61:
Lastname1 Avg_Score1 Lastname2 Avg_Score2 Score_Diff
Brown 78.6000 Johnson 78.3333 0.2667
This query finds a pair of students with the closest average scores.
Two subqueries calculate the average score for each student, aliased as s1 and
s2.
The JOIN clause uses s1.lastname < s2.lastname to ensure that each pair of
students is considered only once.
ORDER BY ABS(s1.avg_score - s2.avg_score) ASC sorts the results in
ascending order based on the absolute difference in average scores.
LIMIT 1 selects only the first row, representing the pair of students with the
smallest difference in average scores.
WINDOW FUNCTIONS
Let's analyze the simplest query that uses the OVER PARTITION BY window
function.
SELECT
studentid,
courseid,
score,
SUM(score) OVER (PARTITION BY courseid ORDER BY studentid) AS
running_total
FROM
student_course;
Output 62:
studentid courseid score running_total
2 1 94 94
4 1 52 146
6 1 80 226
8 1 59 285
9 1 98 383
10 1 62 445
11 1 85 530
2 2 85 85
3 2 83 168
6 2 92 334
6 2 74 334
7 2 86 420
3 3 58 58
4 3 69 127
10 4 55 55
6 5 93 93
8 5 70 163
3 6 94 94
5 6 55 149
6 6 54 203
7 6 94 297
9 6 93 390
10 6 57 447
11 6 55 502
9 7 77 77
4 8 76 76
4 9 70 70
7 9 100 170
12 9 65 235
11 10 81 81
8 11 63 63
12 12 75 75
13 13 100 100
5 14 56 56
In the first row, the score is 94, and the running_total is 94.
In the second row, the score is 52, and the running_total=94 + 52 = 146.
In the third row, the score is 80, and the running_total=94 + 52 + 80 = 226.
And so on until the courseid is changed.
In the last row of the courseid equal to 1, we have the score = 85 and the
total_running = 94 + 52 + 80 + 59 + 98 + 62 + 85 = 530.
Explanation:
The query selects the following columns from the student_course table:
the studentid, which is the unique identifier for each student,
the courseid, which is the unique identifier for each course,
the score received by the student in the course.
The SUM(score) OVER (PARTITION BY courseid ORDER BY
studentid) AS running_total part of the query calculates a running total of
scores within each course.
The SUM(score) aggregate function calculates the sum of the score column.
The OVER (PARTITION BY courseid ORDER BY studentid is a window
function that partitions the data by courseid and sorts it by studentid. For each
row, it calculates the running total of scores up to that row within the same
partition (course).
We assign the running_total alias to the result of the window function.
The query calculates a running total of scores for each student within each
course. The running total is accumulated as the rows are ordered by studentid.
Calculate the running count of courses for each student.
SELECT
studentid,
courseid,
score,
count(courseid) OVER (PARTITION BY studentid ORDER BY courseid) AS
running_total
FROM
student_course;
Output 63:
studentid courseid score running_total
2 1 94 1
2 2 85 2
3 2 83 1
3 3 58 2
3 6 94 3
4 1 52 1
4 3 69 2
4 8 76 3
4 9 70 4
5 6 55 1
5 14 56 2
6 1 80 1
6 2 74 3
6 2 92 3
6 5 93 4
6 6 54 5
7 2 86 1
7 6 94 2
7 9 100 3
8 1 59 1
8 5 70 2
8 11 63 3
9 1 98 1
9 6 93 2
9 7 77 3
10 1 62 1
10 4 55 2
10 6 57 3
11 1 85 1
11 6 55 2
11 10 81 3
12 9 65 1
12 12 75 2
13 13 100 1
Output 64:
studentid courseid score running_total
2 1 94 1
4 1 52 2
6 1 80 3
8 1 59 4
9 1 98 5
10 1 62 6
11 1 85 7
2 2 85 1
3 2 83 2
6 2 92 4
6 2 74 4
7 2 86 5
3 3 58 1
4 3 69 2
10 4 55 1
6 5 93 1
8 5 70 2
3 6 94 1
5 6 55 2
6 6 54 3
7 6 94 4
9 6 93 5
10 6 57 6
11 6 55 7
9 7 77 1
4 8 76 1
4 9 70 1
7 9 100 2
12 9 65 3
11 10 81 1
8 11 63 1
12 12 75 1
13 13 100 1
5 14 56 1
Let's break down this query step by step.
The query selects the following columns from the student_course table:
studentid, courseid, and course.
The count(studentid) OVER (PARTITION BY courseid ORDER BY
studentid) AS running_total calculates
a running count of students for each course.
The count(studentid) aggregate function counts the number of studentid values.
The OVER (PARTITION BY courseid ORDER BY studentid) is a window
function that partitions the data by courseid and orders it by studentid. For each
row, it calculates the running total of students up to that row within the same
partition (course).
The running_total is an alias we assign to the result of the window function.
The query calculates a running total of the number of students in each course.
The running total is accumulated as the rows are ordered by studentid.
In this example, the rows within each courseid are partitioned and ranked based
on their score. The RANK function may have gaps in the ranking sequence
when there are ties, whereas DENSE_RANK will not have any gaps.
Output 65:
studentid courseid score st_rank st_dense_rank
9 1 98 1 1
2 1 94 2 2
11 1 85 3 3
6 1 80 4 4
10 1 62 5 5
8 1 59 6 6
4 1 52 7 7
6 2 92 1 1
7 2 86 2 2
2 2 85 3 3
3 2 83 4 4
6 2 74 5 5
4 3 69 1 1
3 3 58 2 2
10 4 55 1 1
6 5 93 1 1
8 5 70 2 2
3 6 94 1 1
7 6 94 1 1
9 6 93 3 2
10 6 57 4 3
5 6 55 5 4
11 6 55 5 4
6 6 54 7 5
9 7 77 1 1
4 8 76 1 1
7 9 100 1 1
4 9 70 2 2
12 9 65 3 3
11 10 81 1 1
8 11 63 1 1
12 12 75 1 1
13 13 100 1 1
5 14 56 1 1
Explanation:
The query selects the following columns from the student_course table:
studentid, courseid, score, st_rank, st_dense_rank.
The studentid is the unique identifier for each student;
The score is the score received by the student in the course; the
The “RANK() OVER (PARTITION BY courseid ORDER BY score DESC)
AS st_rank” clause calculates the rank of each student within their course.
The “DENSE_RANK() OVER (PARTITION BY courseid ORDER BY score
DESC) AS st_dense_rank” clause calculates the rank of each student within
their course but handles ties differently.
For example, for courseid 1, we have seven students, and their rank varies from
98 to 52. There are no duplicate scores, and the rank is the same as dense_rank,
from 1 to 7.
For courseid 6, we have seven students, and their score varies from 94 to 54.
We have two duplicate scores equal to 94. The first rank and dense_rank for the
94 score are the same and equal to 1. For the 93 score, the rank equals 3, while
the dense_rank equals 2.
If two rows are tied for rank 1, the next row will be ranked 3 by the rank
function and 2 by the dense_rank function.
Find the top 3 class ranks.
SELECT
s.firstname,
s.lastname,
c.coursename,
sc.score,
DENSE_RANK() OVER (PARTITION BY c.coursename ORDER BY sc.score DESC)
AS class_rank
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE class_rank <=3
ORDER BY
sc.score desc, class_rank, c.coursename;
Output 66:
firstname lastname coursename score class_rank
Molly James Assembly 76 1
Ann Williams C++ 100 1
Molly James C++ 69 2
Michael Johnson C++ 58 3
James Folkner CSS 77 1
Cindy Brown HTML 93 1
Alison Cremette HTML 70 2
Cindy Brown Java 92 1
Julia Barklay Java 86 2
Michael Johnson Java 83 3
Julia Barklay JavaScript 100 1
Molly James JavaScript 70 2
John Clifford JavaScript 65 3
Michael Johnson Pearl 94 1
Julia Barklay Pearl 94 1
James Folkner Pearl 93 2
Michael Holden Pearl 57 3
Michael Holden PHP 55 1
Ryan Brown Python 81 1
Alison Cremette Unix 63 1
James Folkner Visual Basic 98 1
Ryan Brown Visual Basic 85 2
Cindy Brown Visual Basic 80 3
Explanation:
The query creates a subquery named subquery that gathers detailed student
information along with their ranks within each course.
SELECT
s.firstname,
s.lastname,
c.coursename,
sc.score,
DENSE_RANK() OVER (PARTITION BY c.coursename ORDER BY sc.score DESC)
AS class_rank
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
The SELECT clause retrieves firstname, lastname, coursename, and score from
the joined tables.
The DENSE_RANK() function ranks students within each course
(c.coursename) based on their scores (sc.score) in descending order. This rank is
given the alias class_rank.
The main outer query then filters and orders the results from the subquery to
meet the specified criteria.
The ORDER BY clause orders the results by class_rank, coursename, and score
in descending order to present the data in a logical and readable format.
This query finds the top 3 students in each course based on their scores. It does
this by ranking students within each course and then filtering to include only
those with a rank of 3 or higher. The results are then ordered to show the top-
performing students in a structured and easy-to-read format.
Let’s create a query ranking students within each course and display the
student's first and last names.
SELECT
s.firstname,
s.lastname,
sc.courseid,
sc.score,
RANK() OVER (PARTITION BY sc.courseid ORDER BY sc.score DESC) AS
rank_in_course
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
ORDER BY sc.courseid, rank_in_course;
Output 67:
firstname lastname courseid score rank_in_course
James Folkner 1 98 1
Ryan Brown 1 85 2
Cindy Brown 1 80 3
Michael Holden 1 62 4
Alison Cremette 1 59 5
Molly James 1 52 6
Cindy Brown 2 92 1
Julia Barklay 2 86 2
Michael Johnson 2 83 3
Cindy Brown 2 74 4
Molly James 3 69 1
Michael Johnson 3 58 2
Michael Holden 4 55 1
Cindy Brown 5 93 1
Alison Cremette 5 70 2
Michael Johnson 6 94 1
Julia Barklay 6 94 1
James Folkner 6 93 3
Michael Holden 6 57 4
Holly Michaels 6 55 5
Ryan Brown 6 55 5
Cindy Brown 6 54 7
James Folkner 7 77 1
Molly James 8 76 1
Julia Barklay 9 100 1
Molly James 9 70 2
John Clifford 9 65 3
Ryan Brown 10 81 1
Alison Cremette 11 63 1
John Clifford 12 75 1
Ann Williams 13 100 1
Holly Michaels 14 56 1
The next query ranks students within each course based on their scores.
Finding the Class Rank of Each Student Within a Course using the
DENSE_RANK() function.
SELECT
s.firstname,
s.lastname,
sc.courseid,
sc.score,
DENSE_RANK() OVER (PARTITION BY sc.courseid ORDER BY sc.score DESC)
AS class_rank
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
ORDER BY
sc.courseid, class_rank;
Output 68:
firstname lastname courseid score class_rank
James Folkner 1 98 1
Ryan Brown 1 85 2
Cindy Brown 1 80 3
Michael Holden 1 62 4
Alison Cremette 1 59 5
Molly James 1 52 6
Cindy Brown 2 92 1
Julia Barklay 2 86 2
Michael Johnson 2 83 3
Cindy Brown 2 74 4
Molly James 3 69 1
Michael Johnson 3 58 2
Michael Holden 4 55 1
Cindy Brown 5 93 1
Alison Cremette 5 70 2
Michael Johnson 6 94 1
Julia Barklay 6 94 1
James Folkner 6 93 2
Michael Holden 6 57 3
Holly Michaels 6 55 4
Ryan Brown 6 55 4
Cindy Brown 6 54 5
James Folkner 7 77 1
Molly James 8 76 1
Julia Barklay 9 100 1
Molly James 9 70 2
John Clifford 9 65 3
Ryan Brown 10 81 1
Alison Cremette 11 63 1
John Clifford 12 75 1
Ann Williams 13 100 1
Holly Michaels 14 56 1
This query ranks students within each course based on their individual scores,
allowing you to identify the top-performing students in each course.
Let’s display the course name instead of the courseid.
SELECT
s.firstname,
s.lastname,
c.coursename,
sc.score,
DENSE_RANK() OVER (PARTITION BY c.coursename ORDER BY
sc.score DESC) AS class_rank
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
ORDER BY
c.coursename, class_rank;
Output 69:
firstname lastname coursename score class_rank
Molly James Assembly 76 1
Ann Williams C++ 100 1
Molly James C++ 69 2
Michael Johnson C++ 58 3
James Folkner CSS 77 1
Cindy Brown HTML 93 1
Alison Cremette HTML 70 2
Cindy Brown Java 92 1
Julia Barklay Java 86 2
Michael Johnson Java 83 3
John Clifford Java 75 4
Cindy Brown Java 74 5
Julia Barklay JavaScript 100 1
Molly James JavaScript 70 2
John Clifford JavaScript 65 3
Michael Johnson Pearl 94 1
Julia Barklay Pearl 94 1
James Folkner Pearl 93 2
Michael Holden Pearl 57 3
Holly Michaels Pearl 55 4
Ryan Brown Pearl 55 4
Cindy Brown Pearl 54 5
Michael Holden PHP 55 1
Ryan Brown Python 81 1
Alison Cremette Unix 63 1
James Folkner Visual Basic 98 1
Ryan Brown Visual Basic 85 2
Cindy Brown Visual Basic 80 3
Michael Holden Visual Basic 62 4
Alison Cremette Visual Basic 59 5
Holly Michaels Visual Basic 56 6
Molly James Visual Basic 52 7
These examples demonstrate the usage of RANK(), DENSE_RANK(), and
ROW_NUMBER() with your students and student_course tables. Changing the
partitioning and ordering criteria allows you to adapt these queries to suit your
specific analysis needs.
Finding the highest score achieved by each student
SELECT
s.studentid,
s.firstname,
s.lastname,
sc.score,
MAX(sc.score) OVER (PARTITION BY s.studentid) AS highest_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
ORDER BY
s.studentid;
Output 70:
studentid firstname lastname score highest_score
3 Michael Johnson 83 94
3 Michael Johnson 58 94
3 Michael Johnson 94 94
4 Molly James 76 76
4 Molly James 52 76
4 Molly James 69 76
4 Molly James 70 76
5 Holly Michaels 56 56
5 Holly Michaels 55 56
6 Cindy Brown 80 93
6 Cindy Brown 74 93
6 Cindy Brown 92 93
6 Cindy Brown 93 93
6 Cindy Brown 54 93
7 Julia Barklay 86 100
7 Julia Barklay 94 100
7 Julia Barklay 100 100
8 Alison Cremette 59 70
8 Alison Cremette 63 70
8 Alison Cremette 70 70
9 James Folkner 77 98
9 James Folkner 98 98
9 James Folkner 93 98
10 Michael Holden 57 62
10 Michael Holden 55 62
10 Michael Holden 62 62
11 Ryan Brown 81 85
11 Ryan Brown 55 85
11 Ryan Brown 85 85
12 John Clifford 65 75
12 John Clifford 75 75
13 Ann Williams 100 100
This query calculates the highest score achieved by each student across all their
courses.
Output 71:
studentid firstname lastname score lowest_score
3 Michael Johnson 83 58
3 Michael Johnson 58 58
3 Michael Johnson 94 58
4 Molly James 76 52
4 Molly James 52 52
4 Molly James 69 52
4 Molly James 70 52
5 Holly Michaels 56 55
5 Holly Michaels 55 55
6 Cindy Brown 80 54
6 Cindy Brown 74 54
6 Cindy Brown 92 54
6 Cindy Brown 93 54
6 Cindy Brown 54 54
7 Julia Barklay 86 86
7 Julia Barklay 94 86
7 Julia Barklay 100 86
8 Alison Cremette 59 59
8 Alison Cremette 63 59
8 Alison Cremette 70 59
9 James Folkner 77 77
9 James Folkner 98 77
9 James Folkner 93 77
10 Michael Holden 57 55
10 Michael Holden 55 55
10 Michael Holden 62 55
11 Ryan Brown 81 55
11 Ryan Brown 55 55
11 Ryan Brown 85 55
12 John Clifford 65 65
12 John Clifford 75 65
13 Ann Williams 100 100
This query calculates the lowest score received by each student across all their
courses.
Identifying Students with Scores Above the Class Average in Multiple Courses
SELECT
s.studentid,
s.firstname,
s.lastname,
sc.courseid,
sc.score,
AVG(sc.score) OVER (PARTITION BY sc.courseid) AS class_avg
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
sc.score > class_avg
GROUP BY
s.studentid;
SELECT
s.studentid,
s.firstname,
s.lastname,
sc.courseid,
sc.score,
AVG(sc.score) OVER (PARTITION BY sc.courseid) AS class_avg
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
sc.score > (SELECT AVG(score) FROM student_course WHERE courseid =
sc.courseid)
GROUP BY
s.studentid,
s.firstname,
s.lastname,
sc.courseid,
sc.score;
Output 72:
studentid firstname lastname courseid score class_avg
9 James Folkner 1 98 87.6667
6 Cindy Brown 1 80 87.6667
11 Ryan Brown 1 85 87.6667
7 Julia Barklay 2 86 89.0000
6 Cindy Brown 2 92 89.0000
4 Molly James 3 69 69.0000
6 Cindy Brown 5 93 93.0000
3 Michael Johnson 6 94 93.6667
7 Julia Barklay 6 94 93.6667
9 James Folkner 6 93 93.6667
7 Julia Barklay 9 100 100.0000
Explanation:
The AVG(sc.score) OVER (PARTITION BY sc.courseid) AS class_avg
calculates the average score for each courseid and assigns it to the alias
class_avg. However, window functions cannot be directly used in the WHERE
clause.
We use a subquery within the WHERE clause to achieve the desired filtering.
This subquery is (SELECT AVG(score) FROM student_course WHERE
courseid = sc.courseid).
The subquery calculates the average score for the specific courseid of the
current row.
The WHERE clause compares the student's score with the average score
obtained from the subquery and includes only students whose score in the
course is greater than the average score for that course.
The GROUP BY clause includes the sc.score to ensure correct grouping when
selecting individual student scores and the calculated class average.
This corrected query successfully identifies students whose scores exceed the
class average for their respective courses.
Let’s find students with scores below the class average for their respective
courses. It is easy to do. Just change the greater than sign to less than.
SELECT
s.studentid,
s.firstname,
s.lastname,
sc.courseid,
sc.score,
AVG(sc.score) OVER (PARTITION BY sc.courseid) AS class_avg
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
WHERE
sc.score < (SELECT AVG(score) FROM student_course WHERE courseid =
sc.courseid)
GROUP BY
s.studentid,
s.firstname,
s.lastname,
sc.courseid,
sc.score;
Output 73:
firstname lastname courseid score class_avg
studentid
8 Alison Cremette 1 59 57.6667
4 Molly James 1 52 57.6667
10 Michael Holden 1 62 57.6667
6 Cindy Brown 2 74 78.5000
3 Michael Johnson 2 83 78.5000
3 Michael Johnson 3 58 58.0000
8 Alison Cremette 5 70 70.0000
10 Michael Holden 6 57 55.2500
6 Cindy Brown 6 54 55.2500
5 Holly Michaels 6 55 55.2500
11 Ryan Brown 6 55 55.2500
12 John Clifford 9 65 67.5000
4 Molly James 9 70 67.5000
Output 74:
Output 75:
The next query assigns student letter scores (A, B, C, D, F) based on their
numerical scores.
SELECT
s.firstname,
s.lastname,
sc.score,
CASE
WHEN sc.score >= 90 THEN 'A'
WHEN sc.score >= 80 THEN 'B'
WHEN sc.score >= 70 THEN 'C'
WHEN sc.score >= 60 THEN 'D'
ELSE 'F'
END AS grade
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid;
Output 76:
firstname lastname score grade
John Clifford 65 D
Molly James 76 C
James Folkner 77 C
Michael Holden 57 F
Michael Holden 55 F
Julia Barklay 86 B
James Folkner 98 A
Ryan Brown 81 B
Alison Cremette 59 F
Holly Michaels 56 F
Molly James 52 F
Michael Holden 62 D
Cindy Brown 80 B
Alison Cremette 63 D
Cindy Brown 74 C
Michael Johnson 83 B
Cindy Brown 92 A
Michael Johnson 58 F
Molly James 69 D
Alison Cremette 70 C
Cindy Brown 93 A
Cindy Brown 54 F
Michael Johnson 94 A
Holly Michaels 55 F
Julia Barklay 94 A
Ryan Brown 55 F
James Folkner 93 A
Julia Barklay 100 A
Molly James 70 C
Ryan Brown 85 B
John Clifford 75 C
Ann Williams 100 A
Let’s build a query that calculates the difference between the maximum scores
of the morning and afternoon sessions for the same course.
SELECT
s.firstname,
s.lastname,
c.coursename as course,
MAX(CASE WHEN sh.starttime BETWEEN '08:00:00' AND '12:00:00' THEN
sc.score END) AS AM_Score,
MAX(CASE WHEN sh.starttime BETWEEN '12:00:00' AND '17:00:00' THEN
sc.score END) AS PM_Score,
MAX(CASE WHEN sh.starttime BETWEEN '08:00:00' AND '12:00:00'
THEN sc.score END) -
MAX(CASE WHEN sh.starttime BETWEEN '12:00:00' AND '17:00:00'
THEN sc.score END) AS difference
FROM
students s
INNER JOIN student_course sc ON s.studentid = sc.studentid
INNER JOIN courses c ON sc.courseid = c.courseid
INNER JOIN schedule sh ON c.courseid = sh.courseid
GROUP BY
s.firstname, s.lastname, c.coursename
HAVING COUNT(*) > 1;
Output 77:
Output 78:
teacherid firstname lastname rate new_rate
8 Greg Gerald 30 33.00
2 John Jefferson 51 58.65
5 Julia Williams 51 56.10
4 Michael Murphy 61 67.10
10 Michael Kelly 25 25
9 Michael Ross 20 22.00
6 John Niven 20 23.00
3 James Barry 51 51
1 John Smith 62 62
Explanation:
The SELECT clause retrieves the teacher's ID, first name, and last name.
CASE statement calculates a teacher's new rate based on the average score of
the students taught by each teacher.
The teacher rate increased by 30% if the average student score is 90 or more.
The teacher rate is increased by 15% if the average student score is between 80
and 90, and the rate is increased by 10% if the average student score is between
70 and 80.
If the score is less than 70, the teacher remains the same.
STORED PROCEDURES
Get Student Grades for a Specific Course.
DELIMITER $$
CREATE PROCEDURE GetStudentGradesForCourse(IN p_courseid INT)
BEGIN
SELECT
s.studentid,
s.firstname,
s.lastname,
sc.score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
c.courseid = p_courseid;
END $$
DELIMITER ;
Drop GetStudentGradesForCourse;
To create a stored procedure for MySQL, copy and paste its beginning and end
parts and put the desired query between them.
DELIMITER $$
CREATE PROCEDURE stored_procedure_name( parameters)
BEGIN
<---------- put your query here----------------------------------- >
END $$
DELIMITER ;
You can use different characters as the delimiter in MySQL. The delimiter
defines the end of a statement, especially when working with stored procedures,
triggers, or other complex SQL scripts. By default, the delimiter is a semicolon
(;), but when you're writing multi-statement scripts, changing the delimiter is
common to avoid conflicts with the semicolons used within the statements.
For example, you can use // or $$ or any other character string as the delimiter.
Let’s take this query and create a stored procedure from it.
SELECT
s.firstname AS st_lastname,
s.lastname AS st_firstname,
t.firstname AS t_firstname,
t.lastname AS t_lastname,
c.coursename
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
JOIN
schedule sch ON c.courseid = sch.courseid
JOIN
teachers t ON sch.teacherid = t.teacherid
WHERE
c.coursename = 'C++'
AND sch.starttime >= '00:00:00'
AND sch.starttime < '12:00:00';
Output 80:
st_lastname st_firstname t_firstname t_lastname coursename
Michael Johnson John Smith C++
Molly James John Smith C++
Let’s name this procedure GetMorningStudentsByCourse()
This procedure required one parameter coursename.
DELIMITER $$
Call GetMorningStudentsByCourse('C++');
Output 81:
st_lastname st_firstname t_firstname t_lastname coursename
Michael Johnson John Smith C++
Molly James John Smith C++
Call GetMorningStudentsByCourse('PHP');
Output 82:
st_lastname st_firstname t_firstname t_lastname coursename
Michael Holden Michael Kelly PHP
Let’s create a stored procedure that gets teachers and students for any course
regardless of the schedule.
DELIMITER $$
CREATE PROCEDURE GetStudentsTeachersByCourse(IN p_course_name
VARCHAR(50))
BEGIN
SELECT
s.firstname AS st_lastname,
s.lastname AS st_firstname,
t.firstname AS t_firstname,
t.lastname AS t_lastname,
c.coursename AS course
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
JOIN
schedule sch ON c.courseid = sch.courseid
JOIN
teachers t ON sch.teacherid = t.teacherid
WHERE
c.coursename = p_course_name;
END $$
DELIMITER ;
Call GetStudentsTeachersByCourse('C++');
Output 83:
Output 84:
st_lastname st_firstname t_firstname t_lastname course
Julia Barklay John Jefferson Java
Cindy Brown John Jefferson Java
Michael Johnson John Jefferson Java
Cindy Brown John Jefferson Java
John Clifford John Jefferson Java
John Clifford John Jefferson Java
Let’s add an end-time parameter to our procedure.
DELIMITER $$
DELIMITER ;
Output 85:
st_lastname st_firstname t_firstname t_lastname course
Alison Cremette Michael Murphy HTML
Cindy Brown Michael Murphy HTML
Create a stored procedure that finds students with scores above the class
average.
DELIMITER $$
CREATE PROCEDURE studentsAboveAverage()
BEGIN
SELECT
s.studentid,
s.firstname,
s.lastname,
c.coursename,
MIN(sc.score) AS sample_score
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
JOIN (
SELECT
sc.courseid,
AVG(sc.score) AS class_average
FROM
student_course sc
GROUP BY
sc.courseid
WHERE
sc.score > avg_grades.class_average
GROUP BY
s.studentid,
s.firstname,
s.lastname,
c.coursename;
END $$
DELIMITER ;
Call studentsAboveAverage();
Output 86:
studentid firstname lastname coursename sample_score
7 Julia Barklay Java 86
9 James Folkner Visual Basic 98
6 Cindy Brown Visual Basic 80
6 Cindy Brown Java 92
4 Molly James C++ 69
6 Cindy Brown HTML 93
3 Michael Johnson Pearl 94
7 Julia Barklay Pearl 94
9 James Folkner Pearl 93
7 Julia Barklay JavaScript 100
11 Ryan Brown Visual Basic 85
Let's create a stored procedure to display morning courses.
DELIMITER $$
SELECT
c.coursename,
c.hours,
c.cost,
s.starttime
FROM
courses c
JOIN
schedule s ON c.courseid = s.courseid
WHERE
TIME(s.starttime) >= '08:00:00'
AND TIME(s.starttime) <= '12:00:00';
END $$
DELIMITER ;
Call GetMorningCourses();
Output 87:
coursename hours cost starttime
C++ 550 3999.95 9:00:00
HTML 200 699.95 8:00:00
CSS 400 899.95 12:00:00
Pearl 300 1699.95 8:00:00
Assembly 400 1699.95 8:00:00
Visual Basic 360 1999.95 9:00:00
Assembly 400 1999.95 12:00:00
Java 500 2999.95 9:00:00
Python 300 999.95 9:00:00
PHP 300 999.95 9:00:00
JavaScript 200 999.95 8:00:00
Find a student with the highest score in a course.
DELIMITER $$
DELIMITER ;
call StudentWithHighestScore('Java');
Output 88a:
call StudentWithHighestScore('CSS');
Output 88b:
firstname lastname coursename score
James Folkner CSS 77
call StudentWithHighestScore('JavaScript');
Output 88c:
firstname lastname coursename score
Julia Barklay JavaScript 100
Find a student's average score.
DELIMITER $$
DELIMITER ;
Call StudentAvgScore('Barklay');
Output 89a:
firstname lastname average_score
Julia Barklay 93.3333
Call StudentAvgScore('Holden');
Output 89b:
firstname lastname average_score
Michael Holden 58.0000
Call StudentAvgScore('Johnson');
Output 89c:
firstname lastname average_score
Michael Johnson 78.3333
Create a procedure for a teacher schedule.
DELIMITER $$
SELECT
t.firstname,
t.lastname,
s.starttime,
s.endtime,
c.coursename
FROM
schedule s
JOIN
courses c ON s.courseid = c.courseid
JOIN
teachers t ON s.teacherid = t.teacherid
WHERE
t.lastname = p_lname;
END $$
DELIMITER ;
Call getTeacherSchedule('Jefferson');
Output 90a:
firstname lastname starttime endtime coursename
John Jefferson 09:00:00 15:00:00 Java
John Jefferson 18:00:00 22:00:00 C++
John Jefferson 18:00:00 22:00:00 Java
John Jefferson 14:00:00 18:00:00 Java
Call getTeacherSchedule('Kelly');
Output 90b:
firstname lastname starttime endtime coursename
Michael Kelly 09:00:00 12:00:00 PHP
Create the procedure for a student schedule.
DELIMITER $$
SELECT
s.firstname,
s.lastname,
c.coursename,
sch.starttime,
sch.endtime
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
JOIN
schedule sch ON c.courseid = sch.courseid
WHERE
s.lastname = p_lname;
END $$
DELIMITER ;
Call getStudentSchedule('Holden');
Output 91:
firstname lastname coursename starttime endtime
Michael Holden Visual Basic 18:00:00 21:00:00
Michael Holden Pearl 08:00:00 11:00:00
Michael Holden PHP 09:00:00 12:00:00
STORED FUNCTIONS
MySQL functions have a slightly different structure compared to stored
procedures. Specifically, you need to declare the variable before the BEGIN
block.
Let’s create a function that accepts a text string and returns unique characters
from the string.
DELIMITER $$
RETURN unique_chars;
END $$
DELIMITER ;
The DETERMINISTIC keyword is used to specify that the function will return
the same result if the input values are the same.
Variables are declared inside the BEGIN block.
You can call a function within a query.
SELECT f_getUniqueCharacters(lastname) as unique_chars from teachers where
lastname='Jefferson';
output 91a:
Unique_chars
Jefrson
SELECT f_getUniqueCharacters(lastname) as unique_chars from teachers where
lastname='Williams';
Output 91b:
unique_chars
Wilams
Let’s take a query that converts scores to grades and create a function.
Here is the query:
SELECT
s.firstname,
s.lastname,
sc.score,
CASE
WHEN sc.score >= 90 THEN 'A'
WHEN sc.score >= 80 THEN 'B'
WHEN sc.score >= 70 THEN 'C'
WHEN sc.score >= 60 THEN 'D'
ELSE 'F'
END AS grade
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid;
Below is the function. It accepts a student's score and converts it to the grade.
It is important to understand that a function can return only one value.
It means that the query inside the function must return only one row.
DELIMITER $$
RETURN grade;
END $$
DELIMITER ;
Output 92b:
Grade
A
SELECT getGrade(score) as Grade FROM student_course where courseid=13 and
studentid=13;
Output 92c:
Grade
A
SELECT getGrade(score) as Grade FROM student_course where courseid=7
and studentid=9;
Output 92d:
Grade
C
Let’s create a function that takes a date of birth and returns the age.
DELIMITER $$
RETURN age;
END $$
DELIMITER ;
Output 94b:
getAge(dob)
25
23
The TIMESTAMPDIFF function in SQL calculates the difference between two
timestamps. It returns the difference as an integer value based on a specified
unit of time. This function is commonly used to measure the duration between
two dates or times.
TIMESTAMPDIFF(unit, datetime_expr1, datetime_expr2)
You can use SECOND, MINUTE, HOUR, DAY, WEEK, MONTH,
QUARTER, and YEAR for the function unit parameter.
You can modify the getAge function to return age in days.
DELIMITER $$
RETURN age;
END $$
DELIMITER ;
DELIMITER //
RETURN v_formatted_date;
END //
DELIMITER ;
SELECT format_mysql_date('2025-01-31');
Output 95a:
format_mysql_date('2025-01-31')
01/31/25
SELECT dob, format_mysql_date(dob) as hdob from students where
lastname='James';
Output 95b:
Output 95c:
firstname lastname dob
Julia Barklay 01/25/01
The DATE_FORMAT function in SQL formats a date value according to a
specified format string.
This function formats a date value based on a specified format string.
The p_date is the date value you want to format.
The '%m/%d/%y' is the format string that specifies how the date should be
formatted.
The format string consists of format specifiers:
The %m stays for the month as a zero-padded number (01-12).
The %d stays for days of the month as a zero-padded number (01-31).
The %y stays for a year as a two-digit number (00-99).
Let’s create a function that converts a “human” date to a MySQL date.
DELIMITER //
RETURN v_mysql_date;
END //
DELIMITER ;
MySQL_Date
NULL
SELECT convert_human_to_mysql_date('01/31/25') as MySQL_Date;
Output 96b:
MySQL_Date
2025-01-31
Let’s try to use an empty string for the date value:
SELECT convert_human_to_mysql_date('') as MySQL_Date;
ERROR 1411 (HY000): Incorrect datetime value: '' for function str_to_date
We can modify our function to handle this error.
DELIMITER //
RETURN v_mysql_date;
END //
DELIMITER ;
Now we get:
SELECT convert_human_to_mysql_datemod('') as
MySQL_Date;
Output 96c:
MySQL_Date
NULL
Let’s create a function that calculates the average score for a course. This
function inputs the course name and returns the average student score.
DELIMITER //
RETURN v_average_score;
END //
DELIMITER ;
RETURN v_average_score;
END //
DELIMITER ;
63.50
SELECT average_student_score('C++', 2) as avg_score;
Output 97b:
avg_score
100.00
HOMEWORK
Please write queries to select the following data.
1. Find the number of students for each teacher.
JavaScript 78
Java 83
Assembly 76
CSS 77
Pearl 72
PHP 55
Visual Basic 73
Python 81
Unix 63
C++ 76
HTML 82
5. List all students who have taken a specific course (e.g., 'JavaScript')
coursename course_revenue
C++ 5999.9
2. Total amount spent by all students on a specific course (e.g., 'Visual Basic')
coursename total_student_spending
Visual Basic 13999.65
13. Average amount spent per student across all courses
coursename total_revenue
Java 17999.7
16. Find the student who has spent the most on courses
coursename avg_student_spending_per_course
Assembly 1699.95
C++ 2999.95
CSS 899.95
HTML 699.95
Java 3599.94
JavaScript 999.95
Pearl 1699.95
PHP 999.95
Python 999.95
Unix 1000
Visual Basic 1999.95
You can find solutions on page 135.
3. Create a Stored Procedure that finds the teacher who teaches the most
students
Call TeacherWithMostStudents();
firstname lastname num_of_students
Julia Williams 8
4. Create a Stored Procedure that lists all students who have taken a specific
course.
call getStudentsForCourses('C++');
5. Create a Stored Procedure that finds the teacher with the highest average
student score across all their courses
Call getTeacherWithHighestStudentScore ();
Call getCoursesCostPerStudent('Brown');
7. Create a Stored Procedure that calculates the total amount spent by a specific
student. (The input parameter is a student's lastname.)
Call getStudentAmountSpent('Brown');
8. Create a Stored Procedure that calculates the total amount all students spend
on a specific course.
Call getAmountSpentPerCourse('Java');
coursename total_student_spending
Java 14999.75
9.00
HOMEWORK SOLUTIONS
5. List all students who have taken a specific course (e.g., 'JavaScript')
SELECT
s.firstname,
s.lastname,
c.coursename
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
c.coursename = 'JavaScript';
SELECT
t.firstname,
t.lastname,
ROUND(AVG(sc.score),2)
FROM
teachers t
JOIN
schedule sch ON t.teacherid = sch.teacherid
JOIN
courses c ON sch.courseid = c.courseid
JOIN
student_course sc ON c.courseid = sc.courseid
GROUP BY
t.teacherid,
t.firstname,
t.lastname
ORDER BY
AVG(sc.score) DESC
LIMIT 1;
SELECT
s.lastname, s.firstname,
ROUND(SUM(c.cost),2) AS total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
s.studentid = 6
GROUP BY s.lastname, s.firstname;
10. Total amount spent by a specific student (e.g., student with ID)
SELECT
ROUND(SUM(c.cost),2) AS total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
s.studentid = 6
AND sc.paid = 1;
12. Total amount spent by all students on a specific course (e.g., 'Visual
Basic')
SELECT
c.coursename,
ROUND(SUM(c.cost),2) AS total_student_spending
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
c.coursename = 'Visual Basic'
GROUP BY c.coursename;
SELECT
s.lastname,
s.firstname,
ROUND(AVG(c.cost),2) AS avg_student_spending
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
GROUP BY
s.lastname,
s.firstname
ORDER BY s.lastname,
s.firstname;
14. Total commission for all teachers (Assuming a 10% commission for teachers
)
SELECT
ROUND(SUM(c.cost * 0.1),2) AS total_teacher_earnings
FROM
teachers t
JOIN
schedule sch ON t.teacherid = sch.teacherid
JOIN
courses c ON sch.courseid = c.courseid
JOIN
student_course sc ON c.courseid = sc.courseid;
-- Assuming a 10% commission for teachers
16. Find the student who has spent the most on courses.
SELECT
s.studentid,
s.firstname,
s.lastname,
ROUND(SUM(c.cost),2) AS total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE sc.paid=1
GROUP BY s.firstname,
s.lastname, s.studentid
ORDER BY total_spent DESC
LIMIT 1;
SELECT
c.coursename,
c.cost
FROM
courses c
WHERE
c.cost = (SELECT MIN(cost) FROM courses);
20. Find the total revenue generated by each teacher (Assuming a 10%
commission for teachers).
SELECT
t.firstname,
t.lastname,
ROUND(SUM(c.cost * 0.1), 2) AS teacher_revenue
FROM
teachers t
JOIN
schedule sch ON t.teacherid = sch.teacherid
JOIN
courses c ON sch.courseid = c.courseid
JOIN
student_course sc ON c.courseid = sc.courseid
GROUP BY
t.teacherid,
t.firstname,
t.lastname;
-- Assuming a 10% commission for teachers
1. Create a Stored procedure that finds the number of students for each teacher.
DELIMITER $$
SELECT
t.firstname,
t.lastname,
COUNT(DISTINCT sc.studentid) as num_of_students
FROM
teachers t
JOIN
schedule sch ON t.teacherid = sch.teacherid
JOIN
courses c ON sch.courseid = c.courseid
JOIN
student_course sc ON c.courseid = sc.courseid
GROUP BY
t.teacherid,
t.firstname,
t.lastname
ORDER BY
COUNT(DISTINCT sc.studentid) DESC;
END $$
DELIMITER ;
Call GetStudentNumber();
2. Create a Stored procedure that lists all courses taught by a specific teacher.
DELIMITER $$
SELECT
c.coursename,
t.lastname,
t.firstname
FROM
courses c
JOIN
schedule sch ON c.courseid = sch.courseid
JOIN
teachers t ON sch.teacherid = t.teacherid
WHERE
t.lastname = p_lname;
END $$
DELIMITER ;
Call getTeacherCourses('Jefferson');
coursename lastname firstname
Java Jefferson John
C++ Jefferson John
Java Jefferson John
Java Jefferson John
3. Create a Stored procedure that finds the teacher who teaches the most
students
DELIMITER $$
SELECT
t.firstname,
t.lastname,
COUNT(DISTINCT sc.studentid) as num_of_students
FROM
teachers t
JOIN
schedule sch ON t.teacherid = sch.teacherid
JOIN
courses c ON sch.courseid = c.courseid
JOIN
student_course sc ON c.courseid = sc.courseid
GROUP BY
t.teacherid,
t.firstname,
t.lastname
ORDER BY
COUNT(DISTINCT sc.studentid) DESC
LIMIT 1;
END $$
DELIMITER ;
Call TeacherWithMostStudents();
firstname lastname num_of_students
Julia Williams 8
4. Create a Stored procedure that lists all students who have taken a specific
course.
DELIMITER $$
SELECT
s.firstname,
s.lastname,
c.coursename
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
c.coursename = p_course;
END $$
DELIMITER ;
call getStudentsForCourses('C++');
SELECT
t.firstname,
t.lastname,
ROUND(AVG(sc.score), 2) AS highest_score
FROM
teachers t
JOIN
schedule sch ON t.teacherid = sch.teacherid
JOIN
courses c ON sch.courseid = c.courseid
JOIN
student_course sc ON c.courseid = sc.courseid
GROUP BY
t.firstname,
t.lastname
ORDER BY
AVG(sc.score) DESC
LIMIT 1;
END $$
DELIMITER;
SELECT
s.firstname,
s.lastname,
ROUND(SUM(c.cost), 2) AS total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
s.lastname=p_lastname
GROUP BY s.firstname, s.lastname;
END $$
DELIMITER;
Call getCoursesCostPerStudent('Brown');
SELECT
s.firstname,
s.lastname,
ROUND(SUM(c.cost),2) AS total_spent
FROM
students s
JOIN
student_course sc ON s.studentid = sc.studentid
JOIN
courses c ON sc.courseid = c.courseid
WHERE
s.lastname = p_lastname
AND sc.paid = 1
GROUP BY s.firstname, s.lastname;
END $$
DELIMITER;
Call getStudentAmountSpent('Brown');
END $$
DELIMITER ;
Call getAmountSpentPerCourse('Java');
coursename total_student_spending
Java 14999.75
9. Create a Stored procedure that finds the least expensive course.
DELIMITER $$
END $$
DELIMITER ;
Call getLeastExpensiveCourse();
coursename cost
HTML 699.95
HTML 699.95
10. Create a Stored procedure that finds the average cost of all courses
DELIMITER $$
CREATE PROCEDURE getAvgCoursesCost()
BEGIN
SELECT
ROUND(AVG(cost),2) AS avg_course_cost
FROM
courses;
END $$
DELIMITER;
Call getAvgCoursesCost();
avg_course_cost
1733.29
DELIMITER ;
lastname name_length
Ross 4
Smith 5
Barry 5
Niven 5
Merry 5
Kelly 5
Karon 5
Murphy 6
Gerald 6
Williams 8
Jefferson 9
Creating a get_string_length() function is unnecessary. The LENGTH() SQL
function returns the length of a string.
SELECT lastname, LENGTH(lastname) as name_length FROM Teachers order by
name_length;
lastname name_length
Ross 4
Smith 5
Barry 5
Niven 5
Merry 5
Kelly 5
Karon 5
Murphy 6
Gerald 6
Williams 8
Jefferson 9
2. Create a function that calculates the average cost of all courses.
DELIMITER //
RETURN v_avg_cost;
END //
DELIMITER ;
SELECT get_avg_courses_cost();
get_avg_courses_cost()
1733.29
SELECT get_avg_courses_cost() as Avg_Courses_Cost;
Avg_Courses_Cost
1733.29
3. Create a function that calculates the total amount spent by a specific student.
(The input parameter is a student's lastname.)
DELIMITER //
-- Handle the case where the student is not found or has not spent anything
IF v_total_spent IS NULL THEN
SET v_total_spent = 0.00; -- Or you could return NULL if you prefer
END IF;
RETURN v_total_spent;
END //
DELIMITER ;
SELECT get_student_amount_spent('Brown');
get_student_amount_spent('Brown')
11699.70
SELECT get_student_amount_spent('Brown') as
student_amount_spent;
student_amount_spent
11699.70
4. Create a function that calculates the total cost of all courses.
DELIMITER //
RETURN v_total_cost;
END //
DELIMITER ;
SELECT get_total_courses_cost();
get_total_courses_cost()
41598.85
SELECT get_total_courses_cost() as total_courses_cost;
total_courses_cost
41598.85
5. Create a function that returns the number of students in a specific course.
DELIMITER //
SELECT
COUNT(DISTINCT sc.studentid) INTO v_student_count
FROM
student_course sc
JOIN
courses c ON sc.courseid = c.courseid
WHERE
c.coursename = p_coursename;
RETURN v_student_count;
END //
DELIMITER ;
SELECT get_number_of_students_in_course('C++');
get_number_of_students_in_course('C++')
3
SELECT get_number_of_students_in_course('C++')
as student_number;
student_number
3
6. Create a function that returns the average student score across all courses.
DELIMITER //
RETURN v_avg_score;
END //
DELIMITER ;
RETURN v_course_cost;
END //
DELIMITER;
RETURN v_teacher_name;
END //
DELIMITER ;
SELECT get_teacher_with_highest_student_score();
get_teacher_with_highest_student_score()
John Jefferson
9. Create a function that finds the teacher with the lowest average student score
across all their courses
DELIMITER //
DELIMITER ;
SELECT get_teacher_with_lowest_student_score();
get_teacher_with_lowest_student_score()
Michael Kelly
RETURN v_difference;
END //
DELIMITER ;
SELECT get_average_score_difference('C++') as moning_afternoon_diff;
moning_afternoon_diff
36.50
SELECT get_average_score_difference('Java')
as moning_afternoon_diff;
moning_afternoon_diff
9.00
Let’s remove the ABS function that displays a negative value as a positive.
DELIMITER //
RETURN v_difference;
END //
DELIMITER ;
SELECT get_average_score_diff('C++')
as moning_afternoon_diff;
moning_afternoon_diff
-36.50
SELECT get_average_score_diff('Java')
as moning_afternoon_diff;
moning_afternoon_diff
9.00
Resources
You can practice your SQL skills on this webpage. It’s linked to a
MySQL database that includes all the necessary tables:
https://www.learn-coding.today/continue_learn_sql.php
You can download a text file containing the CREATE TABLE statements and
data for each table used in this book from the following link:
https://www.learn-coding.today/continue_sql/tables.txt
Thank you for reading! If you found this book helpful, I would
greatly appreciate it if you could leave a brief review on Amazon.
Your support truly makes a difference and helps me improve this
book further.
Would you be willing to share how this book has benefited you?
Your feedback will assist other readers in gaining from this work.
If you'd rather not, that's perfectly fine. Thank you once again for
your support!