Find rows with two or more relationships - sql

I have 3 tables:
Foods table stores all food items, Tags table stores all tags, FoodTagRelation stores the relation between food and tags. I want to write a query to select all Food that have exactly 2 tags with specified Ids (please read the SQL I have written at the bottom)
Foods Table
Id | FoodItem
----------------------
1 | Mango
2 | Custard
3 | Pizza
Tags Table
Id | TagName
----------------------
1 | Fruit
2 | Cold
3 | Hot
4 | Veg
FoodTagRelation
Id | FoodId | TagId
----------------------
1 | 1 | 1
2 | 1 | 4
3 | 2 | 1
4 | 2 | 2
5 | 2 | 4
Now I want to select all foods that have exactly two tags on it: e.g. select all foods which have both tags: Fruit and Cold.
I tried this query, but it returns all food with tags Fruit OR Cold.
select * from Foods
inner join FoodTagRelation
on
Foods.Id=FoodTagRelation.FoodId
where
tagid in ('1','2')
How can I re-write this query to only return foods that have BOTH tags?

For a more generic answer that allows you to change the tags for which you're searching:
DECLARE #Search_Tags TABLE (TagId INT)
INSERT INTO #Search_Tags (TagId) VALUES (1), (2)
SELECT
F.Id,
F.FoodItem
FROM
Foods F
INNER JOIN FoodTagRelation FTR ON
FTR.FoodId = F.Id
INNER JOIN #Search_Tags ST ON
ST.TagId = FTR.TagId
GROUP BY
F.Id,
F.FoodItem
HAVING
COUNT(*) = (SELECT COUNT(*) FROM #Search_Tags)

SELECT
F.id,
F.FoodItem
FROM
Foods F
INNER JOIN FoodTagRelation FTR
ON F.Id = FTR.FoodId
WHERE
FTR.tagid in('1','2')
GROUP BY
F.id,
F.FoodItem
HAVING
count(Distinct FTR.tagid) > 1
Features: uses count distinct, to prevent an issue with duplicate tagid's for a given FoodID in your FoodTagRelation table. (If you don't think that duplicates are a concern, then you can remove the 'distinct' keyword). Secondly, I kept your WHERE clause, because that allows you to look for specific tags, as opposed to just any two. Finally, I listed out your fields, because that was necessary in order to use the group by clause (which in turn, was necessary in order to use the HAVING clause.)

When you said "select all Food which exactly 2 tags", if a food have 3 tag which include Fruit and Cold and some other tag. Does it count?
Anyway, here is query to find food that have both Fruit and Cold.
SELECT *
FROM Foods f
INNER JOIN FoodTagRelation ft1
ON f.Id=ft1.FoodId
INNER JOIN FoodTagRelation ft2
ON f.Id=ft2.FoodId
WHERE
ft1.tagid = 1 AND ft2.tagid = 2

do a group by on FoodID and use having count(tagID) = 2
select *
from foods as f inner join foodtagrelation as ftr on f.id=ftr.foodid
where (ftr.tagid = 1 or ftr.tagid = 2)
group by f.foodid
having count(*) = 2

SELECT * from Foods where FoodId in (
select FoodID from FoodTagRelation where TagId in (1,2)
group by FoodId having count(*)=2
)
NOTE Updating my SQL because the Rusi seems to only care about Foods with exactly to tags where TagId is (1 or 2)

Related

Left join command is not showing all results

