views:

674

answers:

3

Often you need to show a list of database items and certain aggregate numbers about each item. For instance, when you type the title text on Stack Overflow, the Related Questions list appears. The list shows the titles of related entries and the single aggregated number of quantity of responses for each title.

I have a similar problem but needing multiple aggregates. I'd like to display a list of items in any of 3 formats depending on user options:

  • My item's name (15 total, 13 owned by me)
  • My item's name (15 total)
  • My item's name (13 owned by me)

My database is:

  • items: itemId, itemName, ownerId
  • categories: catId, catName
  • map: mapId, itemId, catId

The query below gets: category name, count of item ids per category

SELECT
  categories.catName,
  COUNT(map.itemId) AS item_count
FROM categories
LEFT JOIN map
  ON categories.catId = map.catId
GROUP BY categories.catName

This one gets: category name, count of item ids per category for this owner_id only

SELECT categories.catName, 
COUNT(map.itemId) AS owner_item_count
FROM categories
LEFT JOIN map
  ON categories.catId = map.catId
LEFT JOIN items
  ON items.itemId = map.itemId
WHERE owner = @ownerId
GROUP BY categories.catId

But how do i get them at the same time in a single query? I.e.: category name, count of item ids per category, count of item ids per category for this owner_id only

Bonus. How can I optionally only retrieve where catId count != 0 for any of these? In trying "WHERE item_count <> 0" I get:

MySQL said: Documentation
#1054 - Unknown column 'rid_count' in 'where clause'
+4  A: 

Here's a trick: calculating a SUM() of values that are known to be either 1 or 0 is equivalent to a COUNT() of the rows where the value is 1. And you know that a boolean comparison returns 1 or 0 (or NULL).

SELECT c.catname, COUNT(m.catid) AS item_count,
  SUM(i.ownerid = @ownerid) AS owner_item_count
FROM categories c
  LEFT JOIN map m USING (catid)
  LEFT JOIN items i USING (itemid)
GROUP BY c.catid;

As for the bonus question, you could simply do an inner join instead of an outer join, which would mean only categories with at least one row in map would be returned.

SELECT c.catname, COUNT(m.catid) AS item_count,
  SUM(i.ownerid = @ownerid) AS owner_item_count
FROM categories c
  INNER JOIN map m USING (catid)
  INNER JOIN items i USING (itemid)
GROUP BY c.catid;

Here's another solution, which is not as efficient but I'll show it to explain why you got the error:

SELECT c.catname, COUNT(m.catid) AS item_count,
  SUM(i.ownerid = @ownerid) AS owner_item_count
FROM categories c
  LEFT JOIN map m USING (catid)
  LEFT JOIN items i USING (itemid)
GROUP BY c.catid
HAVING item_count > 0;

You can't use column aliases in the WHERE clause, because expressions in the WHERE clause are evaluated before the expressions in the select-list. In other words, the values associated with select-list expressions aren't available yet.

You can use column aliases in the GROUP BY, HAVING, and ORDER BY clauses. These clauses are run after all the expressions in the select-list have been evaluated.

Bill Karwin
+4  A: 

You can sneak a CASE statement inside your SUM():

SELECT categories.catName, 
    COUNT(map.itemId) AS item_count,
    SUM(CASE WHEN owner= @ownerid THEN 1 ELSE 0 END) AS owner_item_count
FROM categories
LEFT JOIN map  ON categories.catId = map.catId
LEFT JOIN items  ON items.itemId = map.itemId
GROUP BY categories.catId
HAVING COUNT(map.itemId) > 0
Joel Coehoorn
+2  A: 
SELECT categories.catName, 
  COUNT(map.itemId) AS item_count,
  COUNT(items.itemId) AS owner_item_count
FROM categories
INNER JOIN map
  ON categories.catId = map.catId
LEFT JOIN items
  ON items.itemId = map.itemId
  AND items.owner = @ownerId
GROUP BY categories.catId

Note that you could use a HAVING clause on owner_item_count, but the inner join takes care of item_count for you.

eswald