← Return to Network
MySQL Performance Backend

You're Paying a Huge Performance Tax on Your JSON Columns. Here's the One-Line Fix.

You stored it in JSON for flexibility. That flexibility is now costing you full table scans on every filter. Virtual columns are the escape hatch you didn't know existed.


You know the moment. The product manager is changing requirements every other day, and you think: "I'll just throw this into a JSON column for now and clean it up later."

Later never comes. The JSON column stays. The table grows to 5 million rows. And then one morning, your monitoring dashboard turns red.

The query that filtered by a JSON key just timed out in production. Your DBA is asking questions you don't have answers to.

This is The JSON Trap. And almost every backend engineer walks into it at least once.

The problem

MySQL is completely blind to what's inside your JSON

Consider this classic setup — a products table with a flexible attributes column:

CREATE TABLE products (
    id   INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255),
    attributes JSON
);

A few weeks pass. Marketing wants product filtering by color. You write this:

SELECT * FROM products
WHERE attributes->>'$.color' = 'matte_black';

Works perfectly on localhost with 100 rows. On production with 5 million rows? Run EXPLAIN and brace yourself:

Before
type: ALL
Full table scan — every row, every time
After (what we'll fix)
type: ref
Index hit — sub-millisecond lookup

type: ALL means MySQL is opening every single row, deserializing the JSON, extracting the key, and comparing it. You cannot put a standard B-Tree index on a JSON key path. The database is flying blind on every filter.

The fix

Virtual columns: zero disk cost, full index power

A Virtual Generated Column is MySQL's answer to this exact problem. It doesn't store anything on disk. It exposes a computed value — derived from an expression — as if it were a real column. And crucially: you can index it.

Step 1 — expose the JSON key as a virtual column:

ALTER TABLE products
ADD COLUMN product_color VARCHAR(50)
GENERATED ALWAYS AS (attributes->>'$.color') VIRTUAL;

Step 2 — index it:

ALTER TABLE products ADD INDEX idx_product_color (product_color);

That's it. Now run your original query:

SELECT * FROM products
WHERE product_color = 'matte_black';

EXPLAIN now shows type: ref. MySQL goes straight to the B-Tree index. It never touches the JSON payload during the search. You get schema-less flexibility with relational performance.

Under the hood

The connection to functional indexes (this will click something)

If you've read about MySQL 8.0's functional indexes — like INDEX ((LOWER(email))) — this pattern should look familiar. That's not a coincidence.

MySQL implements functional indexes by silently creating a hidden virtual column and indexing it. What you're doing here manually is exactly what MySQL does under the hood for functional indexes. Same mechanism, full control.

Battle-tested rules

Three rules before you go index everything

1
Always use VIRTUAL, not STORED. STORED columns are written to disk on every INSERT and UPDATE. For JSON key extraction — which is cheap CPU work — VIRTUAL is free at write time and zero disk overhead.
2
If you're indexing it, ask if it should be JSON at all. A virtual column + index on a JSON key you filter 90% of the time is a code smell. Promote it to a real column. JSON is for sparse, truly dynamic attributes — not for your core domain fields.
3
This is a bridge, not a destination. Virtual columns buy you time. Use them to ship fast, then schedule the proper migration when query patterns are proven and stable.
Takeaway

JSON columns are a productivity tool — not a performance strategy.

Virtual columns let you have both: the flexibility of schema-less storage for dynamic attributes, and B-Tree index performance for everything you actually query on.

Two ALTER TABLE statements. Zero disk overhead. Full table scan eliminated.

If this was useful, let's connect. I write about engineering leadership, backend architecture, and lessons from building products at scale.

Follow on LinkedIn

Stay in the loop

New posts on engineering leadership, backend performance, and building at scale — straight to your inbox. No spam.

Discussion