Coding questions · Expedia
Audit and onboard unfamiliar datasets safely
This question evaluates a data scientist's competency in dataset discovery, schema and key verification, profiling for distributions/missingness/timezones/currencies, duplicate and join-inflation detection, event-sequence validation, accurate metric computation, and the design of data-quality tests/data contracts within SQL/Python workflows.
You inherit unfamiliar hotel search data with sparse documentation. Provide a concrete, ordered checklist to: a) discover tables/columns and verify primary/foreign keys; b) profile distributions, missingness, and timezones/locale/currency issues; c) detect duplicates and many-to-many join inflation across searches, impressions, clicks, bookings, and cancellations; d) validate event sequencing (search → impression → click → booking → cancellation) with watermarking and late-arrival windows; e) compute metrics correctly (e.g., bookings per search within 7 days; margin = price − cost; GMV vs. contribution margin); f) write data quality tests/data contracts (not-null, uniqueness, referential integrity, numeric ranges). Include at least three pitfalls specific to travel data (e.g., multi-room bookings, partial cancellations/modifications, rebookings, cross-currency FX at booking vs. stay date, children vs. adults counts) and how you’d detect each.
Search-to-booking funnel metrics with GMV and margin
You inherit a hotel search dataset with separate event tables for searches, impressions, clicks, bookings, and cancellations. Using the schema and sample data below, write a SQL query that returns one row per search with the following metrics:
- impressions_count – number of distinct hotels shown (impressions) for that search.
- clicks_count – number of distinct hotels clicked for that search.
- bookings_within_7d – number of bookings whose booking_timestamp is between the search_timestamp and search_timestamp + 7 days (inclusive), and whose original_search_id = search_id.
- gmv_within_7d – total GMV (sum of booking price) for bookings counted in bookings_within_7d.
- contribution_margin_within_7d – total contribution margin (sum of price − cost) for those bookings.
Avoid many-to-many join inflation when combining impressions, clicks, and bookings. Return all searches, even if some metrics are zero.
Table searches
| column | type | constraints |
|---|
| search_id | INT | PRIMARY KEY |
| user_id | INT | NOT NULL |
| search_timestamp | TIMESTAMP | NOT NULL |
| checkin_date | DATE | NOT NULL |
| checkout_date | DATE | NOT NULL |
| num_adults | INT | NOT NULL |
| num_children | INT | NOT NULL |
| currency | VARCHAR(3) | NOT NULL |
| locale | VARCHAR(5) | NOT NULL |
Sample data:
| checkin_date | checkout_date | currency | locale | num_adults | num_children | search_id | search_timestamp | user_id |
|---|
| 2025-05-10 | 2025-05-12 | USD | en-US | 2 | 0 | 1 | 2025-05-01 10:00:00 | 101 |
| 2025-05-20 | 2025-05-22 | EUR | de-DE | 2 | 1 | 2 | 2025-05-02 09:00:00 | 102 |
| 2025-05-25 | 2025-05-28 | GBP | en-GB | 1 | 2 | 3 | 2025-05-03 20:00:00 | 101 |
| 2025-06-15 | 2025-06-18 | USD | en-US | 2 | 0 | 4 | 2025-05-01 08:00:00 | 103 |
Table impressions
| column | type | constraints |
|---|
| impression_id | INT | PRIMARY KEY |
| search_id | INT | NOT NULL |
| hotel_id | INT | NOT NULL |
| impression_timestamp | TIMESTAMP | NOT NULL |
| position | INT | NOT NULL |
Sample data:
| hotel_id | impression_id | impression_timestamp | position | search_id |
|---|
| 10 | 1 | 2025-05-01 10:01:00 | 1 | 1 |
| 11 | 2 | 2025-05-01 10:01:30 | 2 | 1 |
| 20 | 3 | 2025-05-02 09:01:00 | 1 | 2 |
| 21 | 4 | 2025-05-02 09:01:30 | 2 | 2 |
| 20 | 5 | 2025-05-02 09:02:00 | 3 | 2 |
| 30 | 6 | 2025-05-03 20:05:00 | 1 | 3 |
| 40 | 7 | 2025-05-01 08:01:00 | 1 | 4 |
| 10 | 8 | 2025-05-01 10:03:00 | 3 | 1 |
Table clicks
| column | type | constraints |
|---|
| click_id | INT | PRIMARY KEY |
| search_id | INT | NOT NULL |
| hotel_id | INT | NOT NULL |
| click_timestamp | TIMESTAMP | NOT NULL |
Sample data:
| click_id | click_timestamp | hotel_id | search_id |
|---|
| 1 | 2025-05-01 10:02:00 | 10 | 1 |
| 2 | 2025-05-02 09:05:00 | 20 | 2 |
| 3 | 2025-05-02 09:06:00 | 20 | 2 |
| 4 | 2025-05-03 22:00:00 | 30 | 3 |
| 5 | 2025-05-01 08:10:00 | 40 | 4 |
Table bookings
| column | type | constraints |
|---|
| booking_id | INT | PRIMARY KEY |
| original_search_id | INT | |
| hotel_id | INT | NOT NULL |
| user_id | INT | NOT NULL |
| booking_timestamp | TIMESTAMP | NOT NULL |
| checkin_date | DATE | NOT NULL |
| checkout_date | DATE | NOT NULL |
| rooms | INT | NOT NULL |
| adults | INT | NOT NULL |
| children | INT | NOT NULL |
| currency | VARCHAR(3) | NOT NULL |
| price | DECIMAL(10,2) | NOT NULL |
| cost | DECIMAL(10,2) | NOT NULL |
| property_currency | VARCHAR(3) | NOT NULL |
| event_ingested_at | TIMESTAMP | NOT NULL |
Sample data:
| adults | booking_id | booking_timestamp | checkin_date | checkout_date | children | cost | currency | event_ingested_at | hotel_id | original_search_id | price | property_currency | rooms | user_id |
|---|
| 2 | 5001 | 2025-05-01 10:05:00 | 2025-05-10 | 2025-05-12 | 0 | 250 | USD | 2025-05-01 10:06:00 | 10 | 1 | 300 | USD | 1 | 101 |
| 2 | 5002 | 2025-05-10 12:00:00 | 2025-05-20 | 2025-05-22 | 1 | 480 | EUR | 2025-05-13 12:00:00 | 20 | 2 | 600 | USD | 2 | 102 |
| 1 | 5003 | 2025-05-03 21:00:00 | 2025-05-25 | 2025-05-28 | 2 | 300 | GBP | 2025-05-03 21:01:00 | 30 | 3 | 400 | GBP | 1 | 101 |
| 2 | 5004 | 2025-05-01 08:20:00 | 2025-06-15 | 2025-06-18 | 0 | 350 | USD | 2025-05-05 09:00:00 | 40 | 4 | 450 | USD | 1 | 103 |
| 2 | 5005 | 2025-05-02 09:00:00 | 2025-06-15 | 2025-06-18 | 0 | 360 | USD | 2025-05-02 09:01:00 | 40 | 4 | 460 | USD | 1 | 103 |
| 2 | 5006 | 2025-05-05 12:00:00 | 2025-06-01 | 2025-06-03 | 0 | 150 | USD | 2025-05-05 12:00:00 | 50 | NULL | 200 | USD | 1 | 104 |
Table cancellations
| column | type | constraints |
|---|
| cancellation_id | INT | PRIMARY KEY |
| booking_id | INT | NOT NULL |
| cancellation_timestamp | TIMESTAMP | NOT NULL |
| cancelled_rooms | INT | NOT NULL |
| refund_amount | DECIMAL(10,2) | NOT NULL |
Sample data:
| booking_id | cancellation_id | cancellation_timestamp | cancelled_rooms | refund_amount |
|---|
| 5002 | 9001 | 2025-05-15 10:00:00 | 1 | 280 |
| 5004 | 9002 | 2025-05-02 08:00:00 | 1 | 450 |
Table fx_rates
| column | type | constraints |
|---|
| currency | VARCHAR(3) | NOT NULL |
| rate_date | DATE | NOT NULL |
| rate_to_usd | DECIMAL(10,4) | NOT NULL |
Sample data:
| currency | rate_date | rate_to_usd |
|---|
| EUR | 2025-05-10 | 1.1 |
| EUR | 2025-05-20 | 1.2 |
| GBP | 2025-05-03 | 1.3 |
| GBP | 2025-05-25 | 1.25 |
| USD | 2025-05-01 | 1 |
| USD | 2025-05-02 | 1 |
| USD | 2025-05-05 | 1 |
| USD | 2025-05-10 | 1 |
| USD | 2025-06-01 | 1 |
| USD | 2025-06-15 | 1 |
Expected output
One row per search with counts of distinct hotels seen and clicked, and booking/GMV/margin metrics for bookings within 7 days of the search.
| bookings_within_7d | clicks_count | contribution_margin_within_7d | gmv_within_7d | impressions_count | search_id |
|---|
| 1 | 1 | 50 | 300 | 2 | 1 |
| 0 | 1 | 0 | 0 | 2 | 2 |
| 1 | 1 | 100 | 400 | 1 | 3 |
| 2 | 1 | 200 | 910 | 1 | 4 |
Hints
- Pre-aggregate impressions and clicks per search in separate CTEs to avoid many-to-many join inflation.
- To compute bookings within 7 days, join bookings to searches on original_search_id and filter by booking_timestamp BETWEEN search_timestamp AND search_timestamp + INTERVAL '7 days'.
Validate event sequencing and late-arriving bookings
Using the same hotel funnel tables, write a SQL query that returns one row per booking with the following boolean flags:
- has_search – TRUE if the booking has a non-null original_search_id that matches a row in searches.
- has_click_before_booking – TRUE if there is at least one click for the same search_id and hotel_id with click_timestamp ≤ booking_timestamp.
- has_impression_before_click – TRUE if there is at least one impression for the same search_id and hotel_id with impression_timestamp ≤ the (earliest) click_timestamp.
- events_in_chronological_order – TRUE if the observed sequence is ordered as search_timestamp ≤ impression_timestamp ≤ click_timestamp ≤ booking_timestamp ≤ cancellation_timestamp (if a cancellation exists). If any prerequisite event is missing, treat the missing event as breaking the full ordered-chain condition.
- is_late_arrival – TRUE if event_ingested_at > booking_timestamp + 2 days (i.e., the booking event arrived more than 2 days late).
Return all bookings with these flags, ordered by booking_id.
Table searches
| column | type | constraints |
|---|
| search_id | INT | PRIMARY KEY |
| user_id | INT | NOT NULL |
| search_timestamp | TIMESTAMP | NOT NULL |
| checkin_date | DATE | NOT NULL |
| checkout_date | DATE | NOT NULL |
| num_adults | INT | NOT NULL |
| num_children | INT | NOT NULL |
| currency | VARCHAR(3) | NOT NULL |
| locale | VARCHAR(5) | NOT NULL |
Sample data:
| checkin_date | checkout_date | currency | locale | num_adults | num_children | search_id | search_timestamp | user_id |
|---|
| 2025-05-10 | 2025-05-12 | USD | en-US | 2 | 0 | 1 | 2025-05-01 10:00:00 | 101 |
| 2025-05-20 | 2025-05-22 | EUR | de-DE | 2 | 1 | 2 | 2025-05-02 09:00:00 | 102 |
| 2025-05-25 | 2025-05-28 | GBP | en-GB | 1 | 2 | 3 | 2025-05-03 20:00:00 | 101 |
| 2025-06-15 | 2025-06-18 | USD | en-US | 2 | 0 | 4 | 2025-05-01 08:00:00 | 103 |
Table impressions
| column | type | constraints |
|---|
| impression_id | INT | PRIMARY KEY |
| search_id | INT | NOT NULL |
| hotel_id | INT | NOT NULL |
| impression_timestamp | TIMESTAMP | NOT NULL |
| position | INT | NOT NULL |
Sample data:
| hotel_id | impression_id | impression_timestamp | position | search_id |
|---|
| 10 | 1 | 2025-05-01 10:01:00 | 1 | 1 |
| 11 | 2 | 2025-05-01 10:01:30 | 2 | 1 |
| 20 | 3 | 2025-05-02 09:01:00 | 1 | 2 |
| 21 | 4 | 2025-05-02 09:01:30 | 2 | 2 |
| 20 | 5 | 2025-05-02 09:02:00 | 3 | 2 |
| 30 | 6 | 2025-05-03 20:05:00 | 1 | 3 |
| 40 | 7 | 2025-05-01 08:01:00 | 1 | 4 |
| 10 | 8 | 2025-05-01 10:03:00 | 3 | 1 |
Table clicks
| column | type | constraints |
|---|
| click_id | INT | PRIMARY KEY |
| search_id | INT | NOT NULL |
| hotel_id | INT | NOT NULL |
| click_timestamp | TIMESTAMP | NOT NULL |
Sample data:
| click_id | click_timestamp | hotel_id | search_id |
|---|
| 1 | 2025-05-01 10:02:00 | 10 | 1 |
| 2 | 2025-05-02 09:05:00 | 20 | 2 |
| 3 | 2025-05-02 09:06:00 | 20 | 2 |
| 4 | 2025-05-03 22:00:00 | 30 | 3 |
| 5 | 2025-05-01 08:10:00 | 40 | 4 |
Table bookings
| column | type | constraints |
|---|
| booking_id | INT | PRIMARY KEY |
| original_search_id | INT | |
| hotel_id | INT | NOT NULL |
| user_id | INT | NOT NULL |
| booking_timestamp | TIMESTAMP | NOT NULL |
| checkin_date | DATE | NOT NULL |
| checkout_date | DATE | NOT NULL |
| rooms | INT | NOT NULL |
| adults | INT | NOT NULL |
| children | INT | NOT NULL |
| currency | VARCHAR(3) | NOT NULL |
| price | DECIMAL(10,2) | NOT NULL |
| cost | DECIMAL(10,2) | NOT NULL |
| property_currency | VARCHAR(3) | NOT NULL |
| event_ingested_at | TIMESTAMP | NOT NULL |
Sample data:
| adults | booking_id | booking_timestamp | checkin_date | checkout_date | children | cost | currency | event_ingested_at | hotel_id | original_search_id | price | property_currency | rooms | user_id |
|---|
| 2 | 5001 | 2025-05-01 10:05:00 | 2025-05-10 | 2025-05-12 | 0 | 250 | USD | 2025-05-01 10:06:00 | 10 | 1 | 300 | USD | 1 | 101 |
| 2 | 5002 | 2025-05-10 12:00:00 | 2025-05-20 | 2025-05-22 | 1 | 480 | EUR | 2025-05-13 12:00:00 | 20 | 2 | 600 | USD | 2 | 102 |
| 1 | 5003 | 2025-05-03 21:00:00 | 2025-05-25 | 2025-05-28 | 2 | 300 | GBP | 2025-05-03 21:01:00 | 30 | 3 | 400 | GBP | 1 | 101 |
| 2 | 5004 | 2025-05-01 08:20:00 | 2025-06-15 | 2025-06-18 | 0 | 350 | USD | 2025-05-05 09:00:00 | 40 | 4 | 450 | USD | 1 | 103 |
| 2 | 5005 | 2025-05-02 09:00:00 | 2025-06-15 | 2025-06-18 | 0 | 360 | USD | 2025-05-02 09:01:00 | 40 | 4 | 460 | USD | 1 | 103 |
| 2 | 5006 | 2025-05-05 12:00:00 | 2025-06-01 | 2025-06-03 | 0 | 150 | USD | 2025-05-05 12:00:00 | 50 | NULL | 200 | USD | 1 | 104 |
Table cancellations
| column | type | constraints |
|---|
| cancellation_id | INT | PRIMARY KEY |
| booking_id | INT | NOT NULL |
| cancellation_timestamp | TIMESTAMP | NOT NULL |
| cancelled_rooms | INT | NOT NULL |
| refund_amount | DECIMAL(10,2) | NOT NULL |
Sample data:
| booking_id | cancellation_id | cancellation_timestamp | cancelled_rooms | refund_amount |
|---|
| 5002 | 9001 | 2025-05-15 10:00:00 | 1 | 280 |
| 5004 | 9002 | 2025-05-02 08:00:00 | 1 | 450 |
Expected output
One row per booking with flags describing whether the search, impression, click, booking, and cancellation events are present in order, and whether the booking arrived more than 2 days late.
| booking_id | events_in_chronological_order | has_click_before_booking | has_impression_before_click | has_search | is_late_arrival |
|---|
| 5001 | true | true | true | true | false |
| 5002 | true | true | true | true | true |
| 5003 | false | false | true | true | false |
| 5004 | true | true | true | true | true |
| 5005 | true | true | true | true | false |
| 5006 | false | false | false | false | false |
Hints
- Aggregate to the earliest impression, click, and cancellation per booking using MIN() in a CTE.
- Derive the flags by comparing the ordered timestamps and checking event_ingested_at against booking_timestamp + INTERVAL '2 days'.
Detect travel-specific booking pitfalls with data quality flags
Using the same dataset, write a SQL query that returns one row per booking with boolean flags for at least three travel-specific pitfalls. Your output should include:
- is_multi_room – TRUE if rooms > 1 (multi-room bookings can change how you count stays vs. room nights).
- is_partial_cancellation – TRUE if the booking has cancellations with cancelled_rooms between 1 and rooms − 1 (indicating a partial cancellation).
- is_rebooking – TRUE if there exists another booking for the same user_id, hotel_id, and checkin_date (indicating rebookings or modifications).
- has_children_more_than_adults – TRUE if children > adults (suspicious guest-count distribution).
- has_fx_mismatch – TRUE if either currency <> property_currency or the FX rate at booking_date vs. checkin_date implies more than a 5% change in the USD value of the booking (highlighting cross-currency/FX issues).
Return booking_id and these five flags for all bookings, ordered by booking_id.
Table bookings
| column | type | constraints |
|---|
| booking_id | INT | PRIMARY KEY |
| original_search_id | INT | |
| hotel_id | INT | NOT NULL |
| user_id | INT | NOT NULL |
| booking_timestamp | TIMESTAMP | NOT NULL |
| checkin_date | DATE | NOT NULL |
| checkout_date | DATE | NOT NULL |
| rooms | INT | NOT NULL |
| adults | INT | NOT NULL |
| children | INT | NOT NULL |
| currency | VARCHAR(3) | NOT NULL |
| price | DECIMAL(10,2) | NOT NULL |
| cost | DECIMAL(10,2) | NOT NULL |
| property_currency | VARCHAR(3) | NOT NULL |
| event_ingested_at | TIMESTAMP | NOT NULL |
Sample data:
| adults | booking_id | booking_timestamp | checkin_date | checkout_date | children | cost | currency | event_ingested_at | hotel_id | original_search_id | price | property_currency | rooms | user_id |
|---|
| 2 | 5001 | 2025-05-01 10:05:00 | 2025-05-10 | 2025-05-12 | 0 | 250 | USD | 2025-05-01 10:06:00 | 10 | 1 | 300 | USD | 1 | 101 |
| 2 | 5002 | 2025-05-10 12:00:00 | 2025-05-20 | 2025-05-22 | 1 | 480 | EUR | 2025-05-13 12:00:00 | 20 | 2 | 600 | USD | 2 | 102 |
| 1 | 5003 | 2025-05-03 21:00:00 | 2025-05-25 | 2025-05-28 | 2 | 300 | GBP | 2025-05-03 21:01:00 | 30 | 3 | 400 | GBP | 1 | 101 |
| 2 | 5004 | 2025-05-01 08:20:00 | 2025-06-15 | 2025-06-18 | 0 | 350 | USD | 2025-05-05 09:00:00 | 40 | 4 | 450 | USD | 1 | 103 |
| 2 | 5005 | 2025-05-02 09:00:00 | 2025-06-15 | 2025-06-18 | 0 | 360 | USD | 2025-05-02 09:01:00 | 40 | 4 | 460 | USD | 1 | 103 |
| 2 | 5006 | 2025-05-05 12:00:00 | 2025-06-01 | 2025-06-03 | 0 | 150 | USD | 2025-05-05 12:00:00 | 50 | NULL | 200 | USD | 1 | 104 |
Table cancellations
| column | type | constraints |
|---|
| cancellation_id | INT | PRIMARY KEY |
| booking_id | INT | NOT NULL |
| cancellation_timestamp | TIMESTAMP | NOT NULL |
| cancelled_rooms | INT | NOT NULL |
| refund_amount | DECIMAL(10,2) | NOT NULL |
Sample data:
| booking_id | cancellation_id | cancellation_timestamp | cancelled_rooms | refund_amount |
|---|
| 5002 | 9001 | 2025-05-15 10:00:00 | 1 | 280 |
| 5004 | 9002 | 2025-05-02 08:00:00 | 1 | 450 |
Table fx_rates
| column | type | constraints |
|---|
| currency | VARCHAR(3) | NOT NULL |
| rate_date | DATE | NOT NULL |
| rate_to_usd | DECIMAL(10,4) | NOT NULL |
Sample data:
| currency | rate_date | rate_to_usd |
|---|
| EUR | 2025-05-10 | 1.1 |
| EUR | 2025-05-20 | 1.2 |
| GBP | 2025-05-03 | 1.3 |
| GBP | 2025-05-25 | 1.25 |
| USD | 2025-05-01 | 1 |
| USD | 2025-05-02 | 1 |
| USD | 2025-05-05 | 1 |
| USD | 2025-05-10 | 1 |
| USD | 2025-06-01 | 1 |
| USD | 2025-06-15 | 1 |
Expected output
One row per booking with flags for multi-room bookings, partial cancellations, rebookings, suspicious children/adults counts, and FX/currency mismatches.
| booking_id | has_children_more_than_adults | has_fx_mismatch | is_multi_room | is_partial_cancellation | is_rebooking |
|---|
| 5001 | false | false | false | false | false |
| 5002 | false | true | true | true | false |
| 5003 | true | false | false | false | false |
| 5004 | false | false | false | false | true |
| 5005 | false | false | false | false | true |
| 5006 | false | false | false | false | false |
Hints
- Use a window function (COUNT OVER PARTITION BY user_id, hotel_id, checkin_date) to detect rebookings.
- Aggregate cancellations by booking_id and join FX rates at booking and check-in dates to derive partial cancellation and FX mismatch flags.