Zero-Downtime Supabase Migrations Using Expand and Contract
When you change a production database in a Supabase project, old and new versions of the code can run at the same…
When a relational database stores records with fields that vary from one record to another, JSON lets it keep those fields together in a document. PostgreSQL calls its query-oriented binary JSON type jsonb; MySQL provides a native JSON type, with no type named JSONB. Both databases convert JSON input into a form they can process, but they index and search it differently.
There is no universal performance winner. PostgreSQL stands out when queries need flexible searches across document keys and values, especially with a GIN index. MySQL fits workloads that query a known set of JSON paths or check membership in JSON arrays, using generated-column or multi-valued indexes. The right comparison depends on the queries, indexes, update patterns, and data distribution you actually use.
PostgreSQL jsonb converts input into a decomposed binary representation. That conversion adds work when the database writes a document, but the database does not need to parse the original text again for later processing. PostgreSQL’s text-preserving json type behaves differently: it stores the submitted text, and JSON operations reparse it. The PostgreSQL JSON documentation notes that jsonb normalizes documents. It discards whitespace and object-key order; when an object repeats a key, it keeps only the last value.
MySQL validates documents written to a native JSON column and converts them to an internal format designed to find nested values by key or array index. MySQL also normalizes JSON, including discarding earlier values when an object repeats a key. These descriptions explain how each database handles JSON. They do not show that either database answers queries faster or uses less storage for the same documents. The MySQL JSON documentation does not provide a matched storage-size comparison with PostgreSQL.
If your code needs to preserve the exact JSON text that was submitted, check how the chosen database type handles it before comparing query speed. For query-focused document storage, test the indexes and queries that production traffic will use.
PostgreSQL offers a broad index for JSONB searches called GIN. A GIN index breaks a document into index entries for its keys and values, which lets queries search document contents without scanning every row. The default jsonb_ops operator class, which determines what searches the index supports, supports key-existence checks, containment searches, and JSONPath matching. A containment search checks whether a document includes specified keys or values. The narrower jsonb_path_ops supports containment and JSONPath matching, but not key-existence checks. PostgreSQL documents that jsonb_path_ops is often smaller and faster for the operators it supports; it does not fit every search. See the PostgreSQL JSONB indexing options.
A broad GIN index is not automatically the best choice for a frequently queried single value, such as a status or customer identifier. An expression index on that value, or a regular typed column, can better support queries that check for one exact value or whether a value falls within a span. GIN also adds write work because one row can create multiple index entries. PostgreSQL’s GIN documentation describes a pending list that holds new index entries temporarily. Searches can slow down when this list grows large. Measure read speed alongside index size, update throughput, and maintenance behavior.
MySQL takes a more targeted approach. A JSON column itself does not receive a conventional index for arbitrary paths. Instead, the database can index a generated column that extracts a chosen path from the JSON, or create an index directly on a compatible expression that extracts a value. This works well when the service filters or sorts on known fields, but it does not offer the same broad document search as a PostgreSQL GIN index. The MySQL index documentation explains how the optimizer can use indexes on extracted values.
For JSON arrays, InnoDB supports multi-valued indexes. A row contributes multiple index entries, one for each indexed array value, and MySQL can use these indexes with predicates such as MEMBER OF(), JSON_CONTAINS(), and JSON_OVERLAPS(). The indexes do not support range scans or index-only scans, and they cannot cover a query. The MySQL CREATE INDEX documentation details those limitations.
A practical starting point is to use targeted B-tree indexes on both databases for stable single-value fields. Test PostgreSQL GIN when queries need flexible containment or key-existence searches. Test MySQL generated or functional indexes for known paths, and multi-valued indexes for array membership.
PostgreSQL converts JSONB input and maintains any indexes that cover the document. A GIN index can create multiple entries for one updated row, so indexing more of a document makes the database do more work when it writes that row. Compare indexed read speed with the costs of inserts and updates.
MySQL can update part of a JSON document in place under specific conditions. The update must use JSON_SET(), JSON_REPLACE(), or JSON_REMOVE() on the same JSON column. It must replace existing values rather than add elements to a parent object or array, and the replacement generally must not exceed the size of the value it replaces. A direct assignment of a new document does not qualify. These restrictions are described in the MySQL JSON update documentation.
Test the kinds of updates your code actually sends. For example, changing an existing scalar and adding a new object property exercise different MySQL update conditions. Compare both with the indexes enabled, since index maintenance also affects write cost.
Published benchmark numbers do not settle the comparison. One 2026 article reports, for a workload with 1 million rows and JSON payloads of about 500 bytes, PostgreSQL JSONB at 28,400 queries per second for field extraction and 9,800 for nested-key filtering. It reports MySQL JSON at 18,200 and 4,100 queries per second for those tests, respectively. The same article reports higher PostgreSQL throughput for JSON aggregation and indexed lookup. These are publisher-reported results; the available methodology does not establish enough detail about query text, index definitions, hardware, cache state, or data distribution to treat them as general performance ratios.
A separate public benchmark repository reports close results for some single-document lookups, with the leader changing across concurrency settings. Its results cover different tests and do not directly validate or disprove the other benchmark. Together, they show why a headline throughput figure cannot predict performance for a different workload.
Use the same server versions and hardware in both tests. Match durability settings, document structures, and data distribution too. Test single-value extraction and filters that check for an exact value or a span of values. Also test searches for specified keys or values within documents, array membership, aggregation, and the exact updates your code performs. Use comparable indexes and query semantics. Record query plans, index size, and build time. Track write throughput, response times at different percentiles, errors, and peak queries per second. Exact version numbers matter: record the releases and configuration for both databases rather than relying on a broad product label.
If a field drives frequent filtering, sorting, joining, or constraints, store it in a typed relational column or index a specific JSON path. This gives the database a direct way to find the values and makes the comparison clearer. Use PostgreSQL GIN when searches need to span varied document keys or values; choose the operator class based on whether queries need key-existence checks as well as containment.
Use MySQL generated or functional indexes when the queried paths are known and stable. For array-membership queries, test a multi-valued index with the predicates the application sends, while accounting for its scan and covering-index limits. For either database, compare JSON storage with a relational-column design when JSON fields have become central to filtering or sorting.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com