Vì đơn hàng là bản ghi lịch sử, còn bảng products là dữ liệu đang sống.
Nếu chỉ giữ product_id rồi join sang products để lấy giá, thì hôm sau shop đổi giá hoặc đổi tên sản phẩm, hoá đơn cũ sẽ hiển thị sai — tổng tiền in ra không khớp số tiền khách đã trả.
sql
create table order_items (
id bigserial primary key,
order_id bigint not null references orders(id),
product_id bigint references products(id), -- traceability only
product_name text not null, -- snapshot
unit_price numeric(12,2) not null, -- snapshot
quantity int not null check (quantity > 0),
line_total numeric(12,2) not null
);Những cột nên snapshot: tên, SKU, đơn giá, thuế suất, mức giảm giá, và cả địa chỉ giao hàng ở bảng orders.
Đây là ngoại lệ có chủ ý với chuẩn hoá: dữ liệu trùng lặp ở đây không phải lỗi thiết kế, vì nó mô tả một sự kiện đã xảy ra, không phải trạng thái hiện tại. Cũng vì vậy product_id nên để on delete set null chứ không cascade — xoá sản phẩm không được phép xoá lịch sử bán hàng.