MySQL JSON Data Type: Lưu Dữ Liệu Linh Hoạt Không Cần Thay Schema

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Câu chuyện bắt đầu từ một dự án e-commerce mình làm cách đây vài năm. Hệ thống cần lưu “thuộc tính” cho từng loại sản phẩm: áo thì có size, màu sắc, chất liệu — giày thì có số, màu, loại đế. Mỗi loại sản phẩm lại khác nhau hoàn toàn.

Giải pháp lúc đó là EAV (Entity-Attribute-Value) — tạo bảng product_attributes với 3 cột: entity_id, attribute_name, value. Hoạt động được, nhưng mỗi lần query phải JOIN 5–6 lần, EXPLAIN chạy ra execution plan nhìn muốn khóc. Từ MySQL 5.7.8, native JSON data type xuất hiện — và đó là thứ mình ước gì biết sớm hơn.

JSON Data Type là gì, và tại sao không dùng TEXT?

Câu hỏi đầu tiên ai cũng hỏi: “Lưu TEXT rồi parse ở tầng application cũng được mà?” — Được thật. Nhưng TEXT và JSON không phải hai cái giống nhau. MySQL JSON type có ba thứ TEXT không thể làm:

  • Validation tự động: MySQL từ chối INSERT nếu chuỗi không phải JSON hợp lệ. TEXT thì im lặng chấp nhận rác vào database.
  • Binary storage: Dữ liệu được lưu dạng binary tối ưu, truy cập field con nhanh hơn — không phải parse cả chuỗi như TEXT.
  • Native functions: JSON_EXTRACT, JSON_SET, JSON_CONTAINS… dùng thẳng trong SQL. Không cần đẩy xử lý lên application layer.

Kích thước tối đa bị giới hạn bởi max_allowed_packet — mặc định 16MB. Trong thực tế hầu hết dự án, giới hạn này không bao giờ đụng tới.

Tạo Bảng và Insert Dữ Liệu

Tạo bảng products với cột attributes kiểu JSON:

CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price DECIMAL(10, 2),
    attributes JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Cú pháp insert không khác gì lưu chuỗi thường — chỉ cần đảm bảo đó là JSON hợp lệ:

INSERT INTO products (name, price, attributes) VALUES
(
    'Áo thun Basic',
    299000,
    '{"size": ["S", "M", "L", "XL"], "color": "trắng", "material": "cotton", "in_stock": true}'
),
(
    'Giày Running Pro',
    1850000,
    '{"size": [39, 40, 41, 42, 43], "color": "đen", "waterproof": false, "weight_g": 320}'
);

Thử insert JSON sai cú pháp — MySQL reject ngay, không chờ tới tầng application:

-- Dòng này sẽ bị reject:
INSERT INTO products (name, attributes) VALUES ('Test', '{invalid json}');
-- ERROR 3140: Invalid JSON text at position 1...

Thực Hành: Query Dữ Liệu JSON

Đọc field con bằng -> và ->>

Hai operator này là thứ mình đụng nhiều nhất mỗi ngày:

-- -> trả về JSON value (string có dấu ngoặc kép)
SELECT name, attributes->'$.color' AS color FROM products;
-- Kết quả: "trắng"

-- ->> trả về plain text (unquote string)
SELECT name, attributes->>'$.color' AS color FROM products;
-- Kết quả: trắng

-- Truy cập array element (index bắt đầu từ 0)
SELECT name, attributes->>'$.size[0]' AS smallest_size FROM products;

Lọc theo điều kiện JSON

-- Tìm sản phẩm làm từ cotton
SELECT id, name, price
FROM products
WHERE attributes->>'$.material' = 'cotton';

-- JSON_CONTAINS: kiểm tra array có chứa giá trị không
SELECT id, name
FROM products
WHERE JSON_CONTAINS(attributes->'$.size', '"M"');

-- Kết hợp nhiều điều kiện
SELECT id, name, price
FROM products
WHERE JSON_EXTRACT(attributes, '$.in_stock') = true
  AND price < 500000;

Update giá trị trong JSON

-- JSON_SET: thêm hoặc cập nhật field
UPDATE products
SET attributes = JSON_SET(attributes, '$.discount_pct', 15)
WHERE id = 1;

