I have these three tables:
"courses table"
id course_title chapters_count 1 course 1 5 2 course 2 3 "chapters table"
id chapter_title sub_chapters_count course_id –references courses(id) 1 chapter 1 2 1 2 chapter 2 4 1 3 chapter 3 1 1 4 chapter 4 3 1 5 chapter 5 0 1 6 chapter 1 4 2 7 chapter 2 5 2 8 chapter 3 0 2 "sub_chapters table"
id sub_chapter_title chapter_id –references chapters(id) 1 sub chapter 1 1 2 sub chapter 2 1 3 sub chapter 1 2 4 sub chapter 2 2 5 sub chapter 3 2 6 sub chapter 4 2 7 sub chapter 1 3 … …………. …
I want the result to be in JSON format like this:
{
"course": [
{
"course_id": 1,
"course_title": "course 1",
"chapters_count": 5,
"chapters": [
{
"chapter_id": 1,
"chapter_title": "chapter 1",
"sub_chapters": [
{
"sub_chapter_id": 1,
"sub_chapter_title": "sub chapter 1"
},
{
"sub_chapter_id": 2,
"sub_chapter_title": "sub chapter 2"
},
{
"sub_chapter_id": 3,
"sub_chapter_title": "sub chapter 3"
}
]
},
{
"chapter_id": 2,
"chapter_title": "chapter 2",
"sub_chapters": [
{
"sub_chapter_id": 1,
"sub_chapter_title": "sub chapter 1"
},
{
"sub_chapter_id": 2,
"sub_chapter_title": "sub chapter 2"
}
]
}
]
}
]
}
2
Answers
The most complicated part of this type of query is determining how to build the nested aggregates since PostgreSQL doesn’t support nesting aggregate functions.
Run the following SQL to setup a demonstration environment (note that tables do not include a count of subordinate tables’ associated rows):
The following query uses
LATERAL
subqueries to get the nested aggregates:The resulting output is:
The
JSON_*
functions can be changed to theirJSONB_*
equivalents; however, the order of elements within the JSON output might be different. The counts could also be removed without losing any information since the number of elements in an array can be determined easily.The key insight to generating nested JSON objects is to avoid using
JOIN
s and instead write subqueries. The target structure then is mirrored nicely by the query structure:(online demo – thanks to JohnH for the setup SQL)
An alternative to the
ARRAY(SELECT … FROM …)
subqueries is(SELECT jsonb_agg(…) FROM …)
, however that returnsNULL
when no row is found – you’d have to useCOALESCE((SELECT jsonb_agg(…) FROM …), '[]')
to work around that.