I have a table RESTAURANT:
Id | Name
------------------
0 | 'McDonalds'
1 | 'Burger King'
2 | 'Starbucks'
3 | 'Pans'
And a table ORDER:
Id | ResId | Client
--------------------
0 | 1 | 'Peter'
1 | 2 | 'John'
2 | 2 | 'Peter'
Where 'ResId' is a foreign key from RESTAURANT.Id.
I want to select the number of order per restaurant:
Expected result:
Restaurant | Number of orders
----------------------------------
'McDonalds' | 0
'Burguer King' | 1
'Starbucks' | 2
'Pans' | 0
Actual result:
Restaurant | Number of orders
----------------------------------
'McDonalds' | 0
'Burguer King' | 1
'Starbucks' | 2
Command used:
select r.Name, count(o.ResId)
from RESTAURANT r
left join ORDER o on r.Id like o.ResId
group by o.ResId;
Just fix the group by clause:
select r.name, count(*) as cnt_orders
from restaurants r
left join orders o on r.id = o.resid
group by r.id, r.name;
That way, the SELECT and GROUP BY clauses are consistent; I also added the restaurant id to the group, so potential restaurants having the same name are not aggregated together. I also changed like to =: this is more efficient, and does not alter the logic.
You could also phrase this with a subquery, so there is no need for outer aggregation. I would prefer:
select r.*,
(select count(*) from orders o where o.resid = r.id) as cnt_orders
from restaurants r
Your query should be generating an error because the select columns and the group by columns are incompatible. Just aggregate by the unaggregated columns in the select:
select r.Name, count(o.ResId)
from RESTAURANT r left join
ORDER o
on r.Id = o.ResId
group by r.Name;
Notes:
You might want to include r.id in the GROUP BY (and SELECT) in case restaurants can have the same name.
Note the use of = instead of LIKE. The ids look like numbers, so you should use number operations. LIKE is a string operation.
ORDER is a bad name for a table because it is a SQL keyword.
As a general rule, in a LEFT JOIN, you don't want the aggregation keys to be from the second table, because those values could be NULL.

After joining two queries (each having different columns) with UNION I'm getting only one column

I have joined two queries with UNION keyword (Access 2016). It looks like that:
SELECT ITEM.IName, Sum(STOCK_IN.StockIn) AS SumOfIN
FROM ITEM INNER JOIN STOCK_IN ON ITEM.IName = STOCK_IN.IName
GROUP BY ITEM.IName
UNION SELECT ITEM.IName, Sum(STOCK_OUT.StockOut) AS SumOfOut
FROM ITEM INNER JOIN STOCK_OUT ON ITEM.IName = STOCK_OUT.IName
GROUP BY ITEM.IName
I get the following result:
IName | SumOfIN
----------------
Abis Nig | 3
Abrotanum | 1
Acid Acet | 2
Aconite Nap | 2
Aconite Nap | 3
Antim Crud | 3
Antim Tart | 1
But I want the following result:
IName | SumOfIN | SumOfOut
----------------
Abis Nig | 3 | 0
Abrotanum | 1 | 0
Acid Acet | 2 | 0
Aconite Nap | 2 | 3
Antim Crud | 0 | 3
Antim Tart | 0 | 1
Can anyone tell me what changes should I make here?
You need to add dummy values for the third column where they don't exist in the table you are UNIONing. In addition, you need an overall SELECT/GROUP BY since you can have values for both StockIn and StockOut:
SELECT IName, SUM(SumOfIN), Sum(SumOfOut)
FROM (SELECT ITEM.IName, Sum(STOCK_IN.StockIn) AS SumOfIN, 0 AS SumOfOut
FROM ITEM INNER JOIN STOCK_IN ON ITEM.IName = STOCK_IN.IName
GROUP BY ITEM.IName
UNION ALL
SELECT ITEM.IName, 0, Sum(STOCK_OUT.StockOut)
FROM ITEM INNER JOIN STOCK_OUT ON ITEM.IName = STOCK_OUT.IName
GROUP BY ITEM.IName) s
GROUP BY IName
Note that column names in the result table are all taken from the first table in the UNION, so we must name SumOfOut in that query.
You can do this query without UNION at all:
select i.iname, si.sumofin, so.sumofout
from (item as i left join
(select si.iname, sum(si.stockin) as sumofin
from stock_in as si
group by si.iname
) as si
on si.iname = i.iname
) left join
(select so.iname, sum(so.stockout) as sumofout
from stock_out as so
group by so.iname
) as so
on so.iname = i.iname;
This will include items that have no stock in or stock out. That might be a good thing, or a bad thing. If a bad thing, then add:
where si.sumofin > 0 or so.sumofout > 0
If you are going to use union all, then you can dispense with the join to items entirely:
SELECT IName, SUM(SumOfIN), Sum(SumOfOut)
FROM (SELECT si.IName, Sum(si.StockIn) AS SumOfIN, 0 AS SumOfOut
FROM STOCK_IN as si
GROUP BY si.INAME
UNION ALL
SELECT so.IName, 0, Sum(so.StockOut)
STOCK_OUT so
GROUP BY so.IName
) s
GROUP BY IName;
The JOIN would only be necessary if you had stock items that are not in the items table. That would be a sign of bad data modeling.

