Docs /Querying /Subqueries
Querying

Subqueries in BigQuery

Scalar, ARRAY, STRUCT, IN and EXISTS subqueries in BigQuery. A scalar subquery with no rows returns NULL and with two rows fails, ARRAY() collects many rows per outer row, SELECT AS STRUCT returns several values, and LATERAL isn't supported.

A subquery is a query inside another query. BigQuery supports them almost anywhere an expression or a table can go: in SELECT, WHERE, FROM and HAVING. A correlated subquery refers to columns of the outer query and conceptually runs once per outer row. Two more forms, ARRAY(subquery) and SELECT AS STRUCT, remove the main limitation of scalar subqueries: returning one value.

The examples use three customers and three orders: ana has two orders, ben one, cai none.

Scalar subqueries

A subquery in SELECT that returns one column is a scalar subquery. Correlated with the outer row, it computes a value per customer:

each customer's order total
123456789101112131415
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT
  name,
  (SELECT SUM(amount) FROM orders AS o WHERE o.customer_id = c.customer_id) AS total
FROM customers AS c
ORDER BY name
Result in BigQuery
nametotal
1ana75
2ben20
3cainull
3 rows

Cai has no orders, so the SUM over zero rows is NULL. A scalar subquery that returns no row at all also gives NULL:

a subquery that finds nothing
1234567891011
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT (SELECT order_id FROM orders WHERE amount > 1000) AS big_order
Result in BigQuery
big_order
1null
1 row

A scalar subquery that returns more than one row is an error (“Scalar subquery produced more than one element”). Make it return one row with an aggregate, a unique condition, or ORDER BY ... LIMIT 1:

each customer's largest order
12345678910111213
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT name, (SELECT order_id FROM orders AS o WHERE o.customer_id = c.customer_id ORDER BY amount DESC LIMIT 1) AS top_order
FROM customers AS c
ORDER BY name
Result in BigQuery
nametop_order
1anao2
2beno3
3cainull
3 rows

ARRAY subqueries: many rows per outer row

ARRAY(subquery) collects all the rows the subquery returns into an array, so it can’t fail on too many rows. Customers with no orders get an empty array:

each customer's order IDs
123456789101112131415
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT
  name,
  ARRAY(SELECT order_id FROM orders AS o WHERE o.customer_id = c.customer_id ORDER BY order_id) AS order_ids
FROM customers AS c
ORDER BY name
Result in BigQuery
nameorder_ids
1ana[o1, o2]
2ben[o3]
3cai[]
3 rows

This is how you build nested data from flat tables, with an ORDER BY inside to fix the element order. See Arrays.

STRUCT subqueries: several values per outer row

SELECT AS STRUCT returns one row of several columns as a single struct value, so one subquery can compute several things:

order count and largest order per customer
123456789101112131415
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT
  name,
  (SELECT AS STRUCT COUNT(*) AS n, MAX(amount) AS biggest FROM orders AS o WHERE o.customer_id = c.customer_id) AS stats
FROM customers AS c
ORDER BY name
Result in BigQuery
namestats
1ana{n: 2, biggest: 45}
2ben{n: 1, biggest: 20}
3cai{n: 0, biggest: NULL}
3 rows

Read the fields with stats.n and stats.biggest, or expand them with stats.*. See STRUCT.

IN and EXISTS

IN (subquery) and EXISTS (subquery) filter rows without joining:

customers with an order over 25
1234567891011121314
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT name
FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders WHERE amount > 25)
ORDER BY name
Result in BigQuery
name
1ana
1 row

NOT IN returns no rows at all when the subquery contains a NULL; use NOT EXISTS. See NOT IN with a NULL.

Subqueries inside subqueries

Subqueries nest, and an inner subquery can use the result of another:

the largest order above the average
1234567891011121314
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT name, (SELECT MAX(amount) FROM orders WHERE amount > (SELECT AVG(amount) FROM orders)) AS above_avg_max
FROM customers
ORDER BY name
LIMIT 1
Result in BigQuery
nameabove_avg_max
1ana45
1 row

No LATERAL

Postgres and others have LATERAL for a subquery in FROM that refers to earlier tables. BigQuery doesn’t:

LATERAL in FROMBigQuery rejects this query
123456789101112
WITH customers AS (
  SELECT 1 AS customer_id, 'ana' AS name UNION ALL
  SELECT 2, 'ben' UNION ALL
  SELECT 3, 'cai'
),
orders AS (
  SELECT 'o1' AS order_id, 1 AS customer_id, 30 AS amount UNION ALL
  SELECT 'o2', 1, 45 UNION ALL
  SELECT 'o3', 2, 20
)
SELECT c.name, o.order_id
FROM customers AS c, LATERAL (SELECT order_id FROM orders AS o WHERE o.customer_id = c.customer_id) AS o
Error in BigQuery

LATERAL join is not supported at [12:22]

Use a correlated ARRAY() subquery and UNNEST it, a join, or for arrays the correlated FROM t, UNNEST(t.items) form, which is a lateral join in all but name. See UNNEST.

When BigQuery can’t run a correlated subquery

BigQuery runs correlated subqueries by rewriting them into joins. When it can’t, the query fails with “Correlated subqueries that reference other tables are not supported unless they can be de-correlated, such as by transforming them into an efficient JOIN.” Rewrite the subquery as a join with a pre-aggregated table, which is also usually faster:

SELECT c.name, t.total
FROM customers AS c
LEFT JOIN (
  SELECT customer_id, SUM(amount) AS total
  FROM orders
  GROUP BY customer_id
) AS t USING (customer_id);

Cost

A subquery’s tables are billed like any other read: once per column per query, however many times the subquery runs for the outer rows. In Querylab.io, the diamond in the editor margin on a subquery’s first line shows its bytes and cost, and Shift-click runs it as a top-level query (Partial execution). See How a query is billed.