Использование нескольких левых объединений для вычисления средних значений и количества - PullRequest
1 голос
/ 10 октября 2009

Я пытаюсь выяснить, как использовать несколько левых внешних объединений для расчета средних баллов и количества карт. У меня есть следующая схема и данные испытаний. Каждая колода имеет 0 или более очков и 0 или более карт. Мне нужно рассчитать средний счет и количество карт для каждой колоды. Я использую mysql для удобства, в конечном итоге я хочу, чтобы он работал на sqlite на телефоне Android.

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 | 
+--------+-------+------+--------+

Я запускаю следующий запрос ...


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 | 
+-------+-----------------+-------+

... и я получаю правильный ответ для среднего, но неправильный ответ для количества карт. Может кто-нибудь сказать мне, что я делаю не так, прежде чем вырвать свои волосы?

Спасибо!

John

Ответы [ 4 ]

1 голос
/ 10 октября 2009

Что не так, так это то, что вы создаете декартово произведение между score и card.

Вот как это работает: когда вы присоединяете deck к score, у вас может совпадать несколько строк. Затем каждая из этих нескольких строк соединяется с всеми соответствующих строк в card. Нет условий, препятствующих этому, и поведение соединения по умолчанию, когда никакие условия не ограничивают его, состоит в объединении всех строк в одной таблице со всеми строками в другой таблице.

Чтобы увидеть его в действии, попробуйте этот запрос без группы:

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

Вы увидите много повторяющихся данных в столбцах, которые взяты из score и card. Когда вы вычисляете AVG() для данных, в которых есть повторы, избыточные значения магически исчезают (до тех пор, пока значения повторяются равномерно). Но когда вы COUNT() или SUM() их, итоговые суммы далеко.

Могут быть средства от непреднамеренных декартовых продуктов. В вашем случае вы можете использовать COUNT(DISTINCT) для компенсации:

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;

Это решение не решает все случаи непреднамеренных декартовых продуктов. Более универсальное решение состоит в том, чтобы разбить его на два отдельных запроса:

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;

Не все задачи в программировании баз данных должны выполняться в одном запросе. Может быть даже более эффективным (а также более простым, простым в модификации и менее подверженным ошибкам) ​​для использования отдельных запросов, когда вам требуется несколько статистических данных.

1 голос
/ 10 октября 2009
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 имеет встроенную функцию усреднения, просто используйте это:

select deckId, avg(value) as ave
from score 
group by deckId
1 голос
/ 10 октября 2009

Он выполняет то, что вы просите - он соединяет карты 2 и 3 с оценками 1, 2 и 3 - создавая счет 6 (2 * 3) В случае с картой 1 она соединяется со счетами 4 и 5, создавая счет 2 (1 * 2).

Если вы просто хотите подсчитать количество карт, как вы сейчас делаете, СЧИТАЙТЕ (Distinct Card.CardId).

0 голосов
/ 10 октября 2009

Использование левых соединений не очень хороший подход, по моему мнению. Вот стандартный запрос SQL для желаемого результата.

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;
...