Home > MySQL, Web Coding > MySQL: counts of multiple sub-items of each parent

MySQL: counts of multiple sub-items of each parent

October 27th, 2009

Sometimes it is useful to show in a list the numbers of children each object has. For instance how many Products are in each Category.

That’s simple enough to do when only one child type is used:

SELECT
 categories.title, categories.id,
 count(products.id) as productCount
FROM
 categories
   INNER JOIN products ON categories.id = products.categoryid

However, if you wanted to count two separate children types, the same method of querying will give you very odd results:

SELECT
 categories.title, categories.id,
 count(products.id) as productCount, count(images.id) as imagesCount
FROM
 categories
   INNER JOIN products ON categories.id = products.categoryid
   INNER JOIN images ON categories.id = images.categoryid

The lazy programmer would get around this by using the server language – looping through a simple query of the category, and with each row run another query to count the products (and another for the images). Yuck.

Instead, I found this method to work quite well:

SELECT
 cats.title AS title, cats.ID AS id,
 COALESCE(prods.howmany,0) AS productCount,
 COALESCE(imgs.howmany,0) AS imageCount
FROM (
 SELECT title, ID
 FROM categories
) AS cats
LEFT JOIN (
 SELECT
   count(products.id) as howmany,
   products.categoryid as categoryid
 FROM products
   LEFT JOIN categories ON products.categoryid = categories.id
 GROUP BY products.categoryid
) AS prods ON cats.id=prods.categoryid
LEFT JOIN (
 SELECT
   count(images.id) as howmany,
   images.categoryid as categoryid
 FROM images
   LEFT JOIN categories ON images.categoryid = categories.id
 GROUP BY images.categoryid
) AS imgs ON cats.id=imgs.categoryid

Note the use of the ‘COALESCE()’ function so that nulls are returned as a 0 (zero).
As you can see the database is still running three queries. But they are all within one connection, and the server language didn’t do any work.  Then MySQL joins those queries together into one result, easily parsed by your server language of choice. Here, ColdFusion:

<table>
 <tr>
   <td>Category</td>
   <td>Products</td>
   <td>Images</td>
 </tr>
 <cfoutput query="read">
 <tr>
   <td>#title#</td>
   <td>#productCount#</td>
   <td>#imageCount#</td>
 </tr>
 </cfoutput>
</table>

MySQL, Web Coding , , ,

  1. October 27th, 2009 at 15:20 | #1

    You might also look at using subqueries (hopefully formatting doesn’t get totally munged):

    SELECT	categories.title, categories.id,
    		(
    			SELECT	count(*)
    			FROM	products
    			WHERE	products.categoryid = categories.id
    		) AS productCount,
    		(
    			SELECT	count(*)
    			FROM	images
    			WHERE	images.categoryid = categories.id
    		) AS imagesCount
    FROM	categories

    These are correlated subqueries, so if the number of categories is very large then there could be some performance implications, but I have found them to perform well generally.

    I find this clearer and easier to read. The code is half is long and I think the intent is a bit more obvious (though I could be biased). It also works the same on every database I have used.

  2. October 27th, 2009 at 17:21 | #2

    @Steve Bryant
    Wow that sure looks simpler. I tried it out on the same data as my version. Yours is about 4x faster. Wish I saw that a week ago!

  1. No trackbacks yet.