tags:

views:

349

answers:

4

Hi,

I am trying to figure out how to use multiple left outer joins to calculate average scores and number of cards. I have the following schema and test data. Each deck has 0 or more scores and 0 or more cards. I need to calculate an average score and card count for each deck. I'm using mysql for convenience, I eventually want this to run on sqlite on an Android phone.

mysql> select * from deck;
+----+-------+
| id | name  |
+----+-------+
|  1 | one   | 
|  2 | two   | 
|  3 | three | 
+----+-------+
mysql> select * from score;
+---------+-------+---------------------+--------+
| scoreId | value | date                | deckId |
+---------+-------+---------------------+--------+
|       1 |  6.58 | 2009-10-05 20:54:52 |      1 | 
|       2 |     7 | 2009-10-05 20:54:58 |      1 | 
|       3 |  4.67 | 2009-10-05 20:55:04 |      1 | 
|       4 |     7 | 2009-10-05 20:57:38 |      2 | 
|       5 |     7 | 2009-10-05 20:57:41 |      2 | 
+---------+-------+---------------------+--------+
mysql> select * from card;
+--------+-------+------+--------+
| cardId | front | back | deckId |
+--------+-------+------+--------+
|      1 | fron  | back |      2 | 
|      2 | fron  | back |      1 | 
|      3 | f1    | b2   |      1 | 
+--------+-------+------+--------+

I run the following query...


mysql> select deck.name, sum(score.value)/count(score.value) "Ave", 
    ->   count(card.front) "Count" 
    -> from deck 
    -> left outer join score on deck.id=score.deckId 
    -> left outer join card on deck.id=card.deckId
    -> group by deck.id;

+-------+-----------------+-------+
| name  | Ave             | Count |
+-------+-----------------+-------+
| one   | 6.0833333333333 |     6 | 
| two   |               7 |     2 | 
| three |            NULL |     0 | 
+-------+-----------------+-------+

... and I get the right answer for the average, but the wrong answer for the number of cards. Can someone tell me what I am doing wrong before I pull my hair out?

Thanks!

John

A: 

It's running what you're asking--it's joining card 2 and 3 to scores 1, 2, and 3--creating a count of 6 (2 * 3). In card 1's case, it joins to scores 4 and 5, creating a count of 2 (1 * 2).

If you just want a count of cards, like you're currently doing, COUNT(Distinct Card.CardId).

Brisbe42
count(distinct card.front) will fail if cardId #3's front == 'fron'. it's better not to rely on count distinct hack
Michael Buen
+1  A: 
select deck.name, coalesce(x.ave,0) as ave, count(card.*) as count -- card.* makes the intent more clear, i.e. to counting card itself, not the field.  but do not do count(*), will make the result wrong
from deck    
left join -- flatten the average result rows first
(
    select deckId,sum(value)/count(*) as ave -- count the number of rows, not count the column name value.  intent is more clear
    from score 
    group by deckId
) as x on x.deckId = deck.id
left outer join card on card.deckId = deck.id -- then join the flattened results to cards
group by deck.id, x.ave, deck.name
order by deck.id

[EDIT]

sql has built-in average function, just use this:

select deckId, avg(value) as ave
from score 
group by deckId
Michael Buen
Your answer worked well enough (and gave it a vote) but selected the first because it worked (with your suggested change to use distinct Card.cardId) and was a little simpler to follow. Thanks.
John in MD
A: 

Using left joins isn't a good approach, in my opinion. Here's a standard SQL query for the result you want.

select
  name,
  (select avg(value) from score where score.deckId = deck.id) as Ave,
  (select count(*) from card where card.deckId = deck.id) as "Count"
from deck;
Steve Kass
-1 subquery is a performance killer, no matter how easy it is to read. reminds me of bubble sort, it's the easiest to learn and code, but it's the slowest among all sort algorithm
Hao
Nonsense. Bubble sort does more work than necessary. This query need not. It potentially accesses exactly as much information as it must, and no more. If you are convinced otherwise, please post some test results. Of course, it may help the optimizer to provide indexes on the card and score tables to isolate a single deckId value.Why is this subquery worse than an outer joins of all three tables? The triple join is illogical, because there is no reason to pair every row of the score table with every row of the card table to solve this problem.
Steve Kass
doing subsquery is slow, the disk's read/write head need to go back and forth between the three tables. unlike when you join three tables, the read/write head just concentrate on first two tables first, then when you join the third table into it, the rdbms just concentrate on joining the third table to the in-memory result of the first joined tables. hence joining tables are faster than subquery, it doesn't cause unncessary jumping back and forth of disk read/write head. think physical implementation, and cache coherency too
Hao
+1  A: 

What's going wrong is that you're creating a Cartesian product between score and card.

Here's how it works: when you join deck to score, you may have multiple rows match. Then each of these multiple rows is joined to all of the matching rows in card. There's no condition preventing that from happening, and the default join behavior when no condition restricts it is to join all rows in one table to all rows in another table.

To see it in action, try this query, without the group by:

select * 
from deck 
left outer join score on deck.id=score.deckId 
left outer join card on deck.id=card.deckId;

You'll see a lot of repeated data in the columns that come from score and card. When you calculate the AVG() over data that has repeats in it, the redundant values magically disappear (as long as the values are repeated uniformly). But when you COUNT() or SUM() them, the totals are way off.

There may be remedies for inadvertent Cartesian products. In your case, you can use COUNT(DISTINCT) to compensate:

select deck.name, avg(score.value) "Ave", count(DISTINCT card.front) "Count" 
from deck 
left outer join score on deck.id=score.deckId 
left outer join card on deck.id=card.deckId
group by deck.id;

This solution doesn't solve all cases of inadvertent Cartesian products. The more general-purpose solution is to break it up into two separate queries:

select deck.name, avg(score.value) "Ave"
from deck 
left outer join score on deck.id=score.deckId 
group by deck.id;

select deck.name, count(card.front) "Count" 
from deck 
left outer join card on deck.id=card.deckId
group by deck.id;

Not every task in database programming must be done in a single query. It can even be more efficient (as well as simpler, easier to modify, and less error-prone) to use individual queries when you need multiple statistics.

Bill Karwin
I liked your answer (and gave it a vote) for its completeness but selected the above because it worked (with the change to use distinct Card.cardId) and I only had to deal with a single result set. Thanks.
John in MD
@BillK: just *flatten* the aggregate result of the first query, then join another table to it, if need to join another aggregate result, flatten first the second join. Lather, rinse, repeat
Michael Buen