Nested json_agg - sql

I have this postgresql query, which returns a json containing all books titles, the first and lastname of their authors and an array of all reviews titles + the first and lastname of the reviewer.
SELECT json_build_object(
'title', a.title,
'author_firstname', b.firstname,
'author_lastname', b.lastname,
'reviews', json_agg(json_build_object(
'review_title', c.title,
'reviewer_firstname', d.firstname,
'reviewer_lastname', d.lastname
))
)
FROM book AS a
INNER JOIN person AS b ON a.author_id = b.id
LEFT JOIN review AS c ON c.book_id = a.id
INNER JOIN person AS d ON c.reviewer_id = d.id
GROUP BY a.id, b.id
This works and is quite fast. I need to extend this query to include all the titles of the books of the reviewers too.
I followed the logic I used for the first left join
SELECT json_build_object(
'title', a.title,
'author_firstname', b.firstname,
'author_lastname', b.lastname,
'reviews', json_agg(json_build_object(
'review_title', c.title,
'reviewer_firstname', d.firstname,
'reviewer_lastname', d.lastname,
'reviewer_books', json_agg(json_build_object(
'book_title', e.title
))
))
)
FROM book AS a
INNER JOIN person AS b ON a.author_id = b.id
LEFT JOIN review AS c ON c.book_id = a.id
INNER JOIN person AS d ON c.reviewer_id = d.id
LEFT JOIN book AS e ON e.author_id = d.id
GROUP BY a.id, b.id
But this does not work because aggregate function calls cannot be nested. Is there a solution to this, which can be extended to include even deeper relations (like all the reviews of all the books of the reviewer) and is relatively efficient.
Those databases are confusing me, so thank you for your help!

