8

I have three tables: users, items, and boards.

create table users (
    id serial primary key,
    name text
);  

create table items (
    id serial primary key,
    user_id int,
    title text
);  

create table boards (
    id serial primary key,
    user_id int,
    title text
);n

I want to select all the users with the number of items and boards each user has:

select
    users.*,
    count(items) as item_count,
    count(boards) as board_count
from users
    left join items on items.user_id = users.id
    left join boards on boards.user_id = users.id
group by users.id;

Unfortunately, this does not give me the right counts. The counts are way higher than they should be. What is happening, why, and how can I fix this?

2 Answers 2

11

The problem is caused by fact that two left joins produce duplicate values.

To solve this you can either count only distinct values

SELECT u.id, u.name,
       COUNT(DISTINCT i.id) item_count,
       COUNT(DISTINCT b.id) board_count
  FROM users u LEFT JOIN items i
    ON u.id = i.user_id LEFT JOIN boards b
    ON u.id = b.user_id
 GROUP BY u.id, u.name

or count items and boards per user separately and then join them with users

SELECT u.*, 
       COALESCE(i.item_count, 0) item_count, 
       COALESCE(b.board_count, 0) board_count
  FROM users u LEFT JOIN
(
  SELECT user_id, COUNT(*) item_count
    FROM items
   GROUP BY user_id
) i ON u.id = i.user_id LEFT JOIN
(
  SELECT user_id, COUNT(*) board_count
    FROM boards
   GROUP BY user_id
) b ON u.id = b.user_id

Here is SQLFiddle demo

Sign up to request clarification or add additional context in comments.

3 Comments

With a primary key on users.id, you can take name out of the group by in the first query. The second query ought to be the most efficient, of course.
peterm you wouldn't happen to be the guy on robot game would you?
@Brandon No I'm not the guy. But I'm not sure is it good or bad? ;)
3

I think this should do the trick

select
    u.*,
    (select count(*) from items where user_id = u.id) as item_count,
    (select count(*) from boards where user_id = u.id) as board_count
from users as u
group by u.id;

2 Comments

The group-by in the last line is not required.
... and not as efficient a method as joining to pre-aggregated result sets, unless you only wanted a very small subset of users included.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.