SKIP TO CONTENT
moonsdata
Darren on the Moon touching a glowing panel on a stack of shipping containers linked by lines of light, with a cargo ship sailing through the stars
[ CASE STUDY / 003 ]
— CASE STUDY / SHIPPING DATABASE

A shipping company, built from
22 tables.

SQLORACLEER DESIGNMSc2025
MODULEDatabases and Data Management
GRADEFirst
YEAR2025
STACKOracle SQL, Lucidchart
[ TRANSMISSION 002 ]
— THE BRIEF

Design a database
for the high seas.

This was an assignment for the Databases and Data Management module of my MSc, and one I got a First for. The brief was to design and build a database for a fictional shipping company called Royal Anchor Shipping. I worked on it over a few months, alongside my other assignments.

The brief listed ten tables as a starting point. Offices, vessels, containers, bookings, crew and routes. But I wanted it to work the way a real shipping company would, so before designing anything I spent a lot of time researching how the industry actually runs. Routes, ports, customs, tariffs, even what crew members earn.

By the end, those ten tables had become 22.

[ TRANSMISSION 003 ]
— THE DESIGN

Thinking in tables
before writing SQL.

I started with a simple list of everything the company would need to run. Then I mapped the relationships in Lucidchart one area at a time. Crew, vessels, routes, containers, then bookings and orders. Once each area made sense, I linked them all into one ER diagram.

One decision I'm proud of was splitting the crew into separate tables. Contact details, salary, employment, certification and job role each have their own table, so the main crew table stays clean instead of holding everything at once.

Every relationship came with an assumption. A vessel can have many schedules, but each schedule belongs to one vessel. A booking can have many orders, but every order belongs to one booking. Writing those out is what turned the diagram into rules the database could enforce.

The final ER diagram. 22 tables, from crew and vessels to bookings, customs and tracking. Click to enlarge.
[ TRANSMISSION 004 ]
— THE BUILD

Building it
for real.

I built it in Oracle SQL. Every table has primary and foreign keys, plus constraints like NOT NULL to keep bad data out.

To test it properly I needed realistic data, so I generated sample data with ChatGPT and loaded it in with SQL and CSV files. That part took far longer than I expected. The data had to respect every relationship in the design, so I kept going back to fix data types, character lengths and foreign keys until everything lined up.

vessel.sqlSQL
CREATE TABLE vessel (
  vessel_id      VARCHAR2(4) PRIMARY KEY,
  vessel_type_id VARCHAR2(4),
  vessel_name    VARCHAR2(50) NOT NULL,
  IMO            NUMBER(10) NOT NULL,
  MMSI           NUMBER(10) NOT NULL,
  AIS_type_code  NUMBER(2),
  AIS_flag       NUMBER(3) NOT NULL,
  callsign       NUMBER(5) NOT NULL,
  gross_tonnage  NUMBER(6,2) NOT NULL,
  date_built     DATE NOT NULL,
  CONSTRAINT fk_vessel_type
    FOREIGN KEY (vessel_type_id)
    REFERENCES vessel_type(vessel_type_id)
);
One of the 22 tables. The foreign key links every vessel to its type.
[ TRANSMISSION 005 ]
— THE QUERIES

Asking it
real questions.

The final test was six queries that proved the database could answer the questions a shipping company would actually ask. Here are three of them.

track_orders.sqlSQL
SELECT
  book.booking_id,
  book.booking_date,
  cust.first_name AS customer_name,
  reci.first_name AS recipient_name,
  ord.order_id,
  ord.item_description,
  ord.weight,
  ord.volume,
  ord.order_status
FROM bookings book
JOIN customers cust ON book.cust_id = cust.cust_id
JOIN recipients reci ON book.recipient_id = reci.recipient_id
JOIN orders ord ON book.booking_id = ord.booking_id
ORDER BY book.booking_date, book.booking_id, ord.order_id;
Tracking every order, joining bookings, customers, recipients and orders.
heavy_orders.sqlSQL
SELECT
  ord.order_id,
  ord.item_description,
  ord.weight,
  book.booking_id,
  vses.vessel_id,
  po.port_name AS destination
FROM orders ord
JOIN bookings book ON ord.booking_id = book.booking_id
JOIN vessel_schedules vses ON book.ves_schedule_id = vses.ves_schedule_id
JOIN ports po ON vses.port_id = po.port_id
WHERE ord.weight > 1000;
Every order over 1000kg, the vessel carrying it and where it's heading.
vessel_schedule.sqlSQL
SELECT
  ve.vessel_name,
  vs.port_id,
  po.port_name,
  TO_CHAR(vs.arrival_date, 'DD-Mon-YYYY') AS eta,
  TO_CHAR(vs.departure_date, 'DD-Mon-YYYY') AS etd
FROM vessel ve
JOIN vessel_schedules vs ON ve.vessel_id = vs.vessel_id
JOIN ports po ON vs.port_id = po.port_id
WHERE ve.vessel_name = 'RAC ULCV Spirit'
ORDER BY vs.port_order;
The full schedule for one vessel, stop by stop.
[ TRANSMISSION 006 ]
— WHAT I LEARNED

Four things
about thinking in tables.

  1. 01.

    Research is useful, but the design is the foundation. Looking back, I'd spend less time researching and more time on the ER diagram.

  2. 02.

    More isn't always better. At one point my vessel table had almost 20 attributes. Cutting it down to what actually mattered made the whole design stronger.

  3. 03.

    Relationships are the hard part. I redrew the ER diagram several times as I built the tables, because some things only made sense once real data hit them.

  4. 04.

    Sample data is part of the design. Getting it to respect every relationship taught me more about my own database than the diagram did.

— END OF TRANSMISSION 006