SQL Querying for JSON Arrays and Nested Objects

SQL Querying for JSON Arrays and Nested Objects

Modern applications rarely store all their data in simple relational columns.

APIs, event streams, application logs, configuration systems, and modern data platforms frequently produce JSON.

A single JSON record might contain:

{
  "customer": {
    "id": 1042,
    "name": "Sarah"
  },
  "orders": [
    {
      "id": 501,
      "amount": 120
    },
    {
      "id": 502,
      "amount": 85
    }
  ]
}

Traditional SQL works naturally with rows and columns, but JSON introduces another layer of structure.

You may need to:

  • Extract values from nested objects
  • Search inside JSON arrays
  • Expand arrays into rows
  • Filter records based on nested fields
  • Aggregate values from JSON
  • Handle missing properties
  • Convert JSON values into SQL types

The exact syntax varies between database systems, but the underlying concepts are remarkably similar.

To query JSON arrays and nested objects in SQL, you generally need to perform four operations:

JSON Document
     ↓
Navigate
     ↓
Extract
     ↓
Expand Arrays
     ↓
Filter / Aggregate

For example, a nested object such as:

{
  "customer": {
    "name": "Sarah"
  }
}

can often be accessed conceptually as:

customer → name

while an array such as:

{
  "tags": ["python", "sql", "data"]
}

needs to be unnested, flattened, or otherwise expanded before you can treat each element as a normal SQL row.

Why JSON Is Common in Data Engineering

JSON is widely used because it is flexible and easy for applications to exchange.

Common sources include:

  • REST APIs
  • Webhooks
  • Application events
  • SaaS platforms
  • NoSQL databases
  • Log systems
  • Event streaming platforms
  • Configuration files

For example, an event pipeline might receive:

{
  "event": "purchase",
  "user": {
    "id": 123,
    "country": "US"
  },
  "products": [
    {
      "sku": "A100",
      "quantity": 2
    },
    {
      "sku": "B200",
      "quantity": 1
    }
  ]
}

Instead of immediately transforming every nested field into separate tables, a data platform may temporarily store the JSON document.

SQL can then be used to analyze it.

JSON Objects vs JSON Arrays

Understanding the difference is essential.

JSON Object

An object contains key-value pairs.

{
  "name": "Sarah",
  "age": 31,
  "country": "US"
}

You access properties by their keys:

name
age
country

JSON Array

An array contains an ordered collection of values.

[
  "Python",
  "SQL",
  "Power BI"
]

Arrays can also contain objects:

[
  {
    "id": 1,
    "price": 50
  },
  {
    "id": 2,
    "price": 75
  }
]

Querying arrays usually requires an additional operation to turn their elements into rows.

A Sample JSON Dataset

Consider a table called:

customers

with columns:

customer_id
customer_data

where customer_data contains:

{
  "name": "Sarah",
  "country": "US",
  "preferences": {
    "language": "English",
    "currency": "USD"
  },
  "orders": [
    {
      "order_id": 101,
      "amount": 120
    },
    {
      "order_id": 102,
      "amount": 85
    }
  ]
}

There are three different levels here:

customer_data
   ↓
preferences
   ↓
language

and:

customer_data
   ↓
orders
   ↓
array elements
   ↓
order_id / amount

These require slightly different querying techniques.

Querying Nested JSON Objects

The first task is often extracting a value from a nested object.

Conceptually:

customer_data → preferences → language

Different databases provide different operators.

For example, PostgreSQL commonly uses:

SELECT
    customer_data -> 'preferences' ->> 'language' AS language
FROM customers;

The important distinction is:

->   returns JSON
->>  returns text

So:

customer_data -> 'preferences'

returns a JSON object, while:

customer_data -> 'preferences' ->> 'language'

returns the value as text.

PostgreSQL JSON Operators

PostgreSQL provides several useful JSON operators.

OperatorPurpose
->Get JSON object/array element
->>Get JSON value as text
#>Get nested JSON object/array
#>>Get nested value as text

For example:

SELECT
    customer_data ->> 'name' AS customer_name
FROM customers;

For deeper nesting:

SELECT
    customer_data #>> '{preferences,language}' AS language
FROM customers;

Querying JSON in MySQL

MySQL provides functions such as:

JSON_EXTRACT()

For example:

