Databases · SQLite · SQL

MenuDatabase

A normalised (3NF) relational database for a restaurant — the schema, the foreign keys, the junction tables, and a library of SQL queries that run a menu and its orders. Ten tables, built and queried from pure Python with sqlite3; every result below is real output from the test data.

10 tables3NF M:N junctionsforeign keys 11 queries22 tests passing

The model

Ten tables, one fact in one place

Four reference/entity tables (categories, menu items, ingredients, plus customers, staff and dining tables), two many-to-many junction tables, and the transactional core — orders, order lines and payments. A dish stores its category_id, never the category's name; an order stores who, where and when by id, never copies of names.

categoriesPK category_id · name menu_itemsPK item_idFK category_id · price item_ingredientsPK item_id+ingr_idquantity (per unit) ingredientsPK ingredient_idstock · reorder_level ordersPK order_idFK customer/staff/tableplaced_at · status order_itemsFK order_id · item_idquantity · unit_price paymentsFK order_id · amountmethod customersstaff · tables 1:N M:N links 1:N 1:N

menu_items ⇄ ingredients and orders ⇄ menu_items are both many-to-many, each resolved by a junction table that also carries the data that belongs to the pairing (an ingredient quantity; a line quantity and the price at time of sale).

Normalisation

Why it's split this way

A query, end to end

Revenue by category

Order lines joined up through menu items to their category, cancelled orders excluded. The SQL on the left; its real output on the right.

SELECT c.name AS category,
       ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM   order_items oi
JOIN   orders     o  ON o.order_id = oi.order_id
JOIN   menu_items mi ON mi.item_id = oi.item_id
JOIN   categories c  ON c.category_id = mi.category_id
WHERE  o.status <> 'cancelled'
GROUP  BY c.category_id
ORDER  BY revenue DESC;
categoryrevenue
Mains96.5
Starters19.5
Desserts12.0
Drinks9.5

Total = 137.50, which equals the sum of all recorded payments — the schema reconciles.

The query library

Real answers from the seed data

Eleven queries answer real questions. A few, run by demo.py:

Top-selling items

itemunits
Margherita Pizza4
Spaghetti Bolognese3
Bruschetta3
Tiramisu2
Grilled Salmon1

Ingredient usage (top)

ingredientqty used
Tomato1260.0 g
Flour800.0 g
Spaghetti540.0 g
Mozzarella480.0 g
Beef Mince450.0 g

Low stock (reorder)

ingredientstockreorder
Tomato8001500
Basil40100

Order 1, itemised

itemqtyline
Bruschetta16.5
Margherita Pizza222.0
Tiramisu16.0

Also: busiest hours (13:00, 3 orders) and tables (T1, 2 orders), average order value (27.50 over 5 orders), customer order history, items sharing ingredients (Bruschetta & Margherita share Tomato + Basil), and an order-vs-payment reconciliation view.

Verification

Built, seeded, tested

A pytest suite checks the schema creates cleanly with foreign keys enforced, that orphan inserts raise IntegrityError, that CHECK constraints reject a negative price and a bogus payment method, that every query returns its hand-verified result, and that the model stays normalised (no ingredient column on menu_items).

$ python -m pytest -q
......................                                                   [100%]
22 passed in 0.10s