Case study

PakBites: a MySQL database for food delivery

PakBites is a practice project: the database for a business that runs restaurants in several Pakistani cities. I designed the tables, filled them with sample data, wrote the queries, then added triggers, scheduled events, functions and views on top.

  • MySQL 8 · MySQL Workbench
  • 10 tables
  • 5 views
  • 6 triggers
  • 3 functions
  • 2 scheduled events

Built in steps

From the commit history:

  1. 6 FebSet up the repo and practised branches and a merge conflict.
  2. 9 FebRequirements, the first ER diagram, and written answers on normalization and data integrity. Then the tables, and changes with ALTER TABLE.
  3. 10 FebSample data, updates and deletes. Queries: basics, joins, subqueries and grouping.
  4. 11 FebFunctions, triggers, scheduled events and views. Indexes and EXPLAIN.
  5. 12 FebWritten answers on transactions: ACID, isolation levels and deadlocks.

The tables

Final PakBites ER diagram from MySQL Workbench: orders, customers, restaurants, menuitems, orderitems, deliveryriders, reviews, cities, orderstatushistory and dailysummary, plus five views

The final schema in MySQL Workbench. Open the image for full size.

What each table holds, in the words of the project README:

customers
Stores user profiles, including names, contact details, and loyalty_points earned through orders.
restaurants
Contains data for food venues, including their name, cuisine_type, average rating, and location linked to a city.
menuitems
Lists all food items available across all restaurants, including item_name, price, and the specific restaurant_id they belong to.
orders
The central table tracking every transaction, including the customer_id, restaurant_id, rider_id, total_amount, order_status, and timestamps.
orderitems
A bridge table that records the specific menu items included in each order, along with the quantity and calculated line_total.
orderstatushistory
Automatically logs every status change for an order (e.g., from 'pending' to 'delivered') to provide a chronological audit trail.
deliveryriders
Tracks the people delivering food, their current status (available, busy, or offline), and their assigned city.
reviews
Stores customer feedback and numeric ratings for specific restaurants they have ordered from.
cities
A reference table used to categorize restaurants and riders by their geographic location (e.g., Lahore, Islamabad).
dailysummary
A logging table populated by scheduled events to track daily performance metrics like total orders and revenue.

First design vs final tables

The first PakBites ER diagram from 9 February 2026, with PascalCase column names and a Riders table

The first diagram (9 Feb, left) compared with the tables I ended up with:

  • Names went from PascalCase (CustomerID) to snake_case (customer_id).
  • Riders became DeliveryRiders, with a vehicle type, an Available / Busy / Offline status, and location columns (current_lat, current_lng).
  • Customers: FullName became first_name and last_name. loyalty_points was added later with ALTER TABLE.
  • Orders lost DeliveryFee and DeliveryAddress. A CalculateDeliveryFee function was added later.
  • OrderItems gained unit_price. A trigger fills it from the menu price when an item is added.
  • OrderStatusHistory went from free text (VARCHAR(50)) to the same status list (ENUM) as Orders.
  • Cities gained postal_code and a rule that a city name is unique within its province.
  • A new table, DailySummary, filled once a day by a scheduled event.

Rules inside the tables

Every order must point to a real customer and restaurant (and a real rider, once one is assigned), and its status can only be one of five values. A customer can review a restaurant only once, and a rating must be from 1 to 5.

pakbites-database/Phase3-DDL/02-create_tableslines 55–67 @ 182aa08 ↗
CREATE TABLE Orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT NOT NULL,
    restaurant_id INT NOT NULL,
    rider_id INT,
    total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    order_status ENUM('Pending', 'Preparing', 'Out for Delivery', 'Delivered', 'Cancelled') DEFAULT 'Pending',
    payment_method ENUM('Cash', 'Credit Card', 'Wallet', 'Digital Payment') NOT NULL,
    ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES Customers(customer_id),
    FOREIGN KEY (restaurant_id) REFERENCES Restaurants(restaurant_id),
    FOREIGN KEY (rider_id) REFERENCES DeliveryRiders(rider_id)
);
pakbites-database/Phase3-DDL/02-create_tableslines 81–92 @ 182aa08 ↗
CREATE TABLE Reviews (
    review_id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT NOT NULL,
    restaurant_id INT NOT NULL,
    rating INT NOT NULL,
    comment TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES Customers(customer_id),
    FOREIGN KEY (restaurant_id) REFERENCES Restaurants(restaurant_id),
    UNIQUE (customer_id, restaurant_id),
    CONSTRAINT chk_review_rating CHECK (rating BETWEEN 1 AND 5)
);

