Development

SQL Practice Questions with Solutions for Beginners

Nine hands-on SQL exercises on a small student database, from filtering and grouping to joins and safe updates, each with a worked solution and the reasoning behind it.

SmartCampus Buddy TeamSeptember 26, 202610 min read

The fastest way to learn SQL is to write queries and check the results. This set of nine exercises uses a tiny student database. Try each question yourself before reading the solution. The queries use standard SQL that works in MySQL and PostgreSQL; where a dialect differs, the text says so.

The tables

  • students(id, name, city, joined_on)
  • courses(id, title, credits)
  • enrollments(student_id, course_id, score) where each row is one student taking one course

Exercise 1: Filter and sort

List the names of students from Pune in alphabetical order.

SELECT name
FROM students
WHERE city = 'Pune'
ORDER BY name;

WHERE filters rows, and ORDER BY sorts the result. Without ORDER BY, the order is not guaranteed.

Exercise 2: The top three scores

Show the three highest scores.

SELECT student_id, course_id, score
FROM enrollments
ORDER BY score DESC
LIMIT 3;

LIMIT works in MySQL, PostgreSQL and SQLite. SQL Server uses SELECT TOP 3 ... instead.

Exercise 3: Average per group

Find the average score for each course.

SELECT course_id, AVG(score) AS avg_score
FROM enrollments
GROUP BY course_id;

Every selected column must either be in GROUP BY or be inside an aggregate function.

Exercise 4: Filter groups

Show only the courses whose average score is above 70.

SELECT course_id, AVG(score) AS avg_score
FROM enrollments
GROUP BY course_id
HAVING AVG(score) > 70;

HAVING filters after grouping, so it can use aggregates. WHERE cannot.

Exercise 5: Join two tables

List each student's name with the titles of the courses they are enrolled in.

SELECT s.name, c.title
FROM enrollments e
JOIN students s ON s.id = e.student_id
JOIN courses c ON c.id = e.course_id;

An inner join keeps only rows that match on both sides.

Exercise 6: Find what is missing

Find students who are not enrolled in any course.

SELECT s.name
FROM students s
LEFT JOIN enrollments e ON e.student_id = s.id
WHERE e.student_id IS NULL;

The left join keeps every student, and the ones with no matching enrollment have NULL in the enrollment columns. Filtering on NULL leaves exactly those students. NOT EXISTS is an equivalent alternative.

Exercise 7: Count distinct values

How many different students are enrolled in each course?

SELECT course_id, COUNT(DISTINCT student_id) AS students
FROM enrollments
GROUP BY course_id;

COUNT(DISTINCT ...) counts unique values, which matters if a student could appear more than once.

Exercise 8: Compare with a group average

Find the students whose score in a course is above that course's average.

SELECT s.name, e.course_id, e.score
FROM enrollments e
JOIN students s ON s.id = e.student_id
WHERE e.score > (
  SELECT AVG(e2.score)
  FROM enrollments e2
  WHERE e2.course_id = e.course_id
);

The subquery is correlated: it runs in the context of each outer row, using that row's course.

Exercise 9: Update safely

Give every student in course 3 a five-point bonus, capped at 100.

-- Preview first
SELECT * FROM enrollments WHERE course_id = 3;

UPDATE enrollments
SET score = LEAST(score + 5, 100)
WHERE course_id = 3;

Run the SELECT with the same WHERE clause before an UPDATE or DELETE, because forgetting the WHERE changes every row. LEAST exists in MySQL, PostgreSQL and recent SQL Server versions; otherwise use a CASE expression.

What to do next

Key takeaways

  • Filter rows with WHERE, filter groups with HAVING.
  • Use a left join plus an IS NULL check to find missing matches.
  • Correlated subqueries compare each row with a group.
  • Preview with SELECT before you UPDATE or DELETE.