GROUP BY including 0 where none present - sql

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

Related

Efficiently getting multiple counts of foreign key rows in PostgreSQL

I have a database that consists of users who can perform various actions, which I keep track of in multiple tables. I'm creating a point system, so I need to count how many of each type of action the user did. For example, if I had:
users posts comments shares
id | username id | user_id id | user_id id | user_id
------------- -------------- -------------- --------------
1 | abc 1 | 1 1 | 1 1 | 2
2 | xyz 2 | 1 2 | 2 2 | 2
I would want to return:
user_details
id | username | post_count | comment_count | share_count
---------------------------------------------------------
1 | abc | 2 | 1 | 0
2 | xyz | 0 | 1 | 2
This is slightly different from this question about foreign key counts since I want to return the individual counts per table.
What I've tried so far (example code):
SELECT
users.id,
users.username,
COUNT( DISTINCT posts.id ) as post_count,
COUNT( DISTINCT comments.id ) as comment_count,
COUNT( DISTINCT shares.id ) as share_count
FROM users
LEFT JOIN posts ON posts.user_id = users.id
LEFT JOIN comments ON comments.user_id = users.id
LEFT JOIN shares ON shares.user_id = users.id
GROUP BY users.id
While this works, I had to use DISTINCT in all of my counts because the LEFT JOINS were causing high numbers of duplicate rows. I feel like there must be a better way to do this since (please correct me if I'm wrong) on each LEFT JOIN, the DISTINCT is having to filter out an exponentially growing number of duplicated rows.
Thank you so much for any help you could give me with this!
You can join derived tables that already do the aggregation.
SELECT u.id,
u.username,
coalesce(pc.c, 0) AS post_count,
coalesce(cc.c, 0) AS comment_count,
coalesce(sc.c, 0) AS share_count
FROM users AS u
LEFT JOIN (SELECT p.user_id,
count(*) AS cc
FROM posts AS p
GROUP BY p.user_id) AS pc
ON pc.user_id = u.id
LEFT JOIN (SELECT c.user_id,
count(*) AS
FROM comments AS c
GROUP BY c.user_id) AS cc
ON cc.user_id = u.id
LEFT JOIN (SELECT s.user_id,
count(*) AS c
FROM shares AS s
GROUP BY s.user_id) AS sc
ON sc.user_id = u.id;

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.

How to make a comparison for the record that has rows to another rows? [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 3 years ago.
Improve this question
I have first table that has columns:
1. id
2. key
3. value
And second table(more like the list):
key
I need to get distinct id that contains all keys from second table
I have tried self join but it is very slow. Also I tried COUNT = COUNT but performance the same.
Self join:
select f.id from first
join first f2 on f.id = f2.id AND f2.key = f. key
COUNT:
select a.keyfrom #a a
where ( select SUM(CASE WHEN k.[key] is not NULL THEN 1 ELSE 0 END) from [b] b
LEFT JOIN Second s on s.key= b.[Key]
where b.[Key] = a.key) = #KeyCount
You can also check this-
SELECT A.id
FROM TAB1 A
INNER JOIN TAB2 B ON A.[key] = B.[Key]
GROUP BY A.id
HAVING COUNT(DISTINCT A.[key])
= (SELECT COUNT(DISTINCT [Key]) FROM TAB2)
This is somewhat of a stab in the dark, but perhaps this is what you're after...?
SELECT I.ID
FROM TableB B
CROSS APPLY (SELECT DISTINCT ca.ID
FROM dbo.TableA ca) I
LEFT JOIN TableA A ON B.[key] = A.[key]
AND I.ID = A.ID
GROUP BY I.ID
HAVING COUNT(CASE WHEN A.[Key] IS NULL THEN 1 END) = 0;
db<>fiddle
Assuming:
Your 2nd table lists all possible keys, and
Your first table (containing entity IDs, keys, and key values) can only contain 1 entity-key combination,
something like this may work:
SELECT [id], COUNT(*)
FROM Table1
GROUP BY [id]
HAVING COUNT(*) = (SELECT COUNT(*) FROM keys)
Now with some sample data. Assume the following keys:
+--------+----------+
| key_id | key_name |
+--------+----------+
| 1 | Key1 |
| 2 | Key2 |
| 3 | Key3 |
+--------+----------+
And the following entities:
+----+-----+-------+
| id | key | value |
+----+-----+-------+
| 1 | 1 | 1 |
| 1 | 2 | 2 |
| 1 | 3 | 3 |
| 2 | 2 | 2 |
| 2 | 3 | 3 |
+----+-----+-------+
Assume how Entity 1 has all keys, but Entity 2 is missing Key 1. So, as expected, the query returns only Entity 1.
You can use aggregation for the counting:
select f.id
from first f
where exists (select 1 from second s where s.key = f.key)
group by f.id
having count(*) = (select count(*) from second);
This assumes that there are no duplicates in the table. It also assumes that extra keys in first are ok. If not, use left join:
select f.id
from first f left join
second s
on s.key = f.key
group by f.id
having count(s.key) = (select count(*) from second) and
count(*) = count(s.key);

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.

Find rows with two or more relationships

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)