SELECT
    JSON_EXTRACT(customer_data, '$.name') AS customer_name
FROM customers;

You can navigate deeper:

SELECT
    JSON_EXTRACT(
        customer_data,
        '$.preferences.language'
    ) AS language
FROM customers;

MySQL also supports the shorthand -> and ->> operators for JSON columns.

For example:

SELECT
    customer_data->>'$.name' AS customer_name
FROM customers;

JSON Path Expressions

Many SQL systems use JSONPath-style expressions to navigate JSON.

Consider:

{
  "customer": {
    "name": "Sarah"
  }
}

The path is:

$.customer.name

The $ represents the root JSON document.

For example:

$
└── customer
    └── name

JSON paths become particularly useful when working with arrays.

Querying JSON Arrays

Consider:

{
  "tags": [
    "python",
    "sql",
    "analytics"
  ]
}

The array exists at:

$.tags

Individual elements can be accessed using indexes.

For example:

$.tags[0]

returns:

python

while:

$.tags[1]

returns:

sql

But indexing isn’t always the best approach.

If you want to analyze every array element, you generally need to expand the array.

Why You Should Flatten Arrays

Suppose one customer has:

["python", "sql", "analytics"]

and another has:

["sql", "python"]

If these remain inside JSON arrays, counting how many customers use SQL becomes more complicated.

After flattening:

customer_idskill
1python
1sql
1analytics
2sql
2python

Now ordinary SQL becomes possible:

SELECT
    skill,
    COUNT(*) AS customers
FROM customer_skills
GROUP BY skill;

This is one of the most important concepts when querying JSON arrays.

PostgreSQL: jsonb_array_elements()

PostgreSQL provides:

jsonb_array_elements()

to expand a JSON array into rows.

Suppose:

{
  "orders": [
    {"id": 101, "amount": 120},
    {"id": 102, "amount": 85}
  ]
}

You can expand the array:

SELECT
    customer_id,
    jsonb_array_elements(customer_data->'orders') AS order_data
FROM customers;

The result conceptually becomes:

customer_idorder_data
1{"id":101,"amount":120}
1{"id":102,"amount":85}

You can then extract fields from each object.

Extracting Fields From Array Objects

You can combine array expansion with JSON operators.

SELECT
    c.customer_id,
    order_data ->> 'id' AS order_id,
    (order_data ->> 'amount')::numeric AS amount
FROM customers c
CROSS JOIN LATERAL
    jsonb_array_elements(c.customer_data->'orders') AS order_data;

Now each order becomes a regular SQL row.

This pattern is extremely useful for event and API data.

PostgreSQL jsonb_array_elements_text()

If the array contains simple scalar values:

{
  "tags": [
    "python",
    "sql",
    "analytics"
  ]
}

you can use:

jsonb_array_elements_text()

For example:

SELECT
    customer_id,
    tag
FROM customers
CROSS JOIN LATERAL
    jsonb_array_elements_text(customer_data->'tags') AS tag;

This produces one row per tag.

BigQuery: UNNEST()

BigQuery uses a different approach.

Suppose you have an array field called:

orders

You can use:

SELECT
    customer_id,
    order
FROM `project.dataset.customers`,
UNNEST(orders) AS order;

UNNEST() converts an array into rows.

This is one of the most important BigQuery techniques for nested and repeated data.

BigQuery Nested Fields

BigQuery can store nested and repeated fields directly.

For example:

customer
 ├── id
 ├── name
 └── orders[]
      ├── order_id
      └── amount

You can access nested fields using dot notation:

SELECT
    customer.id,
    customer.name
FROM `project.dataset.customers`;

For repeated fields:

SELECT
    customer.id,
    order.order_id,
    order.amount
FROM `project.dataset.customers`,
UNNEST(customer.orders) AS order;

Snowflake: FLATTEN()

Snowflake provides the:

FLATTEN()

table function for expanding arrays and objects.

For example:

SELECT
    customer_id,
    value
FROM customers,
LATERAL FLATTEN(
    input => customer_data:orders
);

You can then extract fields from value.

For example:

SELECT
    customer_id,
    value:id::INT AS order_id,
    value:amount::NUMBER AS amount
FROM customers,
LATERAL FLATTEN(
    input => customer_data:orders
);

DuckDB and JSON