Postgres SQL: getting group count

I have the following table
>> tbl_category
id | category
-------------
0 | A
1 | B
...|...
>>tbl_product
id | category_id | product
---------------------------
0 | 0 | P1
1 | 1 | P2
...|... | ...
I can use the following query to count the number of products in a category.
select category, count(tbl.product) from tbl_product
join tbl_category on tbl_product.category_id = category.id
group by catregory
However, there are some categories that never have any product belonging to. How do I get these to show up in the query result as well?
Use a left join:
select c.category, count(tbl.product)
from tbl_category c left join
tbl_product p
on p.category_id = c.id
group by c.category;
The table where you want to keep all the rows goes first (tbl_category).
Note the use of table aliases to make the query easier to write and to read.

Filtering out rows

I have simplified a table as an example
tray| food
-------+-------
1 | fruit
2 | veg
2 | fruit
2 | meat
3 | meat
4 | bread
What I want to find for each fruit, is the number of trays that ONLY contain that type of food. So the output should look like this:
food| count
-------+-------
fruit | 1
veg | 0
meat | 1
bread | 1
I tried writing a query:
SELECT fruit, COUNT(*)
FROM Inventory
WHERE NOT EXISTS (SELECT *
FROM Inventory I
WHERE I.tray = tray AND I.fruit<>fruit)
GROUP BY fruit;
However the table returned is incorrect and it looks like my sub query is wrong but it makes logical sense to me.
food | count
-------+-------
fruit | 2
veg | 1
bread | 1
meat | 2
It looks like tray 2 is counting once for fruit, meat and veg when it should not. But shouldn't that be ruled out by my NOT EXISTS subquery? How do I fix this?
Clever little problem. Here is one solution:
select f.food, count(t.tray)
from (select distinct food from t
) f left join
(select t.tray, min(food) as minfood, max(food) as maxfood
from tray t
group by tray
) t
on f.food = t.minfood and f.food = t.maxfood
group by f.food;
Getting a count of zero suggests a query where left join and group by would be useful.
select i1.food,
count(ft.tray)
from inventory i1
left join (
select i1.tray,
count(distinct i1.food) as num_food
from inventory i1
group by i1.tray
) ft on i1.tray = ft.tray and num_food = 1
group by i1.food;
The inner query (ft) counts the number of different foods per tray. The outer (main) query counts the number of trays per food for those trays that only contain a single type of food.
Online example: http://rextester.com/PUN27611

GROUP BY including 0 where none present

I have a table of lists, each of which contains posts. I want a query that tells me how many posts each list has, including an entry with a 0 for each list that doesn't have any posts.
eg.
posts:
id | list_id
--------------
1 | 1
2 | 1
3 | 2
4 | 2
lists:
id
---
1
2
3
should return:
list_id | num_posts
-------------------
1 | 2
2 | 2
3 | 0
I have done so using the following query, but it feels a bit stupid to effectively do the grouping and then execute another sub-query to fill in the blanks:
WITH "count_data" AS (
SELECT "posts"."list_id" AS "list_id", COUNT(DISTINCT "posts"."id") AS "num_posts"
FROM "posts"
INNER JOIN "lists" ON "posts"."list_id" = "lists"."id"
GROUP BY "posts"."list_id"
)
SELECT "lists"."id", COALESCE("count_data"."num_posts", 0)
FROM "lists"
LEFT JOIN "count_data" ON "count_data"."list_id" = "lists"."id"
ORDER BY "count_data"."num_posts" DESC
Thanks!
It'll be more efficient to left join directly, avoiding a seq scan with a big merge join in the process:
select lists.id as list_id, count(posts.list_id) as num_posts
from lists
left join posts on posts.list_id = lists.id
group by lists.id
If I understand your question, this should work:
SELECT List_ID, ISNULL(b.list_ID,0)
FROM lists a
LEFT JOIN (SELECT list_ID, COUNT(*)
FROM posts
GROUP BY list_ID
)b
ON a.ID = b.list_ID