Why MySQL’s JSON Type with Functional Indexes Is a Practical Choice for Modern Apps
Storing semi‑structured data in MySQL used to require messy EAV tables; the native JSON type plus functional indexes give you fast, normalized queries.
23 Jul 2026, 21:20 UTC

Problem: Relational tables become unwieldy with semi‑structured data
Many applications need to store data that does not fit a fixed column layout – for example, user preferences, configuration blobs, or event logs. When these pieces live in a traditional table, developers either create many nullable columns (EAV) or maintain separate tables, both of which add complexity and risk data inconsistency.
Thesis: MySQL’s native JSON type together with generated functional indexes lets you keep data normalized while querying it efficiently.
The JSON data type stores the whole document in a single column, preserving the original structure. A virtual generated column extracts a searchable key (such as a status field) and an index on that column makes the query sargable, so the optimizer can use an index scan instead of a full table scan.
Section 1 – Data modeling
Start by adding a JSON column to the table. Then create a virtual generated column that extracts the value you need to filter on, and finally add an index on that generated column.
- Add the JSON column (no NOT NULL unless you require it).
- Define a virtual generated column, e.g.
status_key VARCHAR(20) AS (JSON_UNQUOTE(JSON_EXTRACT(payload, '$.status'))) VIRTUAL. - Create an index on the generated column:
CREATE INDEX idx_status ON your_table(status_key);
All of these statements require the ALTER TABLE privilege and are supported from MySQL 8.0 onward.
Section 2 – Worked example
Assume a table orders with columns id INT PRIMARY KEY and payload JSON. Insert a row that contains a status field:
INSERT INTO orders (id, payload) VALUES (1, '{"order_id":1001,"status":"shipped","total":45.99}');
To find all shipped orders, query the generated column:
SELECT id FROM orders WHERE status_key = 'shipped';
The status_key column is a virtual generated column, so the WHERE clause references the extracted value directly.
Section 3 – Verification and diagnostics
- Confirm the MySQL version:
SELECT VERSION();– you need 8.0 or newer. - Inspect the table definition to verify the JSON column and generated column exist:
SHOW CREATE TABLE orders;. - Run an EXPLAIN on the query to ensure the index is used:
EXPLAIN SELECT id FROM orders WHERE status_key = 'shipped';. Look for "type: ref" or "type: range" onidx_status.
If the EXPLAIN shows a full table scan, the index may not be applicable (e.g., you queried a different JSON path that is not covered by the generated column).
Trade‑offs and limitations
While the approach is powerful, it has constraints. MySQL 8.0+ is required; older versions lack JSON functions and functional indexes. Very large JSON documents can exceed the row size limit (65,535 bytes in InnoDB) and may degrade performance if not indexed properly. Also, the index is only usable when the query filters on the exact generated column – arbitrary JSON paths are not sargable, so complex filters may still require full scans.
Actionable closing
If your workload involves semi‑structured data that changes frequently, adopt the JSON type with a focused functional index. Start by adding the JSON column, create a virtual generated column for the key you filter on most often, and index that column. Verify the version, check the table definition, and always run EXPLAIN to confirm index usage. This pattern gives you the flexibility of JSON without sacrificing the performance benefits of relational indexing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.