DuckDB is particularly useful for local analytics involving JSON.

It provides JSON functions and operators that allow you to extract nested values and work with JSON arrays.

For example:

SELECT
    json_extract(data, '$.customer.name') AS customer_name
FROM events;

DuckDB can also transform JSON into relational structures, making it useful for analyzing API responses and local files without setting up a large database infrastructure.

Filtering Nested JSON Values

Extracting values is only the beginning.

You may want to filter rows.

Suppose you want customers from the United States.

In PostgreSQL:

SELECT *
FROM customers
WHERE customer_data ->> 'country' = 'US';

For nested values:

SELECT *
FROM customers
WHERE customer_data #>> '{preferences,language}'
    = 'English';

The general pattern is:

Extract JSON value
       ↓
Compare with condition
       ↓
Filter rows

Filtering JSON Arrays

Suppose you want customers whose tags contain:

sql

In PostgreSQL, one approach is:

SELECT *
FROM customers
WHERE customer_data->'tags'
    ? 'sql';

This is useful when the JSON value is a JSONB array of strings.

For more complex conditions, you can expand the array:

SELECT DISTINCT
    customer_id
FROM customers c
CROSS JOIN LATERAL
    jsonb_array_elements_text(
        customer_data->'tags'
    ) AS tag
WHERE tag = 'sql';

Filtering Objects Inside Arrays

This becomes more interesting when arrays contain objects.

Suppose:

{
  "orders": [
    {
      "id": 101,
      "status": "completed",
      "amount": 120
    },
    {
      "id": 102,
      "status": "cancelled",
      "amount": 85
    }
  ]
}

You may want customers with orders above $100.

After expanding the array:

SELECT DISTINCT
    c.customer_id
FROM customers c
CROSS JOIN LATERAL
    jsonb_array_elements(
        customer_data->'orders'
    ) AS order_data
WHERE (order_data->>'amount')::numeric > 100;

The important technique is:

JSON array
    ↓
Expand
    ↓
Extract field
    ↓
Cast to correct type
    ↓
Filter

Casting JSON Values

JSON values often need to be converted to SQL data types.

Suppose:

{
  "amount": 120
}

You might initially extract:

order_data ->> 'amount'

which produces text.

You can convert it:

(order_data ->> 'amount')::numeric

Similarly:

(order_data ->> 'id')::integer

or:

(order_data ->> 'created_at')::timestamp

Correct data types matter for:

  • Arithmetic
  • Comparisons
  • Sorting
  • Aggregation
  • Date operations

Aggregating JSON Data

Once JSON arrays are flattened, ordinary SQL aggregation becomes available.

For example:

SELECT
    c.customer_id,
    SUM(
        (order_data ->> 'amount')::numeric
    ) AS total_spend
FROM customers c
CROSS JOIN LATERAL
    jsonb_array_elements(
        customer_data->'orders'
    ) AS order_data
GROUP BY c.customer_id;

This converts nested order data into a customer-level metric.

Counting Array Elements

You don’t always need to flatten an array.

If you only need its length, database-specific JSON functions can often calculate it directly.

In PostgreSQL:

SELECT
    customer_id,
    jsonb_array_length(
        customer_data->'orders'
    ) AS order_count
FROM customers;

This is more efficient and simpler when you don’t need to inspect individual elements.

Handling Missing JSON Fields

Real-world JSON is often inconsistent.

One record might contain:

{
  "customer": {
    "name": "Sarah"
  }
}

while another contains:

{
  "customer": {
    "name": "John",
    "country": "US"
  }
}

The country field is missing from the first record.

Your SQL should therefore account for:

  • Missing properties
  • Null values
  • Empty arrays
  • Unexpected types

Don’t assume every JSON document has the same structure.

NULL vs Missing Values

A JSON property can be:

{
  "country": null
}

or completely absent:

{
  "name": "Sarah"
}

These aren’t always semantically identical.

Your database’s JSON functions may handle these cases differently, so test the behavior before building production logic around it.

JSON Arrays of Scalars vs Objects

There are two major patterns.

Array of Scalars

{
  "tags": [
    "python",
    "sql",
    "data"
  ]
}

Use an array-expansion function to produce:

python
sql
data

Array of Objects

{
  "orders": [
    {
      "id": 1,
      "amount": 50
    },
    {
      "id": 2,
      "amount": 75
    }
  ]
}

