そうです、ここには避けられないデカルト積があります。この問題は、次の 2 つのサブクエリに分解できます。
収入のための 1 つ:
SELECT p.id, p.name, SUM(i.amount) AS income_sum, SUM(number_of_hours_for_amount) AS work_hours_sum
FROM people p
LEFT JOIN income i ON p.id = i.person_id
GROUP BY p.id;
+----+---------+------------+----------------+
| id | name | income_sum | work_hours_sum |
+----+---------+------------+----------------+
| 1 | Groucho | 20.00 | 20 |
| 2 | Harpo | 40.00 | 40 |
| 3 | Chico | 60.00 | 60 |
+----+---------+------------+----------------+
そのクエリの EXPLAIN は次のとおりです。
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------------------------+
| 1 | SIMPLE | p | ALL | PRIMARY | NULL | NULL | NULL | 3 | Using temporary; Using filesort |
| 1 | SIMPLE | i | ALL | NULL | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------------------------+
費用用の 1 つ:
SELECT p.id, SUM(e.amount) AS expenses_sum, SUM(number_of_items_bought) AS items_count
FROM people p
LEFT JOIN expenses e ON p.id = e.person_id
GROUP BY p.id;
+----+--------------+-------------+
| id | expenses_sum | items_count |
+----+--------------+-------------+
| 1 | 30.00 | 4 |
| 2 | 30.00 | 4 |
| 3 | 30.00 | 4 |
+----+--------------+-------------+
説明は次のとおりです。
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------------------------+
| 1 | SIMPLE | p | ALL | PRIMARY | NULL | NULL | NULL | 3 | Using temporary; Using filesort |
| 1 | SIMPLE | e | ALL | NULL | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------------------------+
上記の EXPLAIN レポートでは、クエリが収入テーブルと支出テーブルでテーブル スキャン (タイプ "ALL") を使用し、インデックスなしで結合 ("結合バッファーの使用") を使用していることがわかります。赤信号は、結合に関与する2 つのテーブルがあり、両方がアクセス タイプ「ALL」を使用していることです。これらのテーブルに些細な数以上の行がある場合、非常にコストがかかります。多くの場合、「結合バッファーの使用」と一緒に行われます。これは、コストのかかるクエリの別の危険信号です。
最後に、一時テーブルとファイルソートを使用して、GROUP BY を非効率的に実行します。これはもう 1 つのパフォーマンス キラーです。
Block Nested Loopは MySQL 5.6 のものです。以前のバージョンの MySQL を使用している場合は表示されません。
次のインデックスは、これらのクエリをさらに改善するのに役立ちます。
ALTER TABLE income ADD KEY (person_id, amount, number_of_hours_for_amount);
ALTER TABLE expenses ADD KEY (person_id, amount, number_of_items_bought);
EXPLAIN レポートに非効率なアクセスが表示されなくなりました。結合はインデックス (タイプ「ref」) で行われ、一時テーブルとファイルソートはなくなりました。「インデックスを使用する」は、インデックス内の列のみによって結合されたテーブルにアクセスしていることを示します。テーブルの行にまったく触れる必要はありません。
+----+-------------+-------+-------+---------------+-----------+---------+-----------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-----------+---------+-----------+------+-------------+
| 1 | SIMPLE | p | index | PRIMARY | PRIMARY | 4 | NULL | 3 | NULL |
| 1 | SIMPLE | i | ref | person_id | person_id | 5 | test.p.id | 1 | Using index |
+----+-------------+-------+-------+---------------+-----------+---------+-----------+------+-------------+
+----+-------------+-------+-------+---------------+-----------+---------+-----------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-----------+---------+-----------+------+-------------+
| 1 | SIMPLE | p | index | PRIMARY | PRIMARY | 4 | NULL | 3 | NULL |
| 1 | SIMPLE | e | ref | person_id | person_id | 5 | test.p.id | 1 | Using index |
+----+-------------+-------+-------+---------------+-----------+---------+-----------+------+-------------+
これを 1 つのクエリで実行したいとおっしゃいましたので、その方法を次に示します。
これらの個々の 2 つのクエリを 1つのクエリに結合して、1 人あたり 1 行で結果を取得できます。
SELECT name, income_sum, work_hours_sum, expenses_sum, items_count
FROM
(SELECT p.id, p.name, SUM(i.amount) AS income_sum, SUM(number_of_hours_for_amount) AS work_hours_sum
FROM people p
LEFT OUTER JOIN income i ON p.id = i.person_id
GROUP BY p.id) AS subq_i
INNER JOIN
(SELECT p.id, SUM(e.amount) AS expenses_sum, SUM(number_of_items_bought) AS items_count
FROM people p
LEFT OUTER JOIN expenses e ON p.id = e.person_id
GROUP BY p.id) AS subq_e
USING (id);
+---------+------------+----------------+--------------+-------------+
| name | income_sum | work_hours_sum | expenses_sum | items_count |
+---------+------------+----------------+--------------+-------------+
| Groucho | 20.00 | 20 | 30.00 | 4 |
| Harpo | 40.00 | 40 | 30.00 | 4 |
| Chico | 60.00 | 60 | 30.00 | 4 |
+---------+------------+----------------+--------------+-------------+
EXPLAIN は、この結合されたクエリでもそれほど悪くはありません。一時テーブル、ファイルソート、または結合バッファーはなく、カバー インデックスを適切に使用します。
+----+-------------+------------+-------+---------------+-------------+---------+-----------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------+-------------+---------+-----------+------+-------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 3 | NULL |
| 1 | PRIMARY | <derived3> | ref | <auto_key0> | <auto_key0> | 4 | subq_i.id | 2 | NULL |
| 3 | DERIVED | p | index | PRIMARY | PRIMARY | 4 | NULL | 3 | Using index |
| 3 | DERIVED | e | ref | person_id | person_id | 5 | test.p.id | 1 | Using index |
| 2 | DERIVED | p | index | PRIMARY | PRIMARY | 4 | NULL | 3 | NULL |
| 2 | DERIVED | i | ref | person_id | person_id | 5 | test.p.id | 1 | Using index |
+----+-------------+------------+-------+---------------+-------------+---------+-----------+------+-------------+