Home Functions JSON_EXTRACT()
SQL JSON Function

JSON_EXTRACT()

JSON_EXTRACT() pulls a single value out of a JSON document at a given path. Every major engine solves this, but the spelling differs: MySQL and SQLite use JSON_EXTRACT (and the -> / ->> shorthands), PostgreSQL uses -> and ->>, and SQL Server uses JSON_VALUE and JSON_QUERY.

MySQLPostgreSQLSQL ServerSQLite
Returns: The value found at the given JSON path. By default JSON scalars keep their JSON form (strings stay quoted); use an unquoting variant to get plain text. Returns NULL when the path does not exist.

Syntax

JSON_EXTRACT(doc, path) -- MySQL, SQLitedoc -> 'key' / doc ->> 'key' -- MySQL, PostgreSQLJSON_VALUE(doc, path) -- SQL Server (scalars)JSON_QUERY(doc, path) -- SQL Server (objects and arrays)
ParameterTypeRequiredDescription
doc JSON or JSONB or text yes The JSON document or column to read from.
path JSON path string yes A path expression such as '$.name', '$.items[0]' or '$.address.city' that points at the value to pull out.
key text or integer yes For the -> and ->> operators: an object key ('name') or, in PostgreSQL, an array index (0).

How it works

A JSON column holds a whole document, but most queries only need one field from it. JSON_EXTRACT() and its cousins take a JSON path expression and return the value at that location. Paths start at the document root $, then walk into object keys with $.key and into array elements with $.arr[0]. You can nest them, for example $.address.city.

The crux is that the syntax is not portable. In MySQL you write JSON_EXTRACT(doc, '$.path'), or the shorthand doc -> '$.path' which returns the value still wrapped in JSON quotes, or doc ->> '$.path' which returns unquoted text. SQLite uses the same json_extract(doc, '$.path'). In PostgreSQL the operators take a bare key: doc -> 'key' returns JSON and doc ->> 'key' returns text, with #> and jsonb_path_query for deeper paths. In SQL Server you use JSON_VALUE(doc, '$.path') for a scalar and JSON_QUERY(doc, '$.path') for an object or array.

Because the return type matters, watch the quoting: JSON_EXTRACT() and -> preserve JSON (a string comes back as "Cairo" with the quotes), while ->>, JSON_UNQUOTE() and JSON_VALUE() hand you the plain text Cairo. If you need to compare or CAST the value, you almost always want the unquoted form.

Examples

Extract a top-level key (MySQL / SQLite)

-- profile holds {"name": "Sara", "age": 30}
SELECT JSON_EXTRACT(profile, '$.name') AS name_json,
       JSON_UNQUOTE(JSON_EXTRACT(profile, '$.name')) AS name_text
FROM users
WHERE id = 1;
Result
name_json | name_text
----------+----------
 "Sara"   | Sara

Extract a nested value

-- doc holds {"address": {"city": "Cairo", "zip": "11511"}}
SELECT JSON_EXTRACT(doc, '$.address.city') AS city
FROM orders
WHERE id = 42;
Result
city
-------
 Cairo

Extract an array element

-- tags holds ["sql", "json", "index"]
SELECT JSON_EXTRACT(tags, '$[0]') AS first_tag,
       JSON_EXTRACT(tags, '$[2]') AS third_tag
FROM posts
WHERE id = 7;
Result
first_tag | third_tag
----------+----------
 "sql"    | "index"

The same extract in every engine

-- MySQL (unquoted text)
SELECT profile ->> '$.name' FROM users;

-- PostgreSQL (operator takes a bare key, ->> returns text)
SELECT profile ->> 'name' FROM users;

-- SQL Server (JSON_VALUE for a scalar)
SELECT JSON_VALUE(profile, '$.name') FROM users;

-- SQLite
SELECT json_extract(profile, '$.name') FROM users;
Result
name
------
 Sara

Filter rows on a JSON field in WHERE

-- MySQL / SQLite: keep only VIP customers
SELECT id, JSON_UNQUOTE(JSON_EXTRACT(profile, '$.tier')) AS tier
FROM customers
WHERE JSON_UNQUOTE(JSON_EXTRACT(profile, '$.tier')) = 'vip';

-- PostgreSQL equivalent
SELECT id, profile ->> 'tier' AS tier
FROM customers
WHERE profile ->> 'tier' = 'vip';
Result
id  | tier
----+-----
  3 | vip
 17 | vip

Extract an object with JSON_QUERY (SQL Server)

-- JSON_VALUE returns NULL for an object; use JSON_QUERY
SELECT JSON_QUERY(doc, '$.address')  AS address_obj,
       JSON_VALUE(doc, '$.address.city') AS city
