Finding the next result when none is found? - sql

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 )

Related

Nested json_agg

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;

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;

Postgresql returning repeating data on multiple join

I can't figure out why my output is duplicating, but I'm sure it is incorrect joins. I'm new to SQL in general, so this may be an inefficent query.
SELECT a.id, a.number, a.number2, b.id, b.number3, b.number4, c.id, c.score, (a.number - b.number3) as a_b_difference, (a.number2 - b.number4) as a_b_difference3 FROM file a
INNER JOIN file b on a.id = b.id
INNER JOIN file c on a.id = c.id
I want to subtract the two fields and combine all three files on the id. However, my result populates with repeating data.
This is a sample of what I am getting:
c.id is a.id, so that join is not needed:
SELECT a.id, a.number, a.number2, b.id, b.number3, b.number4, a.id, c.score,
(a.number - b.number3) as a_b_difference,
(a.number2 - b.number4) as a_b_difference3
FROM file a INNER JOIN
file b
ON a.id = b.id;
As for the rest of your problem, I don't see duplication. If too many rows are being generated, perhaps you need to fix your JOIN conditions. You could use SELECT DISTINCT() if the entire row were duplicated. If you want one row from within a group, you can use GROUP BY or DISTINcT ON.
SELECT distinct a.id, a.number, a.number2, b.id, b.number3, b.number4, c.id, c.score, (a.number - b.number3) as a_b_difference, (a.number2 - b.number4) as a_b_difference3 FROM file a
INNER JOIN file b on a.id = b.id
INNER JOIN file c on a.id = c.id

How can I add a column in sql query from nested join?

I am using Access and having three tables: Category, Topic and Post. What I am trying to achieve is to include in the result the date of the last post creation.
Post table has a CreatedOn column.
Currently my query looks like this:
SELECT
category.id,
category.CategoryName,
category.Description,
count(tp.topic.id) AS NumberOfTopics,
Sum(numofposts) AS NumberOfPosts
FROM category
LEFT JOIN (
SELECT
topic.id,
topic.categoryId,
count(post.id) AS numofposts
FROM
topic
LEFT JOIN post ON topic.id = post.topicId
GROUP BY topic.id, topic.categoryId
) AS TP ON category.id=TP.categoryid
GROUP BY category.id, category.CategoryName, category.Description;
My best attempt (in my opinion) was to extend a query in a following way:
SELECT
category.id,
category.CategoryName,
category.Description,
COUNT(topic.id) AS NumberOfTopics,
sum(numofposts) AS NumberOfPosts,
"DUMMY" AS last
FROM category
LEFT JOIN (
SELECT
topic.id,
COUNT(ps.id) AS numofposts,
topic.categoryId
FROM topic
LEFT JOIN (
SELECT
id,
CreatedOn,
topicId
FROM post
ORDER BY post.CreatedOn DESC
) AS ps ON topic.id = ps.topicId
GROUP BY topic.id, topic.categoryId
) AS TP ON category.id=TP.categoryid
GROUP BY category.id, category.CategoryName, category.Description;
Unfortunately I've tried a many different ways to get it, but I am still unsuccessful.
Thanks in advance.
Using inline SELECT clauses:
SELECT
c.id,
c.CategoryName,
c.Description,
(SELECT count(t.id)
FROM topic t
WHERE t.categoryId = c.id
) AS NumberOfTopics,
(SELECT count(p.id)
FROM post p
JOIN topic t ON p.topicId = t.id
WHERE t.categoryId = c.id
) AS NumberOfPosts,
(SELECT max(p.createdOn) FROM post p
JOIN topic t ON p.topicId = t.id
WHERE t.categoryId = c.id
) AS LastPostDate
FROM category c;
This may not be the most efficient query, but produces the right results.
See http://www.sqlfiddle.com/#!3/165d1/2 for a demo.

Optimal SQLite Query Remove Duplicates

I have two tables with the following setup:
category: (id, name)
item: (id, name, category_id) - category_id is foreign key to category table
Now I am writing a query to retrieve a subset from the category table of only used categories:
SELECT c.id, c.name
FROM category c
WHERE c.id IN (SELECT DISTINCT category_id FROM item)
The above query works fine. I'm just wondering if this is the most optimal way of doing the query or if there's something else that I could do via a join or something
Transforming the IN (SELECT) to EXISTS (SELECT ... WHERE ) might help:
SELECT c.id, c.name
FROM category c
WHERE EXISTS (SELECT 1 FROM item WHERE item.category_id = c.id)
Another possibility (I expect it to be slower, but it always depends on your db):
SELECT c.id, c.name
FROM category c
INNER JOIN item ON item.category_id = c.id
GROUP BY c.id
Or you could use DISTINCT instead of GROUP BY:
SELECT DISTINCT c.id, c.name
FROM category c
INNER JOIN item ON item.category_id = c.id
And if speed is that important, don't forget to call ANALYZE from time to time:
http://www.sqlite.org/lang_analyze.html
Some other variants for fun:
SELECT c.id, c.name
FROM category c
INNER JOIN (SELECT DISTINCT item.category_id ) AS i_c ON i_c.category_id = c.id
Another:
SELECT c.id, c.name
FROM category c
EXCEPT
SELECT c.id, c.name
FROM category c
LEFT JOIN item ON item.category_id = c.id
WHERE item.category_id IS NULL
Use Join:
SELECT c.id, c.name
FROM category c
JOIN item i on c.id=i.category_id
GROUP BY c.id, c.name