DEV Community

Nelly Mogere
Nelly Mogere

Posted on

SQL Intermediate Project: Build a Greenwood Academy Database with PostgreSQL

Intermediate SQL Portfolio Project: Master Key Skills

If you already know basic SQL and want a project that feels more practical, this Greenwood Academy database is a good next step. It starts with simple tables, then moves into real database work: inserting data, updating records, filtering results, using operators, counting records, formatting dates, writing CASE WHEN logic, and using a subquery for a more advanced question.

This project is labelled as intermediate SQL because it goes beyond only creating tables and selecting rows. It shows how SQL can help organize school data and turn it into information that teachers, administrators, students, and technical teams can understand.

screenshot of the project files in VS Code

What this project is about

Greenwood Academy is a fictional school database. The database stores students, subjects, and exam results.

Instead of working with random examples, the project follows a story. A school needs to keep student details, know which subjects are taught, record exam marks, correct mistakes, remove cancelled results, and create simple reports.

The database has three main tables:

Table What it stores Example use
students Student names, gender, date of birth, class, and city Find Form 3 students from Nairobi
subjects Subject names, departments, teachers, and credit hours Find all Science subjects
exam_results Student results, marks, grades, and exam dates Find results above 70 marks

The main idea is simple: first create a clean structure, then add data, then ask useful questions.

1. Creating the schema

The project starts by creating a schema called greenwood_academy. A schema helps group related database objects together.

CREATE SCHEMA greenwood_academy;
SET search_path TO greenwood_academy;
Enter fullscreen mode Exit fullscreen mode

This keeps the school database objects organized in one place.

2. Creating the students table

The first table stores student information.

CREATE TABLE students (
    student_id SERIAL PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    gender VARCHAR(1),
    date_of_birth DATE,
    class VARCHAR(10),
    city VARCHAR(50)
);
Enter fullscreen mode Exit fullscreen mode

The most important column here is student_id. It is the primary key, which means every student gets a unique identifier.

For a non-technical reader, this is like giving every learner a school admission number. Even if two students have the same name, the database can still tell them apart.

3. Creating the subjects table

The second table stores the subjects offered by Greenwood Academy.

CREATE TABLE subjects (
    subject_id SERIAL PRIMARY KEY,
    subject_name VARCHAR(100) NOT NULL UNIQUE,
    department VARCHAR(50),
    teacher_name VARCHAR(100),
    credits INT
);
Enter fullscreen mode Exit fullscreen mode

The UNIQUE rule on subject_name prevents the same subject from being entered more than once with the exact same name.

4. Creating the exam results table

The third table connects students to subjects through exam performance.

CREATE TABLE exam_results (
    result_id SERIAL PRIMARY KEY,
    student_id INT NOT NULL,
    subject_id INT NOT NULL,
    marks INT NOT NULL,
    exam_date DATE,
    FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE,
    FOREIGN KEY (subject_id) REFERENCES subjects(subject_id) ON DELETE CASCADE
);
Enter fullscreen mode Exit fullscreen mode

This is where the project becomes relational. The exam_results table does not store the full student name or full subject details. It stores student_id and subject_id, then uses foreign keys to connect back to the correct records.

That means one student can have many exam results, and one subject can appear in many exam results.

5. Altering tables after creation

A database design often changes as the project grows. In this project, I added a phone number column, renamed a subject column, removed the phone number column, and added a grade column.

ALTER TABLE students ADD COLUMN phone_number VARCHAR(20);
ALTER TABLE subjects RENAME COLUMN credits TO credit_hours;
ALTER TABLE students DROP COLUMN phone_number;
ALTER TABLE exam_results ADD COLUMN grade VARCHAR(1);
Enter fullscreen mode Exit fullscreen mode

This part is important because real databases are not always designed perfectly on the first attempt. ALTER TABLE lets you adjust the structure while continuing to build.

6. Inserting student data

After creating the tables, I inserted sample students into the database.

INSERT INTO students (
    first_name,
    last_name,
    gender,
    date_of_birth,
    class,
    city
)
VALUES
    ('Amina', 'Wanjiku', 'F', '2008-03-12', 'Form 3', 'Nairobi'),
    ('Brian', 'Ochieng', 'M', '2007-07-25', 'Form 4', 'Mombasa'),
    ('Cynthia', 'Mutua', 'F', '2008-11-05', 'Form 3', 'Kisumu'),
    ('David', 'Kamau', 'M', '2007-02-18', 'Form 4', 'Nairobi');
