views:

86

answers:

4

I am running a MySQL query to rank my site's users according to the number of book reviews and recipe reviews they have contributed. After initial issues with a multiple JOIN query, I've switched to a series of subqueries, which is much, much faster. However, although I can extract the numbers of reviews from each member, I can't figure out how to add them together so I can sort by the total number.

Here's the current query:

SELECT users.*,
   (SELECT count(*) FROM bookshelf WHERE bookshelf.user_id = users.ID) as titles,
   (SELECT count(*) FROM book_reviews WHERE book_reviews.user_id = users.ID) as bookreviews,
   (SELECT count(*) FROM recipe_reviews WHERE recipe_reviews.user_id = users.ID) as recipereviews
FROM users   

I need to add together bookreviews and recipereviews to get 'reviewtotals'. MySQL won't allow you to use simple syntax to do calculations on aliases, but I presume there's another way to do this??

A: 

You tried the following?

SELECT users.*,
   (SELECT count(*) FROM bookshelf WHERE bookshelf.user_id = users.ID) as titles,
   ((SELECT count(*) FROM book_reviews WHERE book_reviews.user_id = users.ID) +
   (SELECT count(*) FROM recipe_reviews WHERE recipe_reviews.user_id = users.ID)) as reviewtotal
FROM users   
Hogan
Thanks - this is almost it, but I still need to generate totals from bookreviews and recipereviews as well as the calculated sum.
mandel
+1  A: 

Two options:

Option 1:Ugly is hell, and probably slow (depends on the query cache):

SELECT users.*,
   (SELECT count(*) FROM bookshelf WHERE bookshelf.user_id = users.ID) as titles,
   (SELECT count(*) FROM book_reviews WHERE book_reviews.user_id = users.ID) as bookreviews,
   (SELECT count(*) FROM recipe_reviews WHERE recipe_reviews.user_id = users.ID) as recipereviews
   (SELECT count(*) FROM book_reviews WHERE book_reviews.user_id = users.ID) + (SELECT count(*) FROM recipe_reviews WHERE recipe_reviews.user_id = users.ID) as reviewtotals
FROM users   

Option 2: Save the results to a temporary table and then query this table

Perhaps this will work (haven't tried it)

SELECT *, bookreviews+recipereviews as reviewtotals FROM
(SELECT users.*,
   (SELECT count(*) FROM bookshelf WHERE bookshelf.user_id = users.ID) as titles,
   (SELECT count(*) FROM book_reviews WHERE book_reviews.user_id = users.ID) as bookreviews,
   (SELECT count(*) FROM recipe_reviews WHERE recipe_reviews.user_id = users.ID) as recipereviews
FROM users) u  
David Rabinowitz
Number two almost works but you have to add an alias to the first subquery - then it's identical to @Quassnoi's solution, though, which was in a minute earlier. Thanks!
mandel
I saw there was another answer while typing...
David Rabinowitz
+4  A: 

Wrap it into a subquery:

SELECT  *,
        bookreviews + recipereviews AS totalreviews
FROM    (
        SELECT  users.*,
                (SELECT count(*) FROM bookshelf WHERE bookshelf.user_id = users.ID) as titles,
                (SELECT count(*) FROM book_reviews WHERE book_reviews.user_id = users.ID) as bookreviews,
                (SELECT count(*) FROM recipe_reviews WHERE recipe_reviews.user_id = users.ID) as recipereviews
        FROM    users   
        ) q
Quassnoi
This is it! It works well and is quite fast to boot. Thanks so much - I'd been getting close, but the syntax was tripping me up, especially adding the last alias at the bottom.
mandel
+1  A: 

If you want to be safe and fast, do it like this:

SELECT users.*
,      titles.num                            titles
,      book_reviews.num                      book_reviews
,      recipe_reviews.num                    recipe_reviews
,      book_reviews.num + recipe_reviews.num total_reviews
FROM      users   
LEFT JOIN (
          SELECT   user_ID, count(*) AS num
          FROM     bookshelf
          GROUP BY user_ID
          ) as titles
ON        users.ID = titles.user_ID
LEFT JOIN (
          SELECT   user_ID, count(*) AS num
          FROM     book_reviews
          GROUP BY user_ID
          ) as book_reviews
ON        users.ID = reviews.user_ID
LEFT JOIN (
          SELECT   user_ID, count(*) AS num
          FROM     recipe_reviews
          GROUP BY user_ID
          ) as recipe_reviews
ON        users.ID = recipes.user_ID

If you want to stick to the subqueries in the SELECT list, and want it to be safe, take a look at Quassnoi's solution.

If you like to live a bit dangersouly and still want a fast result, you can use user-defined variables. I predict it will be safe in this very particular case:

SELECT users.*,
       (SELECT count(*) FROM bookshelf WHERE bookshelf.user_id = users.ID) as titles,
       @bookreviews:=(
           SELECT count(*) 
           FROM book_reviews 
           WHERE book_reviews.user_id = users.ID
       ) as bookreviews,
       @recipereviews:=(
           SELECT count(*) 
           FROM recipe_reviews 
           WHERE recipe_reviews.user_id = users.ID
       ) as recipereviews,
       @bookreviews + @recipereviews as total_reviews
FROM users   
Roland Bouman
Interesting: Option 1 is a tiny bit faster than Quassnoi's solution (0.02 sec vs 0.024 sec), while Option 2 is 0.88 sec, so comparatively quite a bit slower. I find Quassnoi's much simpler in terms of readability (and my understanding of what's going on). Are there advantages to using the JOIN method in your first example?
mandel
mandel: yes, the advantages become quickly apparent when the sets you are dealing with grow in size. the JOIN is much better scaleable. However, at these clock times, I don't think we can say anything conclusive. I would disregard such a small difference. But still you should see sizeable differences when you get more data to deal with. The subquery solution executes each subquery for each row of the outer query. A nested loop join is typically much more efficient.
Roland Bouman