The brief listed questions the museum's database should be able to answer. Here each one runs against the refined schema and five years of synthetic activity (1 March 2015 to 29 February 2020), next to an honest verdict on whether the 2020 design could have answered it. Results are computed on the server from the bundled SQLite database.
Question 1
2020 design: partly
How many tickets were bought with each payment method in the current financial year?
2020: Tickets can be traced to a payment method (Ticket to Invoices to Payment Methods to EFTPOS or Cash), but no 2020 table records when a purchase was made, so the count cannot be limited to a financial year. 2026: Every order has an ordered_at timestamp and exactly one payment whose method is a single column.
Answer to question 1
Payment method
Tickets
credit_card
629
digital_wallet
420
debit_card
262
cash
81
Assumes: Financial year runs 1 July to 30 June (the Australian convention, the subject's home; the brief does not define one). For a calendar year, set fy_start to '2020-01-01' and fy_end to '2020-12-31'.
Assumes: A ticket counts in the year it was ordered, whatever day it was used.
Tickets by payment method
credit_card629
digital_wallet420
debit_card262
cash81
Show the SQL
-- Tickets sold per payment method in the current financial yearWITH params AS(SELECT'2019-07-01'AS fy_start,'2020-06-30'AS fy_end)SELECT p.method AS payment_method,sum(l.quantity)AS tickets
FROM purchase_order o
JOIN payment p ON p.payment_id = o.payment_id
JOIN order_line l ON l.order_id = o.order_id
JOIN product pr ON pr.product_id = l.product_id
CROSSJOIN params
WHERE pr.code IN('TICKET_ONLINE','TICKET_ON_SITE')ANDdate(o.ordered_at)BETWEEN params.fy_start AND params.fy_end
GROUPBY p.method
ORDERBY tickets DESC;
Over the last six months, which entrance had the most visitors arriving on foot?
2020: Answerable by joining Entrance to Ticket.Transportation. Two catches: a walker who comes back through a different gate is counted twice, and a re-entry through the same gate cannot be stored at all (Entrance is keyed by gate and ticket). 2026: Transport is recorded once, on the first entry scan, so re-entries never double-count.
Answer to question 2
Entrance
Pedestrian visitors
Pyramid
144
Passage Richelieu
48
Carrousel du Louvre
36
99 rue de Rivoli
27
Porte des Lions
16
Assumes: 'On foot' means the visitor answered 'Walk' at their first entry of the day.
Assumes: "Last six months" ends on the dataset's last day, 2020-02-29.
Pedestrian visitors by entrance
Pyramid144
Passage Richelieu48
Carrousel du Louvre36
99 rue de Rivoli27
Porte des Lions16
Show the SQL
-- Entrances ranked by visitors who walked to the museum, last six monthsWITH params AS(SELECT'2020-02-29'AS today)SELECT en.name AS entrance,count(*)AS pedestrian_visitors
FROM museum_visit v
JOIN entrance en ON en.entrance_id = v.entrance_id
CROSSJOIN params
WHERE v.transport_mode ='Walk'AND v.visit_date >date(params.today,'-6 months')AND v.visit_date <= params.today
GROUPBY en.name
ORDERBY pedestrian_visitors DESC;
What share of each day's visitors hire an audio guide?
2020: Answerable: daily visitors come from Entrance and hires from Hired Audio Guide, both via Ticket ID. 2026: The museum_visit view gives one row per admitted ticket per day.
Answer to question 3
Month
Days
Visitors
Hired guide
Avg daily pct
2019-03
28
191
18
17
2019-04
29
253
18
14.3
2019-05
28
217
15
9.5
2019-06
26
154
24
14.9
2019-07
27
208
30
22.3
2019-08
29
200
25
9.6
2019-09
23
104
12
11.8
2019-10
25
206
17
13.6
2019-11
28
217
20
16.4
2019-12
27
234
29
22.6
2020-01
25
124
18
14.2
2020-02
22
107
16
21.8
Assumes: A visitor is an admitted ticket. The share is worked out for every day, then averaged by month for the last 12 months.
Avg daily pct by month
2019-0317%
2019-0414.3%
2019-059.5%
2019-0614.9%
2019-0722.3%
2019-089.6%
2019-0911.8%
2019-1013.6%
2019-1116.4%
2019-1222.6%
2020-0114.2%
2020-0221.8%
Show the SQL
-- Share of each day's visitors who hired an audio guide, averaged by monthWITH daily AS(SELECT v.visit_date,count(DISTINCT v.ticket_id)AS visitors,count(DISTINCT h.ticket_id)AS hired_guide
FROM museum_visit v
LEFTJOIN audio_guide_hire h ON h.ticket_id = v.ticket_id
GROUPBY v.visit_date
)SELECTstrftime('%Y-%m', visit_date)AS month,count(*)AS days,sum(visitors)AS visitors,sum(hired_guide)AS hired_guide,round(avg(100.0* hired_guide / visitors),1)AS avg_daily_pct
FROM daily
WHERE visit_date >date('2020-02-29','-12 months')GROUPBY month
ORDERBY month;
On a given day, how long was each audio guide in use?
2020: Answerable from Hired Audio Guide's Issued Time and Return Time. The catch is upstream: a guide still in service has no Retired Date, so it cannot be registered, and its hires are refused with it. 2026: Return time must be after issue time, and a device cannot be out with two visitors at once.
Answer to question 4
Serial number
Language
Issued
Returned
Minutes in use
D952G324M2778N96
Korean
09:30:55
11:19:09
108
D207S645U1857P52
French
12:09:03
13:52:39
104
D004U524E2632P53
Italian
13:21:58
14:46:12
84
D831V786T8186E00
English
14:41:49
16:20:14
98
D004U524E2632P53
English
15:11:15
16:23:12
72
Assumes: The example day is Tuesday 6 August 2019, in the summer peak; change params.day to look at another.
Assumes: Guides that were never returned show no duration.
Minutes in use by serial number
D952G324M2778N96108
D207S645U1857P52104
D004U524E2632P5384
D831V786T8186E0098
D004U524E2632P5372
Show the SQL
-- Minutes each audio guide was out on one dayWITH params AS(SELECT'2019-08-06'AS day)SELECT h.serial_number,
lang.name AS language,time(h.issued_at)AS issued,time(h.returned_at)AS returned,CAST(round((julianday(h.returned_at)-julianday(h.issued_at))*1440)ASINTEGER)AS minutes_in_use
FROM audio_guide_hire h
JOIN language lang ON lang.language_code = h.language_code
CROSSJOIN params
WHEREdate(h.issued_at)= params.day
ORDERBY h.issued_at;
In what order do visitors most often tour the three wings?
2020: Not answerable. Wings is keyed by the wing name alone, so it can hold at most three rows in total; a second visit to Denon by anyone is a duplicate key. No per-ticket sequence can be stored. 2026: wing_scan stores every scan with its own key, ticket and time.
Answer to question 5
Route
Visitors
Pct
Denon
1,757
18.7
Denon > Sully
1,242
13.2
Richelieu
735
7.8
Denon > Richelieu
648
6.9
Sully
588
6.3
Richelieu > Denon
417
4.4
Sully > Richelieu
360
3.8
Denon > Richelieu > Denon
351
3.7
Sully > Denon
343
3.7
Denon > Sully > Denon
333
3.5
Assumes: A route is the sequence of wing scans for one ticket, with consecutive scans of the same wing merged.
Visitors by route
Denon1,757
Denon > Sully1,242
Richelieu735
Denon > Richelieu648
Sully588
Richelieu > Denon417
Sully > Richelieu360
Denon > Richelieu > Denon351
Sully > Denon343
Denon > Sully > Denon333
Show the SQL
-- Most common routes through the wingsWITH scans AS(SELECT s.ticket_id, s.scanned_at, s.scan_id, s.wing_id,lag(s.wing_id)OVER(PARTITIONBY s.ticket_id ORDERBY s.scanned_at, s.scan_id)AS previous_wing
FROM wing_scan s
),
routes AS(SELECT sc.ticket_id,group_concat(w.name,' > 'ORDERBY sc.scanned_at, sc.scan_id)AS route
FROM scans sc
JOIN wing w ON w.wing_id = sc.wing_id
WHERE sc.previous_wing ISNULLOR sc.previous_wing <> sc.wing_id
GROUPBY sc.ticket_id
)SELECT route,count(*)AS visitors,round(100.0*count(*)/(SELECTcount(*)FROM routes),1)AS pct
FROM routes
GROUPBY route
ORDERBY visitors DESC, route
LIMIT10;
How many visitors managed to visit all three wings?
2020: Not answerable, for the same reason as question 5: Wings cannot record more than one visit per wing. 2026: Counting distinct wings per ticket in wing_scan answers it directly.
Answer to question 6
Year
Visitors
Saw all three
Pct
2015
1,513
249
16.5
2016
1,491
320
21.5
2017
1,724
297
17.2
2018
2,177
318
14.6
2019
2,247
396
17.6
2020
231
46
19.9
All years
9,383
1,626
17.3
Assumes: Counted per admitted ticket, by year of visit, with an all-years total.
Saw all three by year
2015249
2016320
2017297
2018318
2019396
202046
Show the SQL
-- Visitors who scanned into Richelieu, Denon and Sully on their visitWITH wings_seen AS(SELECT ticket_id,count(DISTINCT wing_id)AS wings
FROM wing_scan
GROUPBY ticket_id
),
per_year AS(SELECTstrftime('%Y', v.visit_date)AS year,count(*)AS visitors,sum(ws.wings =3)AS saw_all_three
FROM museum_visit v
LEFTJOIN wings_seen ws ON ws.ticket_id = v.ticket_id
GROUPBY year
)SELECT year, visitors, saw_all_three,round(100.0* saw_all_three / visitors,1)AS pct
FROM per_year
UNIONALLSELECT'All years',sum(visitors),sum(saw_all_three),round(100.0*sum(saw_all_three)/sum(visitors),1)FROM per_year;
Which ten special exhibitions had the highest attendance over the last five years?
2020: Only indirectly. Hall Napoleon admissions point to a time slot and a ticket but not to an exhibition, so each admission has to be matched back through Exhibition Booking on ticket and slot; admissions without a matching booking are lost. 2026: Each admission belongs to a booking, which belongs to a dated slot of one exhibition.
Answer to question 7
Title
Opens on
Closes on
Attendance
Leonardo da Vinci: 500th Anniversary
2019-10-24
2020-02-24
422
Bodies in Marble
2019-02-20
2019-05-27
260
Romantic Fury: French Painting 1820-1850
2018-03-28
2018-07-23
195
Cities of Clay: Mesopotamia
2017-10-04
2018-01-22
170
Light on the Water: Venetian Views
2017-02-22
2017-05-22
159
Crossroads: Art of the Mediterranean
2018-10-17
2019-01-21
154
Quiet Rooms: Dutch Domestic Painting
2016-02-24
2016-05-30
111
Treasures of the Royal Wardrobe
2015-03-11
2015-06-22
105
Porcelain for Kings
2019-06-12
2019-09-16
75
Paper Masters: Renaissance Drawings
2016-06-15
2016-08-29
69
Assumes: Attendance is admissions scanned at the Hall Napoleon door, not bookings.
Assumes: "Last five years" ends on 2020-02-29.
Attendance by title
Leonardo da Vinci: 500th Anniversary422
Bodies in Marble260
Romantic Fury: French Painting 1820-1850195
Cities of Clay: Mesopotamia170
Light on the Water: Venetian Views159
Crossroads: Art of the Mediterranean154
Quiet Rooms: Dutch Domestic Painting111
Treasures of the Royal Wardrobe105
Porcelain for Kings75
Paper Masters: Renaissance Drawings69
Show the SQL
-- Ten best-attended Hall Napoleon exhibitions, last five yearsWITH params AS(SELECT'2020-02-29'AS today)SELECT e.title,
e.opens_on,
e.closes_on,count(h.scan_id)AS attendance
FROM exhibition e
JOIN exhibition_slot s ON s.exhibition_id = e.exhibition_id
JOIN exhibition_booking b ON b.slot_id = s.slot_id
JOIN hall_napoleon_scan h ON h.booking_id = b.booking_id
CROSSJOIN params
WHERE s.slot_date >date(params.today,'-5 years')GROUPBY e.exhibition_id
ORDERBY attendance DESCLIMIT10;
Which 15-minute slots of the special exhibitions are the most popular by attendance?
2020: Answerable by counting Hall Napoleon rows per Time Slot. But Time Slot has no date or exhibition, so the brief's other need, live booked / admitted / remaining places for each slot, cannot be met. 2026: The slot_availability view gives booked, admitted and remaining places for every dated slot.
Answer to question 8
Slot
Admitted
Booked
Turned up pct
12:00
174
208
83.7
11:30
148
174
85.1
11:00
141
176
80.1
13:00
139
154
90.3
13:15
122
148
82.4
15:15
109
118
92.4
14:30
99
105
94.3
11:15
92
100
92
14:15
90
95
94.7
12:45
86
93
92.5
Assumes: Slots are compared by time of day across all exhibitions and dates.
Admitted by slot
12:00174
11:30148
11:00141
13:00139
13:15122
15:15109
14:3099
11:1592
14:1590
12:4586
Show the SQL
-- 15-minute slots ranked by admissionsSELECT s.start_time AS slot,count(h.scan_id)AS admitted,count(b.booking_id)AS booked,round(100.0*count(h.scan_id)/count(b.booking_id),1)AS turned_up_pct
FROM exhibition_slot s
JOIN exhibition_booking b ON b.slot_id = s.slot_id
LEFTJOIN hall_napoleon_scan h ON h.booking_id = b.booking_id
GROUPBY s.start_time
ORDERBY admitted DESC, slot
LIMIT10;
For large online orders (30 or more tickets), how many tickets were never used?
2020: Answerable on paper: Invoices holds the quantity, tickets point to their invoice, and an unused ticket has no Entrance row. 'Online' sits in EFTPOS.Purchase Channel, reached through Payment Methods, one of the tables MySQL refused to create. 2026: Channel is a column of the order; unused tickets are tickets with no entry scan.
Answer to question 9
Order id
Ordered on
Tickets
Unused
669
2015-09-24
51
16
1788
2016-12-18
53
16
4078
2019-01-31
43
14
1212
2016-05-10
32
13
3128
2018-04-20
42
13
3993
2018-12-20
37
13
205
2015-05-07
46
12
51
2015-03-20
45
11
246
2015-05-20
47
11
1876
2017-01-22
46
11
2081
2017-04-18
30
11
2302
2017-06-28
38
11
1202
2016-05-08
52
10
4135
2019-02-18
52
10
4862
2019-09-07
47
10
Unused tickets across every large online order:
288
Assumes: A ticket is unused if it was never scanned at an entrance.
Unused by order id
66916
178816
407814
121213
312813
399313
20512
5111
24611
187611
208111
230211
120210
413510
486210
Show the SQL
-- Large online orders and their unused ticketsSELECT o.order_id,date(o.ordered_at)AS ordered_on,
l.quantity AS tickets,sum(v.ticket_id ISNULL)AS unused,sum(sum(v.ticket_id ISNULL))OVER()AS unused_all_orders
FROM purchase_order o
JOIN order_line l ON l.order_id = o.order_id
JOIN product p ON p.product_id = l.product_id AND p.code ='TICKET_ONLINE'JOIN ticket t ON t.order_id = o.order_id
LEFTJOIN museum_visit v ON v.ticket_id = t.ticket_id
WHERE o.channel ='online'AND l.quantity >=30GROUPBY o.order_id
ORDERBY unused DESC, o.order_id
LIMIT15;
In the winter months, which weekday morning is the busiest?
2020: Answerable from Entrance Date/Time (the Business Day flag is redundant with the date). A visitor who re-enters before noon would be counted twice; in this data none does, so the 2020 answer matches. 2026: Only first entries count as arrivals, so re-entries do not inflate the morning.
Answer to question 10
Weekday
Morning visitors
Mornings
Per morning
Monday
183
29
6.31
Tuesday
172
41
4.2
Friday
133
30
4.43
Wednesday
103
36
2.86
Thursday
101
38
2.66
Assumes: Winter is December to February (Paris).
Assumes: Morning is a first entry before 12:00; weekends are excluded.
Morning visitors by weekday
Monday183
Tuesday172
Friday133
Wednesday103
Thursday101
Show the SQL
-- Winter weekday mornings ranked by visitors entering before noonSELECTCASEstrftime('%w', v.visit_date)WHEN'1'THEN'Monday'WHEN'2'THEN'Tuesday'WHEN'3'THEN'Wednesday'WHEN'4'THEN'Thursday'WHEN'5'THEN'Friday'ENDAS weekday,count(*)AS morning_visitors,count(DISTINCT v.visit_date)AS mornings,round(1.0*count(*)/count(DISTINCT v.visit_date),2)AS per_morning
FROM museum_visit v
WHERECAST(strftime('%m', v.visit_date)ASINTEGER)IN(12,1,2)ANDstrftime('%w', v.visit_date)NOTIN('0','6')ANDtime(v.first_entry_at)<'12:00:00'GROUPBYstrftime('%w', v.visit_date)ORDERBY morning_visitors DESC;