All Data Labs

Data engineering

Bookings Star Schema with Price History

hard4–5 hours3 datasets
Company
Airbnb
Job positions
Data EngineerAnalytics EngineerBI Analyst
Topics
Data modelingStar schemaSlowly changing dimensionsPoint-in-time joinsSQL

The scenario

Airbnb: Model bookings as a star schema that keeps listing price history, so past revenue stays correct when hosts change prices.

Airbnb's finance analysts report booking revenue by city and room type. Hosts change nightly prices, and sometimes even room types, all the time. Today's reports join bookings to the latest listing details, so last quarter's revenue shifts every time a host edits a listing. You're a data engineer on the analytics platform team, and you've been asked to fix this in the data model.

Your task

Design and build a star schema that keeps listing history, so every booking is valued at the price and room type that applied on the day it was booked.

Instructions

  1. 1Explore the daily listing snapshots. Find what changes over time, which days are missing, and which listings are added or removed during the quarter.
  2. 2Build dim_listing as a type 2 slowly changing dimension: start a new version only when the room type or nightly price changes, with a surrogate key, valid_from, valid_to and is_current. A day with no snapshot is not a change.
  3. 3Build dim_host from the hosts file and a dim_date table that covers every date in the bookings.
  4. 4Build fact_bookings with one row per booking: the listing version key, date keys, nights, nightly_price_at_booking and booking_value, which is nights times that price.
  5. 5Report confirmed revenue by check-in month and room type from your model, and compare it with what joining bookings to the latest listing details would show.
  6. 6Add tests that every booking matches exactly one listing version and that a listing's versions never overlap.
  7. 7Document the model with a diagram and explain your design choices in a README.

Datasets

All files come in one download.

listing_snapshots.csv

One row per active listing per day, July 1 to September 30, 2025, as exported by a daily snapshot job.

2,585 rows · 6 columns · 107 KB

ColumnTypeDescription
snapshot_datedateDay the snapshot was taken.
listing_idintegerThe listing.
host_idintegerThe host who owns the listing.
citytextCity the listing is in.
room_typetextEntire home, Private room or Shared room.
nightly_priceintegerPrice per night on that day, in USD.
Preview the first 5 rows
snapshot_datelisting_idhost_idcityroom_typenightly_price
2025-07-013001201LisbonEntire home184
2025-07-023001201LisbonEntire home184
2025-07-033001201LisbonEntire home184
2025-07-043001201LisbonEntire home184
2025-07-053001201LisbonEntire home184

hosts.csv

One row per host.

12 rows · 4 columns · 1 KB

ColumnTypeDescription
host_idintegerUnique ID of the host.
host_nametextHost's name.
joined_datedateDate the host joined.
is_superhosttextWhether the host is a Superhost: yes or no.
Preview the first 5 rows
host_idhost_namejoined_dateis_superhost
201Ana Costa2016-11-12yes
202Pierre Martin2017-04-24no
203Yuki Tanaka2022-02-13no
204Rui Silva2019-08-10yes
205Camille Dubois2023-05-05yes

bookings.csv

One row per booking made during the quarter. The price is not stored on the booking.

900 rows · 7 columns · 52 KB

ColumnTypeDescription
booking_idintegerUnique ID of the booking.
listing_idintegerThe listing that was booked.
guest_idintegerThe guest who booked.
booked_atdateDate the booking was made.
check_indateFirst night of the stay.
check_outdateDay the guest leaves.
statustextconfirmed or canceled.
Preview the first 5 rows
booking_idlisting_idguest_idbooked_atcheck_incheck_outstatus
70101300151882025-07-012025-07-042025-07-05confirmed
70142301855082025-07-012025-07-282025-07-29confirmed
70157302652952025-07-012025-08-122025-08-14confirmed
70210302055532025-07-012025-07-032025-07-10confirmed
70211300454952025-07-012025-07-122025-07-17canceled

Hints

A listing that stops appearing before the last snapshot day was removed. When every listing is missing on the same day, that is a gap in the snapshot job, not a change.

To find the listing version that applied when a booking was made, match on listing and take the latest version that started on or before the booking date.

pd.merge_asof(
    bookings.sort_values("booked_at"),
    versions.sort_values("valid_from"),
    left_on="booked_at",
    right_on="valid_from",
    by="listing_id",
)

Deliverable

A public GitHub repo with your model code (SQL, dbt or Python), CSV outputs of dim_listing and fact_bookings, a monthly revenue report, your tests, and a README with a schema diagram and your design choices.

When you're done, post your repo in the Solutions tab to share it with other learners.

What grading checks

Use this checklist to review your own work before you post and share it.

  • Submitted GitHub repo is public and reachable.
  • Repo contains model code in SQL, dbt or Python.
  • dim_listing has the expected versions for every listing, with correct valid_from and valid_to dates.
  • fact_bookings values every booking at the nightly price that applied on its booking date.
  • Monthly confirmed revenue by room type matches the expected totals.
  • Tests check that each booking matches one listing version and that versions never overlap.
  • README includes a schema diagram and compares the result with joining to the latest listing details.