Expand first, then extract individual properties.

orders[]
   ↓
order object
   ↓
id / amount

JSON Arrays Nested Inside Objects

Real data can have several levels of nesting.

For example:

{
  "customer": {
    "profile": {
      "country": "US"
    },
    "orders": [
      {
        "items": [
          {
            "sku": "A100",
            "quantity": 2
          }
        ]
      }
    ]
  }
}

Now there are multiple levels:

customer
  ↓
orders[]
  ↓
items[]
  ↓
sku

The general strategy remains:

Navigate
   ↓
Expand first array
   ↓
Expand nested array
   ↓
Extract values

Querying Multiple Nested Arrays

Suppose each order contains multiple items.

In PostgreSQL, you might use:

SELECT
    c.customer_id,
    order_data ->> 'id' AS order_id,
    item_data ->> 'sku' AS sku,
    (item_data ->> 'quantity')::integer AS quantity
FROM customers c
CROSS JOIN LATERAL
    jsonb_array_elements(
        customer_data->'orders'
    ) AS order_data
CROSS JOIN LATERAL
    jsonb_array_elements(
        order_data->'items'
    ) AS item_data;

This creates a relational representation:

customer_idorder_idskuquantity
1101A1002
1101B2001
1102C3004

JSON Querying and SQL Joins

Once nested data is expanded, you can join it to normal relational tables.

For example:

SELECT
    c.customer_id,
    o.order_id,
    p.product_name,
    o.amount
FROM customers c
CROSS JOIN LATERAL
    jsonb_array_elements(
        customer_data->'orders'
    ) AS order_data
JOIN products p
    ON p.product_id =
       (order_data->>'product_id')::integer;

This is a powerful pattern for integrating semi-structured and relational data.

When Should JSON Stay Nested?

JSON isn’t automatically bad database design.

Keeping data nested can make sense when:

  • The structure changes frequently
  • The data is consumed as a document
  • The nested structure is rarely queried
  • The source system already produces JSON
  • The data is primarily used for archival purposes

But frequently queried business attributes may benefit from being extracted into regular columns.

When Should You Flatten JSON?

Flattening is often useful when:

  • Analysts query the fields frequently
  • You need joins
  • You frequently aggregate values
  • You need strong type enforcement
  • The schema is relatively stable
  • Query performance matters

A common architecture is:

Raw JSON
   ↓
Bronze / Raw Layer
   ↓
Flattened Transformation
   ↓
Analytics Model

This allows you to preserve the original data while creating efficient structures for analysis.

JSON in Data Lakes and Warehouses

JSON is particularly common in modern data platforms.

A pipeline may look like:

API
 ↓
JSON
 ↓
Object Storage
 ↓
Data Warehouse / Lakehouse
 ↓
SQL Transformation
 ↓
Analytics Table

The raw JSON provides flexibility.

The transformed relational layer provides usability.

Performance Considerations

JSON querying can become expensive when applied to very large datasets.

For example:

SELECT *
FROM events
WHERE JSON_EXTRACT(...)

may require the database to repeatedly parse or navigate JSON documents.

For frequently queried fields, consider:

  • Extracting fields into columns
  • Materializing transformations
  • Creating appropriate indexes
  • Using generated columns
  • Partitioning
  • Clustering
  • Choosing native nested types where supported

Indexing JSON

Some databases support specialized JSON indexes.

PostgreSQL, for example, supports indexes on jsonb.

A GIN index can improve certain containment and key-existence queries:

CREATE INDEX idx_customer_data
ON customers
USING GIN (customer_data);

However, indexing strategy should be based on actual query patterns.

Don’t add JSON indexes blindly.

SQL JSON Functions Are Database-Specific

One of the biggest challenges is portability.

The same operation may look different across databases.

TaskPostgreSQLBigQuerySnowflakeMySQL
Extract JSON->, ->>JSON_VALUE / field access:JSON_EXTRACT()
Expand arrayjsonb_array_elements()UNNEST()FLATTEN()JSON table/functions
Array lengthjsonb_array_length()ARRAY_LENGTH()ARRAY_SIZE()JSON_LENGTH()
JSON pathOperators / pathJSONPathJSON pathJSON path

The exact implementation depends on the database.