You need multiple levels of aggregation. This is something like:
SELECT json_build_object(
'title', b.title,
'author_firstname', p.firstname,
'author_lastname', p.lastname,
r.reviews
)
FROM book b INNER JOIN
person p
ON b.author_id = pb.id LEFT JOIN
(SELECT r.book_id,
json_agg(json_build_object('review_title', r.title,
'reviewer_firstname', pr.firstname,
'reviewer_lastname', pr.lastname,
'reviewer_books', rb.reviewer_books
)
) as reviews
FROM review r JOIN
person pr
on r.reviewer_id = pr.id JOIN
(SELECT r2.reviewer_id, json_agg(json_build_object('book_title', b2.title) as reviewer_books
FROM reviews r2 JOIN
book b2
ON r2.book_id = b2.id
GROUP BY r2.reviewer_id
) rb
on r.reviewer_id = rb.reviewer_id
) r
on r.book_id = b.id;

Related

How do I sum the total of books sold based on title of the book?

select b.ISBN, b.title, a.author_name, b.unit_price, p.quantity
from books as b left join
purchases as p
on b.ISBN = p.book_id left join
authors as a
on b.author_id = a.id;
you can use group by :
select
b.ISBN,
b.title,
a.author_name,
b.unit_price,
sum(p.quantity) as Sold
from
books as b
left join purchases as p on b.ISBN = p.book_id
left join authors as a on b.author_id = a.id
group by
b.ISBN,
b.title,
a.author_name,
b.unit_price

SQL union two complex query

There are 4 tables,
posts(id
post_share(shared_by, post_id
friendship(friend_one, friend_two, status)
add_viewer(id
By using above 4 tables I need to get posts to render in the user's news feed. Currently I only use the first query to do that which means I do not add shared post by user's friends to his/her news feed. Now I need to show the shared post, too just like in facebook.
For that I created new query ( 2nd one ). Now I want to connect both of them together and result only one table or I would like to know if there's a better way to handle this
By assuming currently logged in user_id is 22,
SELECT concat(a.fname, ' ', a.lname) as name, a.id as user_id , p.id as post_id, p.content, p.media FROM posts p
INNER JOIN add_viewers a
ON p.user_id = a.id
WHERE p.user_id in (
SELECT a.id FROM friendships f
INNER JOIN add_viewers a
ON f.friend_one = a.id OR f.friend_two = a.id
WHERE friend_one=22 OR friend_two=22 AND f.status='confirmed'
GROUP BY a.id
)
ORDER BY created_at DESC
LIMIT 10
OFFSET 0
in 2nd query, s.shared_by means the person who share the post and p.user_id means the person who created the post.
SELECT concat(a.fname, ' ', a.lname) as name, a.id as user_id , p.id as post_id, p.content, p.media FROM post_shares s
INNER JOIN posts p
ON p.id = s.post_id
INNER JOIN add_viewers a
ON p.user_id = a.id
WHERE s.shared_by in (
SELECT a.id FROM friendships f
INNER JOIN add_viewers a
ON f.friend_one = a.id OR f.friend_two = a.id
WHERE friend_one=22 OR friend_two=22 AND f.status='confirmed'
GROUP BY a.id
)
ORDER BY created_at DESC
LIMIT 10
OFFSET 0
Try this
With t as
(SELECT a.id as aid FROM friendships f
INNER JOIN add_viewers a
ON f.friend_one = a.id OR
f.friend_two = a.id
WHERE friend_one=22 OR
friend_two=22 AND
f.status='confirmed'
GROUP BY a.id)
SELECT concat(a.fname, ' ', a.lname) as name, a.id as user_id , p.id as post_id, p.content, p.media FROM
post_shares s
INNER JOIN posts p
ON p.id = s.post_id
INNER JOIN t
ON p.user_id = t.aid
OR s.shared_by = t.aid
ORDER BY created_at DESC
LIMIT 10
OFFSET 0

Postgres query returning books with only one author

I'm trying to make a query returning the book title, the book summary and the author name from the books with only one author ordered by the book id. A book can have many authors and an author can write many books, so it's a many to many relationship, but I'm making something wrong or forgetting something.
My tables are:
book_tb(id, title, summary);
author_tb(id, name);
book_author_tb(id, book_id, author_id);
And I have tried:
SELECT b.title, b.summary, a.name
FROM book_tb b
INNER JOIN book_author_tb ba ON b.id = ba.book_id
INNER JOIN author_tb a ON ba.author_id = a.id
GROUP BY b.title, b.summary, a.name, b.id
HAVING count(ba.author_id) = 1
ORDER BY b.id;
One way of doing this is:
SELECT b.title, b.summary, a.name
FROM book_author_tb ba
INNER JOIN book_tb b ON b.id = ba.book_id
INNER JOIN author_tb a ON a.id = ba.author_id
WHERE (
SELECT COUNT(*)
FROM book_author_tb
WHERE book_id = b.id
) = 1
ORDER BY b.id
This:
SELECT COUNT(*) FROM book_author_tb WHERE book_id = b.id
makes sure that the books returned have only 1 author.
Another way (maybe more efficient):
SELECT b.title, b.summary, a.name
FROM (
SELECT book_id, MAX(author_id) author_id
FROM book_author_tb
GROUP BY book_id
HAVING COUNT(*) = 1
) ba
INNER JOIN book_tb b ON b.id = ba.book_id
INNER JOIN author_tb a ON a.id = ba.author_id
ORDER BY b.id
I would do this with just aggregation:
SELECT b.title
,b.summary
,MAX(a.name) AS name
FROM book_author_tb AS ba
INNER JOIN book_tb AS b
ON b.id = ba.book_id
INNER JOIN author_tb AS a
ON a.id = ba.author_id
GROUP BY b.title
,b.summary
HAVING count(*) = 1;
This version is counting the number of authors on each book (assuming no duplicates, which is reasonable).
Your version is also aggregating by the author. The count() is only going to be "1" in that case.
You should calculate count of authors the filter you want in a different subselect. So you can use it in you WHERE statement from now on.
SELECT b.title, b.summary, a.name FROM book_tb b
JOIN book_author_tb ba ON b.id = ba.book_id
JOIN author_tb a ON ba.author_id = a.author_id
GROUP BY b.title, b.summary, a.name, b.id
WHERE (SELECT count(ba.author_id)
FROM book_author_tb
WHERE book_id = b.id) = 1
ORDER BY b.id;

Finding the next result when none is found?

I am attempting to traverse a hierarchy with a CTE and it works fine in one scenario but not another and that is where I am stuck.
Given the query;
;WITH BOMcte (ID, Code, BomName , ProductID, ProductCode, ProductName , ParentAssemblyID )
AS
(
SELECT b.id,
b.code,
b.name,
p.id,
p.default_code,
p.name_template,
b.bom_id
FROM mrp_bom AS b
INNER JOIN product_product p on b .product_id = p.id
WHERE b. bom_id IS NULL
and b.id = #AssemblyID
UNION ALL
SELECT b.id,
b.code,
b.name,
p.id,
p.default_code,
p.name_template,
b.bom_id
FROM mrp_bom AS b
INNER JOIN product_product p on b .product_id = p.ID
INNER JOIN BOMcte AS cte ON b.bom_id = cte.ID
)
SELECT BoM.* FROM BOMcte BoM
The query works just as I expected because the BoM drills down to the child boms on the column bom_id.
In code (from OpenERP) when a child BoM isn't found, (no bom_id) a child product is searched for based on the product_id:
sids = bom_obj.search(cr, uid, [('bom_id','=',False),('product_id','=',bom.product_id.id)])
I am wondering if there is a method I can use to accomplish the same thing in SQL. Once the CTE doesn't return rows, check with the product_id and a null bom_id. I had thought about another recursive member but I don't think that's what I am looking for.
I know my question probably isn't very clear but, any suggestions?
SQL Fiddle example data here: http://sqlfiddle.com/#!3/b9052/1
The reason why trying the following as HABO suggested on b.bom_id = cte.ID or ( b.bom_id is NULL and b.product_id = cte.product_id ) and you already tried doesn't work is because it never logically terminates.
However you do have a terminating expression which is do it once when no children are found. The easiest way to that is to add a UNION which checks to make sure that a row in BOMcte has no child
WHERE NOT EXISTS (SELECT * FROM BOMcte bc WHERE b.id = bc.PARENTASSEMBLYID)
Full SQL
;WITH BOMcte (ID, Code, BomName , ProductID, ProductCode, ProductName , ParentAssemblyID )
AS
(
SELECT b.id,
b.code,
b.name,
p.id,
p.default_code,
p.name_template,
b.bom_id
FROM mrp_bom AS b
INNER JOIN product_product p on b .product_id = p.id
WHERE b. bom_id IS NULL
and b.id = #AssemblyID
UNION ALL
SELECT b.id,
b.code,
b.name,
p.id,
p.default_code,
p.name_template,
b.bom_id
FROM mrp_bom AS b
INNER JOIN product_product p on b .product_id = p.ID
INNER JOIN BOMcte AS cte ON b.bom_id = cte.ID
)
SELECT * FROM BOMcte
UNION
SELECT b.id,
b.code,
b.name,
p.id,
p.default_code,
p.name_template,
b.bom_id
FROM mrp_bom AS b
INNER JOIN product_product p on b.product_id = p.id
WHERE NOT EXISTS (SELECT * FROM BOMcte bc WHERE b.id = bc.PARENTASSEMBLYID)
SQL DEMO
Note: It may be possible to encode the terminating expression in the CTE using an incrementing LEVEL value like those found in the MSDN article on Recursive Queries
I'm a little unclear on what you are trying to do, but something like this for your final join may do it:
on b.bom_id = cte.ID or ( b.bom_id is NULL and b.product_id = cte.product_id )

sql triple join: ambigious attribute name on a count

So I want to count a number of books, but the books are stored in 2 different tables with the same attribute name.
I want to get a result that looks like:
name1 [total number of books of 1]
name2 [total number of books of 2]
I tried this triple join;
SELECT DISTINCT name, count(book)
FROM writes w
LEFT JOIN person p on p.id = w.author
LEFT JOIN book b on b.title = w.book
LEFT JOIN controls l on l.controller=p.id
GROUP BY name
ORDER BY name DESC
but since book exists as an attribute in writes and in controls, it cant execute the query.
It can only do it if I leave out one of joins so it can identify book.
How can I tell the sql engine to count the number of both book attributes together for each person?
As a result of database design that you interested in, you should issue 2 different sql and then merge them to handle single output.
A)
SELECT DISTINCT w.name as 'Name', count(w.book) as 'Cnt'
FROM writes w
LEFT JOIN person p on p.id = w.author
LEFT JOIN book b on b.title = w.book
B)
SELECT DISTINCT l.name as 'Name', count(l.book) as 'Cnt'
FROM controls l
LEFT JOIN person p on p.id = l.controller
LEFT JOIN book b on b.title = l.book
For your purpose, you can get UNION of A and B.
or you can use them as data source on a third SQL
select A.Name, sum(A.Cnt+B.Cnt)
from A, B
where A.Name = B.Name
group by A.Name
order by A.Name
WITH T AS
(
SELECT DISTINCT 'WRITES' FROMTABLE, w.name, w.count(book)
FROM writes w
LEFT JOIN person p on p.id = w.author
LEFT JOIN book b on b.title = w.book
GROUP BY name
UNION ALL
SELECT DISTINCT 'CONTROLLS' FROMTABLE, c.name, count(c.book)
FROM controlls c
LEFT JOIN person p on p.id = c.author
LEFT JOIN book b on b.title = c.book
GROUP BY name
)
SELECT * FROM T ORDER BY NAME
Should work.
HTH
This will work on a per distinct author's ID to how many books they've written. The pre-aggregation will return one record per author with how many books by that author. THEN, join to the person table to get the name. The reason I am leaving it by ID and Name of the author is... what if you have two authors "John Smith", but they have respective IDs of 123 and 389. You wouldn't want these rolled-up to the same person (or do you).
select
P.ID,
P.Name,
PreAgg.BooksPerAuthor
from
( select
w.author,
count(*) BooksPerAuthor
from
writes w
group by
w.author ) PreAgg
JOIN Person P
on PreAgg.Author = P.id
order by
P.Name