JSON_EXTRACT_SCALAR()

Concepts covered: JSON Extraction

The listing editor logs each save as a JSON document in a VARCHAR column named payload. json_extract pulls a fragment out by path and returns it as JSON, so a string comes back wrapped in its quotes and an object comes back whole. That suits a fragment headed for more JSON processing, and it gets in the way when you want a plain value to filter, group or join on.

json_extract_scalar(payload, '$.address.city') returns the value as VARCHAR with the quotes removed. It handles strings, numbers and booleans. A path that lands on an object or an array, a path that does not exist, and a JSON null all give SQL NULL.

SELECT
  event_id,
  json_extract_scalar(payload, '$.listing_id') AS listing_id,
  json_extract_scalar(payload, '$.address.city') AS city,
  json_extract_scalar(payload, '$.address') AS address,
  json_extract_scalar(payload, '$.nightly_price') AS price,
  json_extract_scalar(payload, '$.superhost') AS superhost
FROM listing_events
listing_events
event_idpayload
1{"listing_id": 101, "address": {"city": "Austin", "zip": "78701"}, "nightly_price": 129, "max_guests": 4, "superhost": true, "amenities": ["wifi", "hot_tub", "parking"]}
2{"listing_id": 102, "address": {"city": "Denver", "zip": "80202"}, "nightly_price": 95, "max_guests": 10, "superhost": false, "amenities": ["parking", "wifi", "wifi"]}
3{"listing_id": 103, "address": {"city": "Austin", "zip": "78704"}, "nightly_price": null, "max_guests": 2, "superhost": true, "amenities": ["kitchen", null, "parking"]}
4{"listing_id": 104, "address": {"city": "Denver"}, "nightly_price": "TBD", "max_guests": 6, "amenities": ["hot_tub", "kitchen", null]}
Result
event_idlisting_idcityaddresspricesuperhost
1101AustinNULL129true
2102DenverNULL95false
3103AustinNULLNULLtrue
4104DenverNULLTBDNULL

Event 3's price is NULL because its JSON holds null, and event 4's superhost is NULL because the key is absent. The column cannot tell those 2 cases apart, and the next section shows how to list the keys each document carries. Event 4's price arrives as the text TBD, which matters as soon as the column is cast.

Paths into nested fields and arrays

Path syntax in this lesson

Applied to the 4 save events, those paths reach into the address object and pick amenities by index. Event 4 returns NULL twice, for 2 different reasons: its address carries no zip key, and its third amenity is a JSON null. Index 2 is the third element, so event 1 shows parking in that column.

SELECT
  event_id,
  json_extract_scalar(payload, '$.address.zip') AS zip,
  json_extract_scalar(payload, '$.amenities[0]') AS first_amenity,
  json_extract_scalar(payload, '$.amenities[2]') AS third_amenity
FROM listing_events
listing_events
event_idpayload
1{"listing_id": 101, "address": {"city": "Austin", "zip": "78701"}, "nightly_price": 129, "max_guests": 4, "superhost": true, "amenities": ["wifi", "hot_tub", "parking"]}
2{"listing_id": 102, "address": {"city": "Denver", "zip": "80202"}, "nightly_price": 95, "max_guests": 10, "superhost": false, "amenities": ["parking", "wifi", "wifi"]}
3{"listing_id": 103, "address": {"city": "Austin", "zip": "78704"}, "nightly_price": null, "max_guests": 2, "superhost": true, "amenities": ["kitchen", null, "parking"]}
4{"listing_id": 104, "address": {"city": "Denver"}, "nightly_price": "TBD", "max_guests": 6, "amenities": ["hot_tub", "kitchen", null]}
Result
event_idzipfirst_amenitythird_amenity
178701wifiparking
280202parkingwifi
378704kitchenparking
4NULLhot_tubNULL

Every result is VARCHAR

Numbers come out as text, and text compares character by character. Sorting guest limits of 4, 10, 2 and 6 in descending order as text gives 6, 4, 2, 10, because the character 6 outranks the character 1. The first tab sorts the raw strings. The second casts to INTEGER inside the SELECT, so the sort sees numbers.

listing_events
event_idpayload
1{"listing_id": 101, "address": {"city": "Austin", "zip": "78701"}, "nightly_price": 129, "max_guests": 4, "superhost": true, "amenities": ["wifi", "hot_tub", "parking"]}
2{"listing_id": 102, "address": {"city": "Denver", "zip": "80202"}, "nightly_price": 95, "max_guests": 10, "superhost": false, "amenities": ["parking", "wifi", "wifi"]}
3{"listing_id": 103, "address": {"city": "Austin", "zip": "78704"}, "nightly_price": null, "max_guests": 2, "superhost": true, "amenities": ["kitchen", null, "parking"]}
4{"listing_id": 104, "address": {"city": "Denver"}, "nightly_price": "TBD", "max_guests": 6, "amenities": ["hot_tub", "kitchen", null]}

Text sort

Sorts VARCHAR values: listing 102 with 10 guests lands last.

SELECT
  json_extract_scalar(payload, '$.listing_id') AS listing_id,
  json_extract_scalar(payload, '$.max_guests') AS max_guests
FROM listing_events
ORDER BY max_guests DESC
Result
listing_idmax_guests
1046
1014
1032
10210

Cast first

Sorts INTEGER values: listing 102 comes first.

SELECT
  json_extract_scalar(payload, '$.listing_id') AS listing_id,
  CAST(json_extract_scalar(payload, '$.max_guests') AS INTEGER) AS max_guests
FROM listing_events
ORDER BY max_guests DESC
Result
listing_idmax_guests
10210
1046
1014
1032

CAST stops the whole query when one value will not convert, and event 4's price is TBD. TRY_CAST returns NULL for that row and lets the others through. Wrapped around the extraction, it gives a column that aggregates cleanly, and COUNT over that column reports how many rows had a usable price.

SELECT
  json_extract_scalar(payload, '$.address.city') AS city,
  COUNT(*) AS listings,
  COUNT(TRY_CAST(json_extract_scalar(payload, '$.nightly_price') AS INTEGER)) AS priced,
  AVG(TRY_CAST(json_extract_scalar(payload, '$.nightly_price') AS INTEGER)) AS avg_price
FROM listing_events
GROUP BY json_extract_scalar(payload, '$.address.city')
ORDER BY city
listing_events
event_idpayload
1{"listing_id": 101, "address": {"city": "Austin", "zip": "78701"}, "nightly_price": 129, "max_guests": 4, "superhost": true, "amenities": ["wifi", "hot_tub", "parking"]}
2{"listing_id": 102, "address": {"city": "Denver", "zip": "80202"}, "nightly_price": 95, "max_guests": 10, "superhost": false, "amenities": ["parking", "wifi", "wifi"]}
3{"listing_id": 103, "address": {"city": "Austin", "zip": "78704"}, "nightly_price": null, "max_guests": 2, "superhost": true, "amenities": ["kitchen", null, "parking"]}
4{"listing_id": 104, "address": {"city": "Denver"}, "nightly_price": "TBD", "max_guests": 6, "amenities": ["hot_tub", "kitchen", null]}
Result
citylistingspricedavg_price
Austin21129
Denver2247.5

Event 1's payload holds "amenities": ["wifi", "hot_tub", "parking"]. What does this query return for it?

SELECT
  json_extract_scalar(payload, '$.amenities[1]') AS amenity
FROM listing_events
WHERE event_id = 1

About this lesson

This section is part of the Complex Data: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.