Back to Projects

Gym Management Database System

A fully designed and implemented relational database system for managing members, subscriptions, bookings and staff operations. Built as part of a university group project, the system includes entity-relationship modelling, normalization, constraints, and SQL queries.


For one of my university group projects we built a fully working Gym Management System database using Oracle APEX. The goal was simple. Design a realistic system that could actually run a gym. Not just store names and numbers but handle memberships, bookings, personal training sessions, discounts, attendance and more.

Gym Management System Use Case Diagram We started with the design. Before writing a single line of SQL we identified all the strong and weak entities. Members, Staff, Membership Plans, Classes, Workouts, Class Bookings, Guests.

Then we built a full ERD and carefully defined the relationships between everything. Every foreign key had a purpose. Every table had a reason to exist. That part taught me how important structure is before implementation.

Gym Entity Relationship Diagram

After that we mapped the ERD into a fully normalised relational model. Everything was brought into Third Normal Form. No transitive dependencies. No attributes depending on non key columns. Just clean structure and clarity.

Then came the SQL. We wrote DDL scripts for over fifteen tables including Member, Staff, PT Session, Attendance, Discount Usage and more. Primary keys were defined properly. Foreign key constraints were added using alter table so referential integrity was enforced across the entire system.

CREATE TABLE Staff (
    StaffID NUMBER PRIMARY KEY,
    First_Name VARCHAR2(100),
    Last_Name VARCHAR2(100),
    DOB DATE,
    Gender VARCHAR2(10),
    Salary NUMBER,
    Phone_No VARCHAR2(20),
    Email VARCHAR2(100),
    Specialisation VARCHAR2(100)
);
CREATE TABLE Member (
    MemberID NUMBER PRIMARY KEY,
    First_Name VARCHAR2(100),
    Last_Name VARCHAR2(100),
    DOB DATE,
    Gender VARCHAR2(10),
    Address VARCHAR2(255),
    Phone_No VARCHAR2(20),
    Email VARCHAR2(100),
    PlanID NUMBER
);

Populating the database required planning as well. We inserted into independent tables first such as Equipment and Discounts. Then we populated the dependent tables once foreign key references existed. That order mattered. It prevented constraint errors and forced us to think logically about data flow.

The most interesting part for me was writing analytical queries. We created queries to list all classes with instructors, calculate monthly revenue across memberships and bookings, identify the top trainers by session count, and even detect members whose memberships expired but were still attending the gym. That is when the database started to feel real. It stopped being tables and started being insight.

SELECT
    SUM(m.FEE) AS Membership_Revenue,
    SUM(pt.FEE) AS PT_Session_Revenue,
    SUM(cb.FEE) AS Class_Booking_Revenue,
    (SUM(m.FEE) + SUM(pt.FEE) + SUM(cb.FEE)) AS Total_Revenue
FROM
    MEMBERSHIP m
LEFT JOIN PT_SESSION pt ON pt.MEMBERID = m.PLANID
LEFT JOIN CLASSBOOKING cb ON cb.MEMBERID = m.PLANID
WHERE
    TO_CHAR(m.EXPIRYDATE, 'MM-YYYY') = '12-2024'
    OR TO_CHAR(pt.SESSIONDATE, 'MM-YYYY') = '12-2024'
    OR TO_CHAR(cb.BOOKINGDATE, 'MM-YYYY') = '12-2024';
SELECT
    m.FIRST_NAME || ' ' || m.LAST_NAME AS Member_Name,
    m.EMAIL,
    m.PHONE_NO,
    mem.EXPIRYDATE,
    a.ATTENDANCEDATE
FROM
    MEMBER m
INNER JOIN
    MEMBERSHIP mem ON m.PLANID = mem.PLANID
INNER JOIN
    ATTENDANCE a ON a.MEMBERID = m.MEMBERID
WHERE
    mem.EXPIRYDATE < SYSDATE
    AND a.ATTENDANCEDATE BETWEEN SYSDATE - 30 AND SYSDATE;

We also implemented triggers to automate business logic. Membership status automatically changes to inactive once expired. Class capacity decreases when someone books a spot. Members can be notified before expiry. That was the point where it moved beyond basic CRUD and into automation.

CREATE OR REPLACE TRIGGER Update_Membership_Status
    AFTER UPDATE OR INSERT ON MEMBERSHIP
    FOR EACH ROW
BEGIN
    IF :NEW.EXPIRYDATE < SYSDATE THEN
        UPDATE MEMBERSHIP
        SET STATUS = 'Inactive'
        WHERE PLANID = :NEW.PLANID;
    END IF;
END;
/

Looking back, this project forced me to think like a systems designer. Not just writing code but designing rules, enforcing integrity, and thinking about how data behaves in the real world. We also discussed security considerations and how a system like this would need encryption and stronger protection if deployed in production.

It was a proper foundational database project. Structured. Logical. Clean. It made me appreciate backend systems more than ever.