15

I have 2 tables:

Table person with columns: person_id, person_name
Table pet with columns: pet_id, owner_id, pet_name

person data:
1, 'John'
2, 'Jill'
3, 'Mary'

pet data:
1, 1, 'Fluffy'
2, 1, 'Buster'
3, 2, 'Doggy'

How to write a SELECT query from person left join pet on person_id = owner_id with aggregate functions so my result data looks like:

1, [{pet_id:1,pet_name:'Fluffy'},{pet_id:2,pet_name:'Buster'}], 'John'
2, [{pet_id:3,pet_name:'Doggy'}], 'Jill'
3, [],'Mary'

5 Answers 5

26

Use LEFT JOIN LATERAL and aggregate in the subquery:

SELECT p.person_id, COALESCE(pet.pets, '[]') AS pets, p.person_name
FROM   person p
LEFT   JOIN LATERAL (
   SELECT json_agg(json_build_object('pet_id', pet.pet_id
                                   , 'pet_name', pet.pet_name)) AS pets
   FROM   pet
   WHERE  pet.owner_id = p.person_id
   ) pet ON true
ORDER  BY p.person_id;  -- optional, Q suggests ordered results

db<>fiddle here

This way you do not need to aggregate results from the outer query. Simpler and cleaner when your outer query is more complex than the example in the question. When aggregating multiple related tables, it even becomes a necessity:

It is also typically much faster when there are selective predicates on the outer table person - which is the typical use case.

Make sure there is an index on pet(owner_id) to make it fast.
Or even one on pet(owner_id, pet_id, pet_name) or pet(owner_id) INCLUDE (pet_id, pet_name) in Postgres 11 or later, if your row isn't wide like in your example, and if you get index-only scans out of it.

Oh, and use json_build_object() to preserve attribute names for arbitrary selections:

Related:

Sign up to request clarification or add additional context in comments.

1 Comment

Great explanation! Saved me a lot of time reading through the docs on Lateral joins. Thank you!
8
select
    person_id,
    jsonb_agg(to_jsonb(pet) - 'owner_id'),
    person_name
from person
left join pet on person_id = owner_id
group by person_id;

 person_id |                                 jsonb_agg                                  | person_name 
-----------+----------------------------------------------------------------------------+-------------
         1 | [{"pet_id": 1, "pet_name": "Fluffy"}, {"pet_id": 2, "pet_name": "Buster"}] | John
         2 | [{"pet_id": 3, "pet_name": "Doggy"}]                                       | Jill
         3 | [null]                                                                     | Mary
(3 rows)

Db<>fiddle.

Comments

3

demo:db<>fiddle

select
    COALESCE(
        json_agg(row_to_json(row(p2.pet_id::text, p2.pet_name))) FILTER (WHERE pet_id IS NOT NULL), 
       '[]'
    ) as json,
    p1.person_name
from person p1
left join pet p2
    on p1.person_id = p2.owner_id
group by
    p1.person_name;
  1. FILTER clause to filter out NULL values. That creates a NULL value for Mary.
  2. If you want to add an empty JSON array: Use COALESCE, which replaces NULL with a default value

Comments

2

Postgres' built in JSON and aggregation functions can handle your requirement:

select
    json_agg(row_to_json(row(p2.pet_id::text, p2.pet_name))) as json,
    p1.person_name
from person p1
left join pet p2
    on p1.person_id = p2.owner_id
group by
    p1.person_name;

6 Comments

@TomBerghuis If you like to get a valid JSON array instead of a simple array, you could use json_agg() instead of array_agg()
thanks I'm trying your solution but getting the error SQL Error [42883]: ERROR: function json_agg(json, unknown) does not exist¶ Hint: No function matches the given name and argument types. You might need to add explicit type casts.
I got array_agg to work, but for the persons with no pets I am getting [{pet_id:null, pet_name:null}] how to filter this?
json_agg works for me dbfiddle.uk/…
Filter out the NULL values with FILTER (WHERE pet_id IS NOT NULL). If you want to create an empty array you can use COALESCE dbfiddle.uk/…
|
1

some lateral query can also be written in correlated subquery.
You want to select some columns from tableA and aggregate info from tableB. you can use select tableA columns, aggregate function from tableA.

It will work because aggregate functions in this context only generate one row.
Based on Erwin's answer. You can also:

SELECT
    p.person_id,
    p.person_name,
    (
        SELECT
            coalesce(json_agg(json_build_object('pet_id', pet.pet_id, 'pet_name', pet_name)), '[]') AS pets
        FROM
            pet
        WHERE
            pet.owner_id = p.person_id)
FROM
    person_p p;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.