← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

JSON Columns in MySQL & PostgreSQL

database PHP 5.0+ Intermediate
debt(d7/e5/b5/t5)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). Detection hints list mysql-slow-query-log and laravel-debugbar, both runtime tools that only reveal problems after queries execute slowly. There's no static analysis that catches 'you should have normalized this' or 'you're missing a GIN index on this JSON path' — it requires query profiling or performance testing to surface.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix says to add generated columns and indexes, which is a schema migration plus potentially query rewrites. If data was wrongly stored as JSON when it should be normalized, that's extracting JSON fields into proper columns, migrating data, and updating all queries — a significant refactor within the data layer.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). Applies to web/cli/queue contexts broadly. Once JSON columns hold data that should be relational, every future query against that data is slower, every join is awkward, and every developer must learn the JSON path syntax. It's not quite system-defining (b7) but does create ongoing friction across multiple workstreams when misused.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception states JSON columns appear to replace proper schema design, tempting devs to avoid migrations. This is a known gotcha — experienced devs learn it, but it contradicts the initial appeal of 'just store everything in JSON'. The jsonb vs json confusion in PostgreSQL adds another documented trap layer.

About DEBT scoring →

Also Known As

JSONB JSON type JSON column

TL;DR

Native JSON column types allow storing and querying semi-structured data within a relational database — without sacrificing ACID guarantees or the ability to index specific JSON paths.

Explanation

MySQL 5.7+ and PostgreSQL 9.4+ (jsonb) support native JSON storage. PostgreSQL's jsonb stores binary-parsed JSON enabling GIN indexes on any path. MySQL's JSON type provides path operators (->>, JSON_EXTRACT) and generated columns for indexing. JSON columns are useful for variable attributes, configuration, and metadata — but abusing them to avoid schema design produces an unmaintainable document store inside a relational database.

Diagram

flowchart LR
    subgraph MySQL JSON Column
        MJ[JSON column] -->|JSON_EXTRACT| MQ[Query inside JSON]
        MJ -->|Generated column + index| MI[Index on JSON path]
    end
    subgraph PostgreSQL JSONB
        PJ[JSONB column<br/>binary stored] -->|"-> ->>"| PQ[Navigate JSON]
        PJ -->|GIN index| PI[Fast search inside JSON]
        PJ -->|"@>"| CONTAIN[Contains operator]
    end
    subgraph When to Use
        USE[Semi-structured data<br/>variable attributes per row<br/>schema not yet known]
        AVOID[Highly structured data<br/>frequently queried fields<br/>foreign key relations needed]
    end
    style USE fill:#238636,color:#fff
    style AVOID fill:#f85149,color:#fff

Common Misconception

JSON columns replace proper schema design — they are an escape hatch for genuinely variable data, not a substitute for normalised tables when the structure is known.

Why It Matters

Storing all user preferences or metadata as JSON in one column avoids dozens of migration files — but querying and indexing specific JSON fields requires understanding the database's JSON operators.

Common Mistakes

  • Using JSON for data that has a fixed, known structure — a proper column is faster, indexable, and type-safe.
  • Not creating a GIN index (PostgreSQL) or generated column index (MySQL) on frequently queried JSON paths.
  • Querying JSON with LIKE '%value%' — always use the database's native JSON path operators.
  • Mixing jsonb (binary, fast) and json (text, preserves whitespace) in PostgreSQL without understanding the difference.

Code Examples

✗ Vulnerable
-- Querying JSON with LIKE — full table scan, no index:
SELECT * FROM users WHERE metadata LIKE '%"role":"admin"%';

-- MySQL: JSON_EXTRACT without generated column — not indexed:
SELECT * FROM users WHERE JSON_EXTRACT(metadata, '$.role') = 'admin';
✓ Fixed
-- PostgreSQL: GIN index on jsonb + path operator:
CREATE INDEX idx_users_metadata ON users USING GIN (metadata);
SELECT * FROM users WHERE metadata @> '{"role": "admin"}';

-- MySQL: generated column + index:
ALTER TABLE users
    ADD COLUMN role VARCHAR(50) GENERATED ALWAYS AS (metadata->>'$.role') STORED,
    ADD INDEX idx_users_role (role);

Added 15 Mar 2026
Edited 19 Apr 2026
Views 28
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S
No pings yesterday
Amazonbot 8 Perplexity 7 Unknown AI 3 Ahrefs 2 Google 2 Majestic 1
crawler 20 crawler_json 1 pre-tracking 2
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Use JSON columns for truly variable/optional attributes — but add generated columns and indexes for any JSON field you query; never store data in JSON that you need to filter, join, or aggregate
📦 Applies To
PHP 5.0+ web cli queue-worker laravel
🔗 Prerequisites
🔍 Detection Hints
JSON column with JSON_EXTRACT in WHERE clause without generated column index; normalised data stuffed into JSON to avoid schema migration
Auto-detectable: ✗ No mysql-slow-query-log laravel-debugbar
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: Medium Context: File

✓ schema.org compliant