Đi từ danh từ trong nghiệp vụ → bảng, rồi chốt quan hệ và ràng buộc.
Bảng lõi cho đơn hàng:
users(id, email UNIQUE, ...)
products(id, name, price, stock)
orders(id, user_id FK, status, total_amount, created_at)
order_items(order_id FK, product_id FK, quantity, unit_price,
PRIMARY KEY (order_id, product_id))Những quyết định cần nói được:
- Quan hệ n–n tách bảng nối: một đơn nhiều sản phẩm, một sản phẩm ở nhiều đơn → order_items.
- Snapshot giá tại thời điểm mua (unit_price trong order_items): giá sản phẩm đổi sau này không được làm sai đơn cũ — đừng JOIN sang products.price để tính lại tiền.
- status là state machine (pending → paid → shipped / cancelled): bước chuyển hợp lệ do code kiểm soát; cần audit thì thêm bảng order_status_history.
- Ràng buộc để DB tự canh: FK, CHECK (quantity > 0), unique key chống trùng (vd idempotency key trên orders).
- Index theo truy vấn thật: orders(user_id, created_at DESC) cho "đơn của tôi"; order_items(product_id) cho thống kê bán chạy.
Riêng đặt vé (chỗ ngồi hữu hạn) thêm hai điểm:
- Mỗi ghế của một suất chiếu/chuyến là một hàng riêng (show_seats); bảng bookings giữ chỗ có expires_at — giữ 10 phút chưa thanh toán thì nhả.
- Chống hai người lấy cùng ghế: unique constraint (show_id, seat_id) trên booking còn hiệu lực + claim ghế bằng UPDATE ... WHERE atomic (cùng cơ chế với bài chống oversell).
Bẫy hay gặp: chỉ lưu tổng tiền mà không lưu đơn giá từng dòng; nhét items thành JSON trong orders (mất khả năng query và ràng buộc); quên TTL giữ chỗ khiến ghế bị khóa vĩnh viễn.