Thiết kế hệ thống SPU - SKU theo mô hình EAV

  • July 24, 2026
  • 10

Nếu bạn từng tự hỏi "Shopee, Tiki, Lazada làm sao quản lý được hàng triệu sản phẩm thuộc đủ loại ngành hàng khác nhau — từ áo thun có màu/size, đến điện thoại có RAM/ROM, đến sách chỉ có tác giả/NXB — mà không cần sửa database mỗi khi thêm ngành hàng mới?" — câu trả lời nằm ở mô hình SPU/SKU kết hợp EAV. Đây là bài toán thiết kế database kinh điển và cũng dễ làm sai nhất khi mới bắt đầu xây hệ thống e-commerce.


1️⃣ SPU và SKU là gì? Phân biệt cho rõ trước khi thiết kế

SPU (Standard Product Unit) — sản phẩm ở mức khái niệm, đại diện cho thứ khách hàng nhìn thấy đầu tiên trên trang danh sách.

SKU (Stock Keeping Unit) — biến thể cụ thể có thể bán, có giá riêng, tồn kho riêng, mã vạch riêng.

SPU: "Áo thun basic cotton"
 ├── SKU: Đen - Size M   (giá 150.000đ, còn 20)
 ├── SKU: Đen - Size L   (giá 150.000đ, còn 15)
 ├── SKU: Trắng - Size M (giá 150.000đ, còn 30)
 └── SKU: Trắng - Size L (giá 155.000đ, còn 8)

💡 Một SPU luôn có ít nhất 1 SKU. Sản phẩm không có biến thể (VD: 1 cuốn sách) vẫn có đúng 1 SKU tương ứng — SPU và SKU không loại trừ nhau, SKU luôn là "con" của SPU.

2️⃣ Vì sao KHÔNG gộp chung vào 1 bảng products?

Đây là câu hỏi đầu tiên ai cũng đặt ra. Nếu gộp chung, mỗi biến thể là 1 dòng độc lập trong bảng products — nghe đơn giản, nhưng gây ra hàng loạt vấn đề thực tế:

Vấn đề Gộp chung 1 bảng Tách SPU/SKU
Trang danh sách sản phẩm Bị trùng lặp — mỗi biến thể hiện 1 dòng Chỉ hiện 1 SPU, chọn biến thể sau
Giá / tồn kho riêng từng biến thể Khó tách bạch SKU độc lập hoàn toàn
Review, Q&A, SEO Bị phân tán theo từng biến thể Gom về đúng 1 SPU
Thêm ngành hàng mới với thuộc tính khác Phải sửa cấu trúc bảng Chỉ cần thêm dữ liệu, không đổi schema

3️⃣ Vấn đề cốt lõi: mỗi ngành hàng có bộ thuộc tính hoàn toàn khác nhau

Đây là lúc bài toán trở nên thú vị. Thử hình dung cách tiếp cận "ngây thơ" — thiết kế bảng cứng với cột cố định:

CREATE TABLE products (
    id INT,
    name VARCHAR(255),
    color VARCHAR(50),     -- chỉ thời trang mới cần
    size VARCHAR(50),      -- chỉ thời trang, giày dép mới cần
    ram VARCHAR(50),       -- chỉ điện thoại, laptop mới cần
    material VARCHAR(50),  -- chỉ nội thất, thời trang mới cần
    author VARCHAR(255)    -- chỉ sách mới cần
);

Vấn đề: bảng phình to với hàng trăm cột, đa số NULL tuỳ ngành hàng. Tệ hơn, mỗi lần sàn mở thêm 1 ngành hàng mới (VD: "Giày dép" cần thêm thuộc tính "Size chân"), bạn phải ALTER TABLE — rủi ro downtime, không thể mở rộng vô hạn theo kiểu này. Đây chính là lúc mô hình EAV phát huy tác dụng.

4️⃣ EAV (Entity - Attribute - Value) là gì?