Triggers: the database keeps its own records

When an order's status changes, a trigger writes the old and the new status to orderstatushistory, so every order has a full timeline. Another trigger refuses a review if the customer never ordered from that restaurant.

pakbites-database/Phase6-advanced/02_stored_procedures.sqllines 29–46 @ 4770530 ↗
create trigger trg_orders_after_update_status
after update on orders
for each row
begin
    if old.order_status <> new.order_status then
        insert into orderstatushistory (order_id, old_status, new_status, changed_at)
        values (new.order_id, old.order_status, new.order_status, current_timestamp);
    end if;
end //

create trigger trg_reviews_before_insert
before insert on reviews
for each row
begin
    if not exists (select 1 from orders where customer_id = new.customer_id and restaurant_id = new.restaurant_id) then
        signal sqlstate '45000' set message_text = 'customer must order from restaurant before reviewing';
    end if;
end //

Four more triggers copy the menu price into each order line, keep the order total up to date when lines are added or removed, and recalculate a restaurant's rating after each review.

Scheduled events

Every 15 minutes, orders still pending after 2 hours are cancelled. Once a day, yesterday's order count and revenue are saved to dailysummary.

pakbites-database/Phase6-advanced/04_events_views.sqllines 76–98 @ 182aa08 ↗
create event event_autocancel_pending_orders
on schedule every 15 minute
do
begin
    update orders
    set order_status = 'cancelled'
    where order_status = 'pending'
    and ordered_at < now() - interval 2 hour;
end //

create event event_daily_revenue_summary
on schedule every 1 day
starts (curdate() + interval 1 day)
do
begin
    insert into dailysummary (summary_date, total_orders, total_revenue)
    select 
        curdate() - interval 1 day, 
        count(order_id), 
        coalesce(sum(total_amount), 0)
    from orders
    where date(ordered_at) = curdate() - interval 1 day;
end //

A view of live orders, and a fee function

activeorders lists every order that is not yet delivered or cancelled, with the customer, restaurant and rider names and how long ago it was placed.

pakbites-database/Phase6-advanced/04_events_views.sqllines 1–13 @ 182aa08 ↗
create view activeorders as
select 
    o.order_id, 
    concat(c.first_name, ' ', c.last_name) as customer_name, 
    r.name as restaurant_name, 
    concat(dr.first_name, ' ', dr.last_name) as rider_name, 
    o.order_status, 
    timediff(now(), o.ordered_at) as time_since_placed 
from orders o 
join customers c on o.customer_id = c.customer_id 
join restaurants r on o.restaurant_id = r.restaurant_id 
left join deliveryriders dr on o.rider_id = dr.rider_id 
where o.order_status not in ('delivered', 'cancelled');

Delivery fees go up with distance: Rs. 50 under 3 km, up to Rs. 250 over 15 km.

pakbites-database/Phase6-advanced/01_functions.sqllines 1–14 @ 182aa08 ↗
delimiter //
Create Function CalculateDeliveryFee (distance_km DECIMAL(5,2)) 
returns decimal deterministic 
begin 
declare fee decimal (5,2);
set fee = case
        when distance_km < 3 then 50.00
        when distance_km >= 3 and distance_km <= 7 then 100.00
        when distance_km > 7 and distance_km <= 15 then 150.00
        else 250.00
           end;
    return fee;
end //
delimiter ;

One of the queries

Riders who delivered more orders than the average rider:

pakbites-database/phase5-Queries/03_subquerieslines 13–14 @ 182aa08 ↗
select first_name, last_name from deliveryriders where rider_id in (select rider_id from orders where order_status = 'delivered' 
group by rider_id having count(*) > (select count(*) / count(distinct rider_id) from orders where order_status = 'delivered'));