-- JSON_REMOVE: xóa field
UPDATE products
SET attributes = JSON_REMOVE(attributes, '$.discount_pct')
WHERE id = 1;

-- JSON_MERGE_PATCH: merge object (ghi đè key trùng)
UPDATE products
SET attributes = JSON_MERGE_PATCH(attributes, '{"new_arrival": true, "season": "summer"}')
WHERE id = 2;

Performance và Index — Chỗ Dễ Bị Sai Nhất

MySQL không thể đánh index trực tiếp lên cột JSON. Đây là cái bẫy kinh điển — bảng vài trăm nghìn row, query theo JSON field, execution time nhảy lên 2–3 giây mà nhìn code không thấy vấn đề gì. Chạy EXPLAIN mới lộ ra: “Full table scan”.

Workaround gọn nhất là dùng Generated Column: tạo một cột ảo sinh ra tự động từ JSON field, rồi đánh index lên cột đó:

-- Thêm virtual generated column từ JSON field
ALTER TABLE products
ADD COLUMN material VARCHAR(50)
    GENERATED ALWAYS AS (attributes->>'$.material') VIRTUAL;

-- Đánh index lên generated column
CREATE INDEX idx_material ON products (material);

-- Từ giờ query này dùng index thay vì full scan
SELECT id, name FROM products WHERE material = 'cotton';
-- EXPLAIN sẽ thấy "Using index" thay vì "Full table scan"

STORED thay vì VIRTUAL thì giá trị được lưu vật lý xuống disk — tốc độ đọc nhanh hơn một chút, đổi lấy thêm storage. Với boolean field hay dùng để filter:

ALTER TABLE products
ADD COLUMN in_stock BOOLEAN
    GENERATED ALWAYS AS (JSON_EXTRACT(attributes, '$.in_stock')) STORED,
ADD INDEX idx_in_stock (in_stock);

JSON_TABLE — Flatten Array JSON Thành Nhiều Row (MySQL 8.0+)

Đôi khi cần “unfold” một array JSON thành nhiều row để xử lý kiểu tabular. JSON_TABLE làm được việc đó ngay trong SQL, không cần kéo data lên application rồi loop:

-- Liệt kê từng size của từng sản phẩm thành row riêng
SELECT
    p.id,
    p.name,
    s.size_value
FROM products p,
JSON_TABLE(
    p.attributes,
    '$.size[*]'
    COLUMNS (size_value VARCHAR(10) PATH '$')
) AS s
WHERE p.id = 1;

Kết quả:

+----+---------------+------------+
| id | name          | size_value |
+----+---------------+------------+
|  1 | Áo thun Basic | S          |
|  1 | Áo thun Basic | M          |
|  1 | Áo thun Basic | L          |
|  1 | Áo thun Basic | XL         |
+----+---------------+------------+

Khi Nào Nên Dùng, Khi Nào Không?

Sau vài lần triển khai thực tế, mình rút ra được ranh giới khá rõ. JSON column hợp với:

  • Schema linh hoạt, thay đổi thường xuyên: attributes sản phẩm, metadata, config per-user, feature flags
  • Dữ liệu thường đọc cả object: ít khi cần filter theo từng field con
  • Prototype nhanh: chưa chắc chắn schema, muốn iterate mà không phải migration mỗi lần

Không nên dùng khi:

  • Cần query phức tạp theo nhiều field trong JSON thường xuyên — normalize ra bảng riêng tốt hơn, SQL rõ ràng hơn
  • Dữ liệu cần foreign key constraint — JSON column không hỗ trợ
  • Team chưa quen MySQL JSON functions — dễ viết query không dùng index mà không hay

Kết Luận

MySQL JSON type không phải cây đũa thần, nhưng đúng use case thì nó giải quyết bài toán schema linh hoạt rất gọn — không cần EAV pattern rắc rối, không cần extension thêm. Pattern mình hay dùng: normalize các field quan trọng (hay filter, hay sort) ra cột riêng có index; metadata phụ thì để trong JSON column.

MySQL 5.7+ và 8.0 đều có JSON type sẵn, không cần cài thêm gì. Mở terminal lên, tạo một bảng test, chạy qua các ví dụ trên — cảm giác khác hẳn so với parse JSON ở tầng application rồi mới biết field nào có, field nào không.

Share: