MariaDB JSON vs Postgres JSONB: What You Actually Lose
I've run both in production. MariaDB's JSON support is not what Postgres JSONB is, and the gap matters more than MariaDB's docs let on.
I keep running into developers who assume MariaDB's JSON column support is roughly equivalent to Postgres JSONB. It is not. The marketing language is similar, the SQL syntax looks similar, but under the hood they're solving the problem at completely different levels of seriousness. After running both in production — MariaDB for a handful of legacy client apps I inherited, Postgres for most of what I've built at NWOS over the last several years — I have opinions.
Let me be direct about the conclusion up front: for most CRUD apps, the gap won't kill you. But if you ever need to query inside JSON at any scale, or you rely on indexing JSON paths, or you care about storage efficiency, Postgres wins and it's not close.
What Problem Storing JSON in a Relational DB Actually Solves
The pitch is that sometimes your data is genuinely semi-structured. Not "we didn't want to design a schema" semi-structured — I mean legitimately variable. I work with a biotech client whose instrument data has a core set of fields but then a metadata blob that varies by instrument type, firmware version, and run configuration. Normalizing that into relational tables would require a new migration every time they add an instrument. Storing it as JSON in a metadata column is the right call.
Same story with a print management client I have — job tickets have a fixed header, but the production options object is different for digital, offset, wide-format, and finishing. JSON in a column is reasonable. The question is how well each database lets you work with that data once it's in there.
The Core Difference: Stored Text vs. Binary Format
This is the thing MariaDB's documentation soft-pedals. In Postgres, JSONB is stored in a decomposed binary format. It's parsed once on insert, and after that the database can operate on the structure directly — look up keys, traverse paths, evaluate expressions — without re-parsing the raw string every time.
MariaDB's JSON column is, under the covers, a LONGTEXT with a CHECK constraint that validates the JSON on insert. That's it. The data is stored as text. Every time you reach into it with JSON_EXTRACT() or -> shorthand, MariaDB parses the string from scratch.
You can verify this yourself:
-- MariaDB
SHOW CREATE TABLE jobs\G
-- You'll see: `options` longtext CHARACTER SET utf8mb4 ...
-- with a CHECK (json_valid(`options`))
-- Postgres
\d jobs
-- options | jsonb | ...
-- It's a real type, not a dressed-up text column
That distinction has real consequences.
Indexing: Where MariaDB Falls Apart
Postgres JSONB has GIN indexes. You can index the contents of a JSONB column — every key-value pair in the document — with a single index declaration:
-- Postgres
CREATE INDEX idx_jobs_options ON jobs USING GIN (options);
-- Now this uses the index:
SELECT * FROM jobs WHERE options @> '{"format": "offset"}';
That @> containment operator, backed by a GIN index, is legitimately fast on millions of rows. I've used it on a table with ~4M rows for a print client and query times went from multi-second full scans to single-digit milliseconds.
MariaDB has no equivalent. What it offers is a functional index on a specific extracted path:
-- MariaDB
ALTER TABLE jobs ADD INDEX idx_format ((JSON_VALUE(options, '$.format')));
-- Only works if you're querying exactly that path:
SELECT * FROM jobs WHERE JSON_VALUE(options, '$.format') = 'offset';
That works for known, static paths you've planned ahead for. But the whole point of semi-structured data is often that you don't know all the paths ahead of time. The moment you need to query a new JSON path in MariaDB, you're doing a full table scan until you add a new functional index — and you have to add one per path.
In Laravel, you can abstract both with Eloquent's whereJsonContains, but the generated SQL and the underlying execution plan are completely different:
// Laravel — same call, wildly different performance depending on the database
Job::whereJsonContains('options->format', 'offset')->get();
// On Postgres this generates: options @> '{"format":"offset"}'
// and uses your GIN index.
// On MariaDB this generates: JSON_CONTAINS(options, '"offset"', '$.format')
// Full table scan unless you have a functional index on that exact path.
This caught me off guard the first time I moved a query-heavy feature from a Postgres app to a MariaDB app for a client who insisted on staying on their existing MySQL/MariaDB stack. The Eloquent code looked identical. The query plan was not.
Operators and Functions
Postgres JSONB has a rich set of operators. The containment operators (@>, <@), the existence operators (?, ?|, ?&), path-based access with #> and #>>, and jsonb_path_query for full JSONPath expressions. You can join against a JSONB array with jsonb_array_elements. You can aggregate rows back into JSONB with jsonb_agg.
MariaDB has JSON_EXTRACT, JSON_VALUE, JSON_CONTAINS, JSON_SEARCH, and a handful of others. It's functional for basic operations. But it's more verbose, less composable, and there are gaps.
Example: extracting all values from a JSONB array and filtering on them is clean in Postgres:
-- Postgres: find jobs where any tag in a JSON array matches
SELECT id FROM jobs
WHERE EXISTS (
SELECT 1 FROM jsonb_array_elements_text(options->'tags') t
WHERE t.value = 'rush'
);
-- Or with GIN and @>:
SELECT id FROM jobs WHERE options @> '{"tags": ["rush"]}';
In MariaDB, JSON_SEARCH can do this but the syntax is clunkier and the performance story is the same — no indexing on array contents:
-- MariaDB
SELECT id FROM jobs
WHERE JSON_SEARCH(options, 'one', 'rush', NULL, '$.tags[*]') IS NOT NULL;
Fine for small tables. Starts hurting around 100k rows if you're running this frequently.
Update Operations
Postgres lets you surgically update a JSONB column without rewriting the whole document:
-- Postgres: set a single nested key
UPDATE jobs
SET options = jsonb_set(options, '{shipping,carrier}', '"fedex"')
WHERE id = 42;
MariaDB has JSON_SET, JSON_REPLACE, JSON_REMOVE — similar surface area here, actually. This is one area where MariaDB is more or less on par for basic operations. I'll give it that.
Storage Size
Because MariaDB is storing text, it's storing every character of your JSON including whitespace (if it was inserted with whitespace), key names on every row, all of it. Postgres JSONB strips whitespace on insert, deduplicates nothing in a single document, but stores the parsed structure which is generally more compact for dense objects. For a table with millions of rows and chunky JSON blobs, this adds up. One client's analytics table shrank about 20% when I moved it from MariaDB to Postgres, partly from the binary storage, partly from trimmed whitespace in the original insert pipeline.
When I'd Still Use MariaDB JSON
Honestly — when I don't have a choice. If a client is on a managed hosting stack that's MySQL/MariaDB only, or they have an existing app I'm extending, I'm not going to blow up their infrastructure to get JSONB. I'll use it, I'll be careful about which JSON paths I query, I'll add functional indexes for any high-frequency query paths, and I'll document the limitations.
For small tables — under 50k rows, low query frequency — the performance difference is invisible. A print job record that gets written once and read a handful of times? MariaDB JSON is fine.
I'd also use it when the JSON is truly write-once, read-rarely archival data. Storing a snapshot of an API response for auditing purposes. You don't query inside it, you just want to know what it was. MariaDB JSON works perfectly fine for that.
When I'd Reach for Postgres JSONB
Any time querying inside the JSON is a real use case. Any time the JSON paths aren't fully known up front. Any time the table will grow past six figures of rows and those JSON queries are in hot paths. Any time I'm starting a new project from scratch.
Postgres is my default database for new NWOS projects. Has been for years. JSONB is one of a dozen reasons — window functions, CTEs that actually perform, LISTEN/NOTIFY, native arrays, full text search, partial indexes. The JSON story is just particularly stark because the surface-level APIs look so similar and the production behavior is so different.
The Bottom Line
MariaDB JSON is a validated text column with a parsing API bolted on. Postgres JSONB is a first-class binary type with real indexing, real operators, and a real query planner that understands it. For most apps most of the time, you can live with MariaDB's version. But don't mistake "functional" for "equivalent" — the moment your data or your query patterns grow, you'll feel the gap, and it's a big one.
Need help shipping something like this? Get in touch.