N054-H2 Tier 4 · Advanced · hard ecommerce · Brightlane

Return every category ID and a JSON array of objects — one per product in that category — where each object contains the product's `id` under the key `'id'` and the product's color attribute under the key `'color'`

Part of JSONB Aggregation (jsonb_agg, json_build_object) in SQL

The problem

Brightlane's data completeness audit needs every product represented in a per-category export, regardless of whether a 'color' attribute is recorded.

Write a query to return every category ID and a JSON array of objects — one per product in that category — where each object contains the product's id under the key 'id' and the product's color attribute under the key 'color'.

Assumptions:

  • Each category_id with at least one product should appear once.
  • For each category, the array contains one object per product (no de-duplication). Each object has exactly two keys, 'id' and 'color'. Products with a 'color' key in attributes populate that key with the text color value; products without one populate that key with JSON null.
  • The order of objects within each array is unspecified.

Output:

  • One row per category, with columns category_id and product_colors.
Schema · ecommerce 5 tables
categories
id integer
name text
parent_id? integer
products
id integer
name text
category_id integer
price numeric
stock_qty integer
attributes? jsonb
order_items
id integer
order_id integer
product_id integer
quantity integer
unit_price numeric
customers
id integer
name text
email text
city? text
country text
created_at timestamptz
is_active boolean
orders
id integer
customer_id integer
ordered_at timestamptz
status text
total_amount numeric

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution Try it yourself first
Solution query
SELECT
  category_id,
  JSONB_AGG(
    JSON_BUILD_OBJECT('id', id, 'color', attributes ->> 'color')
  ) AS product_colors
FROM
  products
GROUP BY
  category_id

The shape

The audit needs every product represented, even ones with no 'color' on record. Wrapping the per-row build in json_build_object('id', id, 'color', attributes->>'color') is what makes that work: json_build_object writes JSON null for any NULL argument but always emits the row's object, so every product produces one element in the outer jsonb_agg array.

Clause by clause

  • SELECT category_id, jsonb_agg(json_build_object('id', id, 'color', attributes->>'color')) AS product_colors builds a two-key object for each product row, then aggregates the objects per category. The 'id' value is always present. The 'color' value comes from attributes->>'color', which returns NULL when the key is absent — json_build_object writes that out as JSON null, keeping the key in place. The full object is never NULL, so jsonb_agg never drops a row.
  • FROM products reads the product records.
  • GROUP BY category_id partitions by category. Category 4 returns six objects, each with the product's id and color: null, because none of the books have a 'color' key. Category 5 returns six objects, each with a real color value.

The trap

The same audit written as jsonb_agg(attributes->>'color') would drop every key from the output and surface only the color values, which is the wrong shape. But the more dangerous near-miss is jsonb_agg(CASE WHEN attributes ? 'color' THEN json_build_object('id', id, 'color', attributes->>'color') END) — wrapping the build in a CASE that returns NULL when the color key is missing. The CASE returns NULL, the outer aggregate sees NULL, and jsonb_agg skips that row entirely. Whole products vanish from the audit. The fix is the one shown in the canonical query: keep json_build_object outside any NULL-producing branch, let it emit color: null for the missing rows, and rely on jsonb_agg keeping the row because the object itself is not NULL.

You practiced jsonb_agg(json_build_object(..., 'col', maybe_null))json_build_object writes JSON null for null arguments, and jsonb_agg includes the row, so every record appears in the array regardless of whether the JSONB-extracted value is missing.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.

Practice, feedback, mastery, review. That's the loop that turns reading into real skill.

Start free

No account, no credit card. Start solving in under a minute.