Build JSON object from one-to-many relationship data in a single query?
If containers can be empty, the currently accepted solution does not work for you. It has to be an outer join to preserve rows without match - to get equivalent results to the correlated subqueries you are using in your fiddle:
select *, array(select thing_id from container_thing where container_id = container.id) as "thingIds" from container
1.
SELECT to_json(sub) AS container_with_things
FROM (
SELECT c.*, json_agg(thing_id) AS "thingIds"
FROM container c
LEFT JOIN container_thing ct ON ct.container_id = c.id
WHERE c.id IN (<list of container ids>)
GROUP BY c.id
) sub;
2.
With more than a few rows per container (you mentioned 20) it's typically faster to aggregate before you join:
SELECT to_json(sub) AS container_with_things
FROM (
SELECT c.*, ct."thingIds"
FROM container c
LEFT JOIN (
SELECT container_id AS id, json_agg(thing_id) AS "thingIds"
FROM container_thing
WHERE container_id IN (<list of container ids>) -- repeat condition
GROUP BY 1
) ct USING (id)
WHERE c.id IN (<list of container ids>)
) sub;
3.
Or you can combine the ARRAY constructor you found with LEFT JOIN LATERAL
:
SELECT to_json(sub) AS container_with_things
FROM (
SELECT c.*, ct."thingIds"
FROM container c
LEFT JOIN LATERAL (
SELECT ARRAY (
SELECT thing_id
FROM container_thing
WHERE container_id = c.id
-- ORDER BY thing_id -- optional order for deterministic results
) AS "thingIds"
) ct ON true
WHERE c.id IN (<list of container ids>)
) sub;
- What is the difference between LATERAL and a subquery in PostgreSQL?
Might be faster, yet.
SQL Fiddle. (Extending @a_horse's fiddle.)
Note that the result for empty containers is subtly different in the three queries above:
"thingIds":[null]
"thingIds":null
"thingIds":[]
4.
In Postgres 9.5 (since you are using it) you could also work with jsonb
and its functionality and one less subquery:
SELECT jsonb_set(to_jsonb(c), '{thingIds}', "thingIds") AS container_with_things
FROM container c
LEFT JOIN (
SELECT container_id AS id, jsonb_agg(thing_id) AS "thingIds"
FROM container_thing
WHERE container_id IN (<list of container ids>) -- repeat condition
GROUP BY 1
) ct USING (id)
WHERE c.id IN (<list of container ids>);
Alternatively:
SELECT to_jsonb(c) || jsonb_build_object('thingIds', "thingIds") AS container_with_things
FROM ...
This looks like a good fit for Postgres' JSON functions:
select to_json(x)
from (
select c.*, json_agg(ct.thing_id) as "thingIds"
from container_thing ct
join container c on ct.container_id = c.id
group by c.id
) x
SQLFiddle example: http://sqlfiddle.com/#!15/cd9992/1