The concepts, however, remain similar.

A General JSON Querying Pattern

Across database systems, the workflow is usually:

1. Locate JSON document
        ↓
2. Navigate to property
        ↓
3. Extract value
        ↓
4. Expand arrays if necessary
        ↓
5. Convert data types
        ↓
6. Filter
        ↓
7. Aggregate
        ↓
8. Join with relational data

Once you understand this pattern, switching between databases becomes much easier.

Common Mistakes

Treating JSON Like Normal Columns

A nested JSON field isn’t automatically a relational column.

You need to navigate its structure.

Forgetting to Flatten Arrays

Trying to aggregate an entire array as if it were a scalar often produces incorrect results.

Ignoring Data Types

Extracted JSON values may be strings.

Always cast them appropriately before calculations.

Assuming Every Record Has the Same Schema

Production JSON frequently contains missing or optional properties.

Expanding Arrays Unnecessarily

If you only need the array length, don’t create one row per element.

Ignoring Performance

Repeated JSON parsing across billions of records can become expensive.

Using Database-Specific Syntax Without Checking

PostgreSQL, BigQuery, Snowflake, and MySQL use different JSON functions.

Best Practices

Keep Raw JSON When Useful

Preserving the original payload can make debugging and reprocessing easier.

Create Structured Models for Frequent Queries

Frequently accessed fields should generally be made easy to query.

Flatten Arrays Deliberately

Only expand arrays when the analysis requires element-level data.

Cast Values Explicitly

Convert JSON strings to numeric, date, Boolean, or other appropriate SQL types.

Handle Missing Fields

Production data is rarely perfectly uniform.

Monitor Schema Changes

API changes can silently alter JSON structures.

Test With Real Data

JSON structures often contain edge cases that aren’t obvious from documentation.

Optimize High-Volume Queries

Use indexes, materialized transformations, partitioning, clustering, or native nested data types where appropriate.

Conclusion

JSON gives modern data systems flexibility, but it also introduces complexity when you need to analyze nested information.

The most important concepts are:

Navigate nested objects → extract values → expand arrays → cast types → filter → aggregate.

The exact syntax changes between PostgreSQL, MySQL, BigQuery, Snowflake, DuckDB, and other databases.

But the underlying mental model remains the same.

If you frequently work with APIs, event data, logs, or semi-structured data, understanding how SQL interacts with JSON arrays and nested objects is an essential data engineering skill.

The goal isn’t to keep everything in JSON forever.

Instead, a strong data architecture often preserves raw JSON for flexibility while transforming frequently used information into reliable, typed, query-friendly structures for analytics.

Frequently Asked Questions

How do I query a nested JSON object in SQL?

Use the JSON extraction operators or functions provided by your database. For example, PostgreSQL uses -> and ->> to navigate JSON objects and extract values.

How do I query a JSON array in SQL?

If you need individual elements, use the database’s array-expansion function. PostgreSQL provides jsonb_array_elements(), BigQuery uses UNNEST(), and Snowflake provides FLATTEN().

What is JSONPath?

JSONPath is a path-expression syntax used by many systems to navigate nested JSON documents. A path such as $.customer.name means to start at the document root, find customer, and then access name.

Why do I need to flatten JSON arrays?

Flattening converts array elements into individual rows, allowing you to use normal SQL operations such as WHERE, GROUP BY, JOIN, and SUM().

How do I query nested arrays?

Navigate to the first array, expand it, then expand any nested arrays inside each resulting object. This creates a relational representation of the nested structure.

Can SQL join JSON data with normal tables?

Yes. After extracting and appropriately casting JSON values, you can use them in standard SQL joins.

Is JSON slower to query than normal columns?

It can be, especially when complex JSON extraction is repeatedly performed across large datasets. Frequently queried fields may benefit from being materialized as regular columns or optimized with appropriate indexing.

Should JSON data always be normalized?

No. JSON can be useful for raw ingestion, flexible schemas, APIs, and document-oriented workloads. Frequently queried analytical fields can be transformed into structured relational models.

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

In PostgreSQL, -> returns a JSON value, while ->> extracts the value as text.

Can DuckDB query JSON files directly?

Yes. DuckDB provides JSON functionality that can be used to extract and analyze semi-structured data, making it useful for local analytics and data engineering workflows.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top