Enter fullscreen mode Exit fullscreen mode

Sample output from the students table:

student_id first_name last_name class city
1 Amina Wanjiku Form 3 Nairobi
2 Brian Ochieng Form 4 Mombasa
3 Cynthia Mutua Form 3 Kisumu
4 David Kamau Form 4 Nairobi

7. Inserting subjects and exam results

The subjects table stores what the school teaches.

INSERT INTO subjects (
    subject_name,
    department,
    teacher_name,
    credit_hours
)
VALUES
    ('Mathematics', 'Sciences', 'Mr. Njoroge', 4),
    ('English', 'Languages', 'Ms. Adhiambo', 3),
    ('Biology', 'Sciences', 'Ms. Otieno', 4),
    ('History', 'Humanities', 'Mr. Waweru', 3);
Enter fullscreen mode Exit fullscreen mode

The exam results table then records marks for specific students and subjects.

INSERT INTO exam_results (
    student_id,
    subject_id,
    marks,
    exam_date,
    grade
)
VALUES
    (1, 1, 78, '2024-03-15', 'B'),
    (1, 2, 85, '2024-03-16', 'A'),
    (2, 1, 92, '2024-03-15', 'A'),
    (2, 3, 55, '2024-03-17', 'C');
Enter fullscreen mode Exit fullscreen mode

This is where the three tables start working together.

8. Updating and deleting records

In a real system, data changes. A student may move, a mark may be corrected, or an exam result may be cancelled.

-- Esther Akinyi moved from Nakuru to Nairobi
UPDATE students
SET city = 'Nairobi'
WHERE student_id = 5;

-- Correct marks for result_id 5 from 49 to 59
UPDATE exam_results
SET marks = 59
WHERE result_id = 5;

-- Remove a cancelled exam result
DELETE FROM exam_results
WHERE result_id = 9;
Enter fullscreen mode Exit fullscreen mode

This section shows that SQL is not only for reading data. It is also used to maintain accurate records.

9. Filtering students and subjects

Filtering is one of the most useful SQL skills. It allows you to ask focused questions.

-- Find all students in Form 4
SELECT *
FROM students
WHERE class = 'Form 4';
Enter fullscreen mode Exit fullscreen mode

Expected output:

first_name last_name class city
Brian Ochieng Form 4 Mombasa
David Kamau Form 4 Nairobi
Hassan Abdi Form 4 Mombasa

Another example finds all subjects in the Sciences department.

-- Find all subjects in the Sciences department
SELECT *
FROM subjects
WHERE department = 'Sciences';
Enter fullscreen mode Exit fullscreen mode

Expected output:

subject_name department teacher_name
Mathematics Sciences Mr. Njoroge
Biology Sciences Ms. Otieno
Physics Sciences Mr. Kamande
Chemistry Sciences Ms. Muthoni
Computer Studies Sciences Mr. Oduya

10. Combining conditions with AND and OR

SQL becomes more useful when conditions are combined.

-- Find Form 3 students from Nairobi
SELECT *
FROM students
WHERE class = 'Form 3'
  AND city = 'Nairobi';
Enter fullscreen mode Exit fullscreen mode

Expected output:

first_name last_name class city
Amina Wanjiku Form 3 Nairobi
Grace Mwangi Form 3 Nairobi
James Kariuki Form 3 Nairobi

The AND keyword means both conditions must be true.

The OR keyword is used when either condition can be true.

-- Find students who are in Form 2 or Form 4
SELECT *
FROM students
WHERE class = 'Form 2'
   OR class = 'Form 4';
Enter fullscreen mode Exit fullscreen mode

11. Using BETWEEN, IN, NOT IN, and LIKE

Intermediate SQL includes operators that make queries cleaner.

Operator Meaning Example use
BETWEEN Match values inside a range Marks from 50 to 80
IN Match any value in a list Students from Nairobi, Mombasa, or Kisumu
NOT IN Exclude values in a list Students not in Form 2 or Form 3
LIKE Match a text pattern Subjects containing Studies
-- Find exam results where marks are between 50 and 80
SELECT *
FROM exam_results
WHERE marks BETWEEN 50 AND 80;
Enter fullscreen mode Exit fullscreen mode
-- Find students who live in selected cities
SELECT *
FROM students
WHERE city IN ('Nairobi', 'Mombasa', 'Kisumu');
Enter fullscreen mode Exit fullscreen mode
-- Find subjects whose name contains Studies
SELECT *
FROM subjects
WHERE subject_name LIKE '%Studies%';
Enter fullscreen mode Exit fullscreen mode

