Bài thiết kế kinh điển — lõi là cặp bảng orders + order_items (quan hệ user–product là nhiều–nhiều qua đơn hàng):
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending', -- pending → paid → shipped → completed / cancelled
total_amount NUMERIC(12, 2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INT NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(12, 2) NOT NULL, -- snapshot giá tại thời điểm mua
PRIMARY KEY (order_id, product_id)
);Các quyết định phải giải thích được:
- unit_price snapshot: giá lưu ngay trong order_items, KHÔNG join sang products.price — giá sản phẩm đổi sau này không được làm sai lịch sử đơn. Tương tự nên snapshot địa chỉ giao hàng.
- Tiền dùng NUMERIC, không float (sai số tích lũy).
- status là state machine: chỉ cho chuyển trạng thái hợp lệ (pending → paid → shipped...; cancelled từ pending/paid) — enforce ở tầng app hoặc CHECK/trigger. Đơn đã hủy giữ nguyên hàng, không xóa.
- order_items là bảng nối có thuộc tính (quantity, unit_price); PK ghép (order_id, product_id) chặn trùng sản phẩm trong một đơn.
- Index: FK orders.user_id (lịch sử mua của user), orders.status hoặc partial index cho đơn đang xử lý.
- total_amount là giá trị denormalize từ items — tính khi tạo đơn trong cùng transaction, chấp nhận để đọc nhanh.
Mở rộng hay được hỏi tiếp: thanh toán tách bảng payments (một đơn nhiều lần thanh toán/refund), trừ tồn kho chống race condition, và mã giảm giá.