Thay vì mỗi thuộc tính là 1 cột cố định, EAV lưu mỗi thuộc tính là 1 dòng với 3 thành phần:

  • Entity (thực thể) — cái gì đang được mô tả → sku_id
  • Attribute (thuộc tính) — loại thuộc tính → "Màu sắc"
  • Value (giá trị) — giá trị cụ thể → "Đen"
sku_id | attribute_id | attribute_value_id
101    | 1 (Màu)      | 5  (Đen)
101    | 2 (Size)     | 12 (M)
102    | 1 (Màu)      | 5  (Đen)
102    | 2 (Size)     | 13 (L)

Với cấu trúc này, thêm 1 thuộc tính mới cho ngành hàng mới (VD: "Dung lượng pin" cho điện thoại) chỉ cần INSERT thêm 1 dòng vào bảng attributeskhông cần đổi schema database. Đây là lý do mọi sàn TMĐT lớn đều dùng biến thể của EAV cho phần thuộc tính sản phẩm.

5️⃣ Schema đầy đủ — từng bảng và mục đích của nó

-- 1. Danh mục sản phẩm (cây phân cấp)
CREATE TABLE categories (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    parent_id BIGINT NULL,
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    FOREIGN KEY (parent_id) REFERENCES categories(id)
);

-- 2. SPU - sản phẩm gốc, chứa thông tin dùng chung
CREATE TABLE spus (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    category_id BIGINT NOT NULL,
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    description TEXT,
    status ENUM('draft','active','inactive') DEFAULT 'draft',
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- 3. Định nghĩa các "trục" thuộc tính có thể tồn tại
CREATE TABLE attributes (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,          -- "Màu sắc", "Kích cỡ"
    type ENUM('variant','specification') DEFAULT 'variant'
);

-- 4. Giá trị cụ thể của từng thuộc tính (tái sử dụng được)
CREATE TABLE attribute_values (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    attribute_id BIGINT NOT NULL,
    value VARCHAR(255) NOT NULL,         -- "Đen", "M", "128GB"
    FOREIGN KEY (attribute_id) REFERENCES attributes(id)
);

-- 5. SPU này dùng những attribute nào
CREATE TABLE spu_attributes (
    spu_id BIGINT NOT NULL,
    attribute_id BIGINT NOT NULL,
    PRIMARY KEY (spu_id, attribute_id)
);

-- 6. SKU - biến thể cụ thể, đơn vị giao dịch thực tế
CREATE TABLE skus (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    spu_id BIGINT NOT NULL,
    sku_code VARCHAR(100) UNIQUE NOT NULL,
    price DECIMAL(15,2) NOT NULL,
    stock_quantity INT DEFAULT 0,
    FOREIGN KEY (spu_id) REFERENCES spus(id)
);

-- 7. Tổ hợp thuộc tính của từng SKU — trái tim của EAV
CREATE TABLE sku_attribute_values (
    sku_id BIGINT NOT NULL,
    attribute_value_id BIGINT NOT NULL,
    PRIMARY KEY (sku_id, attribute_value_id)
);
erDiagram
    CATEGORIES ||--o{ CATEGORIES : "danh mục cha - con"
    CATEGORIES ||--o{ PRODUCTS : "chứa"
    PRODUCTS ||--o{ PRODUCT_VARIANTS : "có nhiều biến thể"
    PRODUCTS ||--o{ PRODUCT_ATTRIBUTES : "dung thuoc tinh nao"
    PRODUCTS ||--o{ PRODUCT_SPECIFICATIONS : "thong so"
    ATTRIBUTES ||--o{ PRODUCT_ATTRIBUTES : "duoc gan vao SPU"
    ATTRIBUTES ||--o{ ATTRIBUTE_VALUES : "co cac gia tri cu the"
    ATTRIBUTES ||--o{ PRODUCT_SPECIFICATIONS : "dung lam loai thong so"
    ATTRIBUTE_VALUES ||--o{ VARIANT_ATTRIBUTE_VALUES : "gia tri cu the (Den, M...) duoc bien the chon"
    PRODUCT_VARIANTS ||--o{ VARIANT_ATTRIBUTE_VALUES : "co to hop gia tri thuoc tinh"

    CATEGORIES {
        bigint id PK
        bigint parent_id FK
        varchar name
        varchar slug
    }

    PRODUCTS {
        bigint id PK
        bigint category_id FK
        varchar name
        varchar slug
        text description
        enum status
    }

    ATTRIBUTES {
        bigint id PK
        varchar name
        enum type "variant hoac specification"
    }

    ATTRIBUTE_VALUES {
        bigint id PK
        bigint attribute_id FK
        varchar value
    }

    PRODUCT_ATTRIBUTES {
        bigint product_id FK
        bigint attribute_id FK
    }

    PRODUCT_VARIANTS {
        bigint id PK
        bigint product_id FK
        varchar variant_code
        decimal price
        decimal sale_price
        int stock_quantity
        varchar barcode
        varchar image_url
        enum status
    }

    VARIANT_ATTRIBUTE_VALUES {
        bigint product_variant_id FK
        bigint attribute_value_id FK
    }

    PRODUCT_SPECIFICATIONS {
        bigint product_id FK
        bigint attribute_id FK
        varchar value
    }

📌 Vì sao tách attributesattribute_values thành 2 bảng riêng?

Vì 1 attribute (Màu sắc) có nhiều giá trị (Đen, Trắng, Xanh...) dùng lại được cho hàng nghìn SPU khác nhau. Nếu không tách, mỗi SPU phải tự định nghĩa lại "Đen" là gì — vừa trùng lặp dữ liệu, vừa không filter được kiểu "lọc tất cả sản phẩm màu Đen trên toàn sàn".

📌 Cột type trong bảng attributes để làm gì?

Đây là điểm rất dễ bị bỏ sót nhưng cực kỳ quan trọng về nghiệp vụ. Có 2 loại thuộc tính khác nhau hoàn toàn:

  • variant — tạo ra SKU riêng biệt (Màu, Size), khách phải chọn trước khi mua
  • specification — chỉ mô tả thêm (Chất liệu, Xuất xứ), khách không cần chọn, chỉ đọc thông tin

Nếu gộp chung 2 loại này, hệ thống sẽ tự sinh SKU cho cả "Chất liệu" — sai hoàn toàn về nghiệp vụ, vì áo thun không nên có 3 SKU chỉ vì có 3 dòng mô tả chất liệu khác nhau.

6️⃣ Ưu và nhược điểm của EAV — cần cân nhắc cả 2 mặt

✅ Ưu điểm

  • Thêm thuộc tính mới không cần đổi schema — chỉ INSERT
  • 1 hệ thống database phục vụ được vô hạn ngành hàng khác nhau
  • Tái sử dụng giá trị thuộc tính trên toàn hệ thống, dễ filter chéo ngành hàng

⚠️ Nhược điểm — nhiều người bỏ qua khi chọn EAV

Nhược điểm Chi tiết
Query phức tạp Tìm "SKU màu Đen VÀ size M" cần self-join nhiều lần vào cùng 1 bảng trung gian
Không có type-safety ở DB Giá trị lưu dạng string nên mất khả năng so sánh số (128GB > 64GB)
Khó validate ở DB level Không dùng được CHECK constraint để giới hạn giá trị hợp lệ
Filter nhiều điều kiện chậm dần Lọc theo 4-5 thuộc tính cùng lúc cần join 4-5 lần, hiệu năng giảm theo số điều kiện
Khó làm báo cáo/BI Dữ liệu dạng dọc (long format) khó pivot so với dạng ngang (wide format)

7️⃣ Cách khắc phục nhược điểm EAV trong thực tế — Hybrid EAV

Các hệ thống lớn hiếm khi dùng EAV thuần túy. Cách tiếp cận thực dụng nhất là EAV lai (Hybrid EAV), kết hợp 3 kỹ thuật sau:

🔹 Kỹ thuật 1: JSON Snapshot song song với bảng EAV chuẩn hoá

Giữ bảng sku_attribute_values làm nguồn dữ liệu chuẩn (để admin quản lý, validate), đồng thời đồng bộ sang cột JSON để đọc nhanh, không cần JOIN:

ALTER TABLE skus ADD COLUMN attributes_snapshot JSON;
-- {"color": "Đen", "size": "M"}

-- PostgreSQL: index GIN cho phép filter nhanh không cần JOIN
CREATE INDEX idx_sku_attrs ON skus USING GIN (attributes_snapshot);
SELECT * FROM skus WHERE attributes_snapshot @> '{"color": "Đen"}';

🔹 Kỹ thuật 2: Đẩy phần search/filter phức tạp sang Search Engine riêng

Các sàn lớn không filter trực tiếp trên EAV trong SQL, mà đẩy dữ liệu sang Elasticsearch — nơi mỗi SKU được "làm phẳng" (denormalize) thành 1 document với đầy đủ thuộc tính dạng field, filter nhanh nhờ inverted index. Database quan hệ chỉ đóng vai trò nguồn sự thật, còn filter/search thực tế chạy trên search engine.

🔹 Kỹ thuật 3: Chỉ EAV hoá phần thực sự biến động, giữ cột cứng cho phần cố định

Các trường mọi SKU đều có và cần type-safety (price, stock_quantity) nên giữ là cột cứng trong bảng skus, không đưa vào EAV. Chỉ những thuộc tính biến đổi theo ngành hàng (màu, size, RAM...) mới cần EAV. Đây là nguyên tắc quan trọng nhất: EAV không phải giải pháp cho mọi trường dữ liệu, chỉ dành cho phần thực sự động.

8️⃣ Luồng nghiệp vụ thực tế khi tạo sản phẩm

  1. Admin tạo SPU (tên, category, mô tả, ảnh chung)
  2. Chọn các attribute variant áp dụng (VD: Màu, Size)
  3. Chọn giá trị cho từng attribute (Đen/Trắng/Xanh, S/M/L)
  4. Hệ thống tự sinh tổ hợp (Cartesian product) → tạo sẵn danh sách SKU nháp
  5. Admin điền giá, tồn kho riêng cho từng SKU (hoặc bulk update)
  6. Khách xem SPU → chọn thuộc tính → hệ thống tìm đúng SKU tương ứng (qua attributes_snapshot hoặc bảng trung gian) → hiển thị giá/tồn/ảnh của SKU đó

🎯 Tổng kết

Thiết kế SPU/SKU với EAV giải quyết đúng bài toán cốt lõi của e-commerce: làm sao 1 schema database phục vụ được vô số ngành hàng với bộ thuộc tính hoàn toàn khác nhau, mà không phải sửa cấu trúc bảng mỗi khi mở rộng. Nhưng EAV không miễn phí — cái giá phải trả là độ phức tạp khi query và performance khi filter nhiều điều kiện.

Nguyên tắc thực chiến quan trọng nhất khi áp dụng:

  • Tách rõ SPU (khái niệm) và SKU (đơn vị giao dịch thực tế) — mọi giao dịch, tồn kho, giỏ hàng luôn tham chiếu sku_id
  • Phân biệt rõ thuộc tính variant (tạo SKU) và specification (chỉ mô tả)
  • Không dùng EAV thuần túy — kết hợp JSON snapshot và search engine để bù lại nhược điểm hiệu năng
  • Chỉ EAV hoá phần thực sự biến động theo ngành hàng, giữ cột cứng cho phần cố định

Nắm vững những nguyên tắc này, bạn sẽ tự tin thiết kế được hệ thống catalog sản phẩm mở rộng tốt, không phải "đập đi làm lại" khi sàn phát triển thêm ngành hàng mới.


Bạn đang thiết kế catalog cho ngành hàng nào? Để lại comment, mình cùng bàn cách áp dụng EAV cho case cụ thể của bạn! 🚀