FROM orders
WHERE id = 42;
Result
address_obj                        | city
-----------------------------------+-------
 {"city":"Cairo","zip":"11511"}    | Cairo

Common mistakes

Wrong
-- Comparing a quoted JSON value to plain text always fails:
-- JSON_EXTRACT returns "vip" (with quotes), not vip
SELECT * FROM customers
WHERE JSON_EXTRACT(profile, '$.tier') = 'vip';
Right
-- Unquote first with ->> or JSON_UNQUOTE
SELECT * FROM customers
WHERE profile ->> '$.tier' = 'vip';
-- or: WHERE JSON_UNQUOTE(JSON_EXTRACT(profile, '$.tier')) = 'vip'

The number one JSON mistake: JSON_EXTRACT() and -> keep the JSON quotes, so "vip" never equals vip. Use ->>, JSON_UNQUOTE() or SQL Server JSON_VALUE() when you want plain text.

Wrong
-- Reads and parses the JSON of every single row.
-- On a large table this is a full scan.
SELECT * FROM customers
WHERE JSON_UNQUOTE(JSON_EXTRACT(profile, '$.tier')) = 'vip';
Right
-- Add a generated / functional index on the extracted value
-- MySQL:
ALTER TABLE customers
  ADD tier VARCHAR(20)
  AS (JSON_UNQUOTE(JSON_EXTRACT(profile, '$.tier'))) STORED,
  ADD INDEX idx_tier (tier);
-- PostgreSQL:
CREATE INDEX idx_tier ON customers ((profile ->> 'tier'));

Filtering on a JSON field in WHERE cannot use a normal index, so it scans every row. Add a generated column or a functional / expression index. See how indexes improve performance.

Wrong
-- Typo in the path: 'citty' does not exist.
-- This returns NULL, not an error, so the bug is silent.
SELECT JSON_EXTRACT(doc, '$.address.citty') AS city
FROM orders;
Right
-- Use the correct key. NULL means the path missed.
SELECT JSON_EXTRACT(doc, '$.address.city') AS city
FROM orders;

A path that does not match returns NULL rather than raising an error, so typos hide. If a JSON extract mysteriously yields NULL, check the path spelling and the actual document shape first.

Performance

Extracting from JSON means parsing the document at query time. For a single row by primary key that is cheap, but filtering or grouping on a JSON path across a big table forces the engine to parse every row, which is a full scan. Whenever a JSON field appears in WHERE, JOIN or ORDER BY on a large table, plan for an index.

The standard fix is a generated (computed) column that materialises the extracted value, plus a normal index on it. MySQL supports STORED and VIRTUAL generated columns, PostgreSQL supports functional (expression) indexes such as CREATE INDEX ... ((profile ->> 'tier')), and SQL Server lets you index a computed column built from JSON_VALUE(). PostgreSQL jsonb also offers GIN indexes for containment queries.

Prefer jsonb over json in PostgreSQL: jsonb is parsed and stored in a binary form, so extraction and indexing are much faster. See the indexing guide for how expression indexes are used by the optimiser.

Interview questions

How do you extract a value from a JSON column, and how does it differ across databases?

MySQL and SQLite use JSON_EXTRACT(doc, '$.path'). MySQL and PostgreSQL also have the -> and ->> operators. SQL Server uses JSON_VALUE() for scalars and JSON_QUERY() for objects and arrays. The JSON path syntax ($.key, $.arr[0]) is broadly shared.

What is the difference between -> and ->> ?

Both extract a value, but -> returns it as JSON (a string keeps its quotes) while ->> returns it as plain text. Use ->> when you need to compare, cast or display the value.

SELECT profile -> '$.name'  AS as_json,   -- "Sara"
       profile ->> '$.name' AS as_text;  -- Sara

In SQL Server, when do you use JSON_VALUE versus JSON_QUERY?

Use JSON_VALUE() to read a scalar (string, number, boolean). Use JSON_QUERY() to read an object or array. If you point JSON_VALUE at an object it returns NULL, and JSON_QUERY at a scalar returns NULL, so pick the one that matches the shape.

Why is filtering on a JSON field in WHERE often slow, and how do you fix it?

A plain B-tree index covers columns, not values buried inside a JSON document, so the engine parses every row. Fix it with a generated / computed column that stores the extracted value plus an index on it, or a functional / expression index (PostgreSQL) or a GIN index for jsonb containment.

Your JSON_EXTRACT returns NULL but you expected a value. What is wrong?

Most likely the JSON path does not match the document: a misspelled key, wrong nesting, or an out-of-range array index. A path miss returns NULL rather than an error. Double-check the path and the real document structure. Also confirm the column actually holds valid JSON.

Master SQL, one function at a time

Browse the full SQL functions library, or learn the fundamentals with our free, structured courses.