These operators reduce repetition and make the question easier to read.

12. Working with string functions

String functions help format text without changing the original stored data.

-- Show city names in uppercase and lowercase
SELECT
    city,
    UPPER(city) AS upper_city,
    LOWER(city) AS lower_city
FROM students;
Enter fullscreen mode Exit fullscreen mode

Expected output:

city upper_city lower_city
Nairobi NAIROBI nairobi
Mombasa MOMBASA mombasa
Kisumu KISUMU kisumu

Another useful string function is CONCAT, which joins text values together.

-- Combine subject name and teacher name
SELECT CONCAT(subject_name, ' - ', teacher_name) AS subject_teacher
FROM subjects;
Enter fullscreen mode Exit fullscreen mode

Expected output:

subject_teacher
Mathematics - Mr. Njoroge
English - Ms. Adhiambo
Biology - Ms. Otieno

13. Working with dates

Dates are very important in school data because students have birth dates and exams have exam dates.

-- Extract year, month, and day from each student's date of birth
SELECT
    first_name,
    date_of_birth,
    EXTRACT(YEAR FROM date_of_birth) AS birth_year,
    EXTRACT(MONTH FROM date_of_birth) AS birth_month,
    EXTRACT(DAY FROM date_of_birth) AS birth_day
FROM students;
Enter fullscreen mode Exit fullscreen mode

Expected output:

first_name date_of_birth birth_year birth_month birth_day
Amina 2008-03-12 2008 3 12
Brian 2007-07-25 2007 7 25

The project also formats exam dates into a friendlier format.

-- Format each exam date as a friendly date
SELECT
    result_id,
    exam_date,
    TO_CHAR(exam_date, 'Day, DDth Month YYYY') AS friendly_date
FROM exam_results;
Enter fullscreen mode Exit fullscreen mode

14. Important gotcha: hidden spaces in TO_CHAR day names

This was one of the most important small lessons I noticed while working with dates.

When TO_CHAR(exam_date, 'Day') is used, PostgreSQL pads the day name with extra spaces so all day names line up with the longest day name, Wednesday. That means the output may look correct on the screen, but the value can contain hidden trailing spaces.

Format used What it looks like What may actually be returned
TO_CHAR(exam_date, 'Day') Saturday 'Saturday ' with hidden padding
TO_CHAR(exam_date, 'FMDay') Saturday 'Saturday' without padding

This matters because hidden spaces can break conditional logic. A value that looks like Saturday may not equal the text 'Saturday' if the database actually returned 'Saturday '.

-- This can fail because 'Day' may return padded text such as 'Saturday '
SELECT
    result_id,
    TO_CHAR(exam_date, 'Day') AS exam_day,
    CASE
        WHEN TO_CHAR(exam_date, 'Day') IN ('Saturday', 'Sunday') THEN 'Weekend'
        ELSE 'Weekday'
    END AS day_type
FROM exam_results;
Enter fullscreen mode Exit fullscreen mode

The safer version from this project uses FMDay. The FM modifier means fill mode, and it removes the blank padding from the formatted result.

-- Safer version: FMDay removes hidden padding from the day name
SELECT
    result_id,
    TO_CHAR(exam_date, 'FMDay') AS exam_day,
    CASE
        WHEN TO_CHAR(exam_date, 'FMDay') IN ('Saturday', 'Sunday') THEN 'Weekend'
        ELSE 'Weekday'
    END AS day_type
FROM exam_results;
Enter fullscreen mode Exit fullscreen mode

Another safe option is to wrap the formatted value with TRIM().

-- Alternative fix: TRIM removes leading and trailing spaces
SELECT
    result_id,
    TRIM(TO_CHAR(exam_date, 'Day')) AS exam_day,
    CASE
        WHEN TRIM(TO_CHAR(exam_date, 'Day')) IN ('Saturday', 'Sunday') THEN 'Weekend'
        ELSE 'Weekday'
    END AS day_type
FROM exam_results;
Enter fullscreen mode Exit fullscreen mode

The lesson is simple: when using TO_CHAR() for day names in conditions, use FMDay or TRIM(). Otherwise, the query may look correct but still classify weekends as weekdays because of invisible spaces.

Add a screenshot of one date function query result

Weekend or weekday labelling

15. Counting records

Counting helps turn raw records into summaries.

-- Count how many students are currently in Form 3
SELECT
    class,
    COUNT(student_id) AS total_students
FROM students
WHERE class = 'Form 3'
GROUP BY class;
Enter fullscreen mode Exit fullscreen mode

Expected output:

class total_students
Form 3 4

Another count checks exam results with marks of 70 or above.

-- Count exam results with marks of 70 or above
SELECT COUNT(result_id) AS total_results
FROM exam_results
WHERE marks >= 70;
Enter fullscreen mode Exit fullscreen mode

This is where the database starts to behave like a reporting tool.

16. Using CASE WHEN for readable labels

CASE WHEN is useful when raw values need to become meaningful labels.

-- Label each exam result with a grade description
SELECT
    result_id,
    marks,
    CASE
        WHEN marks >= 80 THEN 'Distinction'
        WHEN marks >= 60 THEN 'Merit'
        WHEN marks >= 40 THEN 'Pass'
        ELSE 'Fail'
    END AS result_grade
FROM exam_results;
Enter fullscreen mode Exit fullscreen mode

Expected output:

result_id marks result_grade
1 78 Merit
2 85 Distinction
3 92 Distinction
4 55 Pass
5 59 Pass

Case when output

17. Using CASE WHEN with class labels

The project also labels students as Senior or Junior based on their class.

-- Label students as Senior or Junior
SELECT
    first_name,
    last_name,
    class,
    CASE
        WHEN class IN ('Form 3', 'Form 4') THEN 'Senior'
        WHEN class IN ('Form 2', 'Form 1') THEN 'Junior'
    END AS class_label
FROM students;
Enter fullscreen mode Exit fullscreen mode

Expected output:

first_name last_name class class_label
Amina Wanjiku Form 3 Senior
Brian Ochieng Form 4 Senior
Esther Akinyi Form 2 Junior

This shows how SQL can add meaning to stored values.

18. Finding upcoming birthdays with a subquery

The most advanced query in this project finds students whose birthdays are coming up in the next 30 days.

The challenge is that date_of_birth includes the original year of birth. To compare birthdays with the current date, the query first calculates each student's birthday in the current year, then filters from that calculated result.

-- Find students whose birthday is coming up in the next 30 days
SELECT
    student_id,
    first_name,
    last_name,
    date_of_birth,
    birthday_this_year - CURRENT_DATE AS days_until_birthday
FROM (
    SELECT
        *,
        (
            date_of_birth
            + (EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM date_of_birth))
            * INTERVAL '1 year'
        )::date AS birthday_this_year
    FROM students
) sub
WHERE birthday_this_year BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '30 days';
Enter fullscreen mode Exit fullscreen mode

This is a strong SQL example because it solves a problem in two stages:

Stage What happens
Inner query Calculates each student's birthday in the current year
Outer query Filters only birthdays between today and the next 30 days

The lesson here is that SQL queries can be built step by step. When a question is too complex, create the missing calculated value first, then filter from it.

What I learned from this project

This project helped me see SQL as more than a language for writing commands. It is a way to organize a real-world situation into clear tables and useful questions.

I practiced:

  • Creating schemas and tables
  • Using primary keys and foreign keys
  • Inserting sample data
  • Updating and deleting records
  • Filtering with WHERE, AND, and OR
  • Using BETWEEN, IN, NOT IN, and LIKE
  • Working with string and date functions
  • Counting records with COUNT
  • Grouping records with GROUP BY
  • Creating labels with CASE WHEN
  • Solving a multi-step question with a subquery

Final thoughts

Greenwood Academy is a small database project, but it follows the same thinking used in larger systems. First, you decide what information matters. Then you store it in the right tables. After that, SQL helps you ask better questions and produce answers that people can use.

That is what makes this an intermediate SQL project. It is not only about syntax. It is about using SQL to turn raw data into structured, readable, and meaningful information.

Top comments (3)

Collapse
 
danson profile image
Danson Kuria

Good job NellyπŸ‘πŸΎ

Collapse
 
nelly_mogere_194bac0cb2ba profile image
Nelly Mogere

I appreciate it Danson

Collapse
 
sirphilip profile image
PHILIP KAPLONG

NICE WORK