Database design practice · ARU MOD002589

The airline model, rebuilt small

Ten tables. It answers all three client questions. Click a question to see the path it takes.

How to read the diagram

Box
A table. Name on top, columns underneath.
PK · primary key
The row's own ID. Like a passport number: it never repeats.
FK · foreign key
A column holding another table's PK. It is what makes a line real.
+ green column
Added to answer one of the questions.
dashed box
A whole new table.
1..1 · 0..* · 1..*
Exactly one · none or many · at least one.

Read each line from the many end. A Booking is made by one Client. A Client makes none or many Bookings.

The model

Pick a question. The tables and lines it uses light up; the rest fade.

On a phone, scroll the diagram sideways.

Q1 · Price-change alerts

Find all changed prices, and the customers who opted in to price-change alerts.As a programmer, I want to send price-change alerts to customers who opted in.

Old model

Couldn't answer

fares.price only held today's price. A new price overwrote the old one, so there was nothing to compare. Nothing recorded who wanted alerts.

What was added

  • FareHistory: a new row every time a price changes. Old price, new price, when.
  • PriceAlert: links a Client to a Flight they want alerts for.

Why a table in the middle? One client can watch many flights. One flight can be watched by many clients. That is many-to-many, and many-to-many always becomes a link table.

The SQL

SELECT  c.name, c.email, f.flight_number,
        fa.passenger_type, fh.old_price, fh.new_price
FROM    FareHistory fh
JOIN    Fare       fa ON fa.fare_id   = fh.fare_id
JOIN    Flight     f  ON f.flight_id  = fa.flight_id
JOIN    PriceAlert pa ON pa.flight_id = f.flight_id
JOIN    Client     c  ON c.client_id  = pa.client_id
WHERE   fh.changed_at >= '2019-11-01'   -- since the last run
ORDER BY c.name;

Read it like this

  1. Start at FareHistory: every price change.
  2. Join Fare, then Flight: which flight the price belongs to.
  3. Join PriceAlert: who is watching that flight.
  4. Join Client: their name and email.
  5. Keep only changes since the program last ran.

What it returns with the practice data

nameflightfareoldnew
Amy ClarkeEK001adult480.00450.00
Amy ClarkeEK001child360.00340.00
Raj PatelBA139adult580.00610.00
Tom HughesEK001adult480.00450.00
Tom HughesEK001child360.00340.00
In short

The first model couldn't answer Q1 because a new price overwrote the old one. FareHistory keeps every change, and PriceAlert records who opted in. Clients and flights are many-to-many here, so PriceAlert is a link table between them.

Q2 · Bookings by country

Find bookings per country for one airline, most first.As an airline, I want to know which country most bookings come from, to plan more services.

Old model

Half

It could find the airline, and the departure airport's country. It had no customer country.

Decide first: "which country" could mean the customer's country or the departure airport's country. This example uses the customer's, because that's where demand comes from.

What was added

  • Country: a lookup table. Each country is written once.
  • Client.country_id: points at Country.
  • Airport.country_id: points at the same table, so "UK" is never spelt two ways.

Not a normalisation problem. The old tables were linked fine. The query would not fail; the country just wasn't stored anywhere.

The SQL

SELECT  co.name AS country, COUNT(*) AS bookings
FROM    Booking b
JOIN    Flight  f  ON f.flight_id   = b.flight_id
JOIN    Airline a  ON a.airline_id  = f.airline_id
JOIN    Client  c  ON c.client_id   = b.client_id
JOIN    Country co ON co.country_id = c.country_id
WHERE   a.name = 'Emirates'
  AND   b.status = 'confirmed'
GROUP BY co.country_id, co.name
ORDER BY bookings DESC;

Read it like this

  1. Start at Booking.
  2. Join Flight and Airline. Keep only Emirates.
  3. Join Client and Country to get each booking's country.
  4. Leave out cancelled bookings.
  5. GROUP BY country, then COUNT the rows in each group.
  6. ORDER BY the count, biggest first (DESC).

What it returns with the practice data

countrybookings
United Kingdom3
India2
United Arab Emirates1
In short

"Which country" was ambiguous, so this uses the customer's country. A Country lookup table stores each country once, and both Client and Airport point to it. Cancelled bookings are left out so the count is real demand.

Q3 · Family flights

List all flights in December 2019 that offer child discounts and kids' meals.As a user, I want better options to travel with my family.

Old model

Couldn't answer

Fares had no child price. There was no meal data. dietary_restrictions is what a passenger needs, not what a flight offers.

Watch out: filter on the flight's departure date. The booking date is when someone paid, not when they fly.

What was added

  • Fare.passenger_type: adult, child or infant. A child discount is a child fare row.
  • Flight.offers_kids_meal: yes or no.
  • Passenger.date_of_birth: so a child fare can be checked against a real child.

Simple vs flexible. Yes/no is the simple option. If the airline had many meal types, a Meal table plus a link table would be better.

The SQL

SELECT DISTINCT f.flight_number, f.departure_time
FROM    Flight f
JOIN    Fare fa ON fa.flight_id = f.flight_id
WHERE   f.departure_time >= '2019-12-01'
  AND   f.departure_time <  '2020-01-01'
  AND   f.offers_kids_meal = TRUE
  AND   fa.passenger_type  = 'child'
ORDER BY f.departure_time;

Read it like this

  1. Start at Flight.
  2. Keep flights that depart in December 2019.
  3. Keep flights that offer a kids' meal.
  4. Join Fare. Keep flights that have a child fare.
  5. DISTINCT: show each flight once, even if it has two child fares.

Stretch: check the child price is lower than the adult price. That needs a subquery (textbook 6.3.5).

What it returns with the practice data

flight_numberdeparture_time
EK0012019-12-20 08:00
BA1392019-12-23 20:00

EK003 has a kids' meal but no child fare. BA107 has a child fare but no kids' meal. EK002 flies in November. All three are correctly left out.

In short

Q3 needed two things the first model didn't have: passenger_type on Fare, so a flight can have a child fare, and a yes/no offers_kids_meal on Flight. The filter uses the flight's departure date, not the booking date.

Why this one is normalised

Every fact lives in one place. Other tables point at it with an ID instead of copying it.

A country's nameonly in Country
An airline's nameonly in Airline
A client's emailonly in Client
An airport's cityonly in Airport
A price changeits own row in FareHistory

One thing per box. passenger_type holds one word. offers_kids_meal is yes or no. No lists with commas.

What was left out, and why

staffpayroll_detailsflight_staffaircraftplane_historydisruptionsroute_historybaggage_optionsclient_payment_information

None of the three questions need them. Add a table only when a question needs it.

Try it yourself

The full MySQL script (all 10 tables, mock data and these three queries) is in this repository: airline_worked_example.sql. Run it in MySQL Workbench or any MySQL 8 client. Then change a price, add a client, and run the queries again.