2

私はMYSQLを使用しています。私は3つのテーブルを持っています。

  1. people2 つの列で構成されるテーブル:

    • id - テーブルの主キー
    • name - 個人の名前
  2. incomepeople テーブルからの人々の収入を含むテーブル。このテーブルの各レコードは、個人の収入を表します。このテーブルでは、1 人の人の収入がゼロまたは多数になる可能性があります。テーブル構造は次のとおりです。

    • person_id ('people' テーブルへの外部キー)
    • amount (DECIMAL型 - 金額)
    • number_of_hours_for_amount (INTEGER タイプ - この収入を得るために必要な時間数)
  3. expenses人々の費用を含むテーブル。このテーブルの各レコードは、その人の支出と、その 1 回の支出で購入したアイテムの量を表します。1 人の人物が、このテーブルにゼロまたは多数の経費レコードを持つことができます。テーブル構造は次のとおりです。

    • person_id ('people' テーブルへの外部キー)
    • amount (DECIMAL型の金額)
    • number_of_items_bought (INTEGER タイプ - この費用で購入したアイテムの数)

私がやろうとしているのは、すべての人のリスト (1 人あたり 1 つのレコード) を提供する単一のクエリを 1 つ作成することです。

  • その人の名前、
  • 彼のすべての収入の合計、
  • 彼が働いた総時間数、
  • 彼のすべての費用の合計、
  • 彼が購入したアイテムの総数。

私が試した最初の素朴なアプローチは、論理的には非常にうまく機能しましたが、パフォーマンスは非常に低く、次のようになりました。

SELECT name, income_sum, work_hours_sum, expenses_sum, items_count
FROM (people
      LEFT JOIN 
           (SELECT person_id, sum(amount) as income_sum, 
                   sum(number_of_hours_for_amount) as work_hours_sum
            FROM income
            GROUP BY person_id) as income_subquery
      ON people.id = income_subquery.person_id)

LEFT JOIN
     (SELECT person_id, sum(amount) as expenses_sum, 
             sum(number_of_items_bought) as items_count
      FROM expenses
      GROUP BY person_id) as income_subquery
ON people.id = income_subquery.person_id

私が理解している限り、このクエリの問題は、サブクエリからデータを取得すると、これらのテーブルが一時的なサブクエリ テーブルであるため、これらのテーブルのインデックスが適切に使用されないため、結合が非常に非効率的に行われることです。

既存のインデックスを有効に活用する最善の方法は、サブクエリを介さずに 3 つのテーブル間で直接結合を行うことです。しかし、これは正しい解決策ではありません。デカルト積が作成され、必要以上に表示されるレコードからの集計の合計に重複した値が追加されるためです。

(私が試した別のオプションは、各人の収入と支出の値を SELECT セクション (依存サブクエリ) の select_expressions として計算することでした。これも十分に速く動作しませんでした)

効率的でこれらの結果が得られるクエリを探しています。

4

4 に答える 4

3

そうです、ここには避けられないデカルト積があります。この問題は、次の 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 |
+----+-------------+------------+-------+---------------+-------------+---------+-----------+------+-------------+
于 2013-07-04T18:22:34.727 に答える
0

このようなものは、あなたをかなり近づけるはずです:

select id, name, (select sum(amount) from income i where i.person_id = p.id) as 'total_income_amount',
                 (select sum(number_of_hours_for_amount) from income i where i.person_id = p.id) as 'total_number_of_hours_for_amount',
                 (select sum(amount) from expenses e where e.person_id = p.id) as 'total_expenses_amount',
                 (select sum(number_of_items_bought) from expenses e where e.person_id = p.id) as 'total_number_of_items_bought'
from   people p;
于 2013-07-04T17:56:07.573 に答える
0

これを試して。両方の結合で のインデックスを使用する必要がありますpeople.id

SELECT name, income_sum, work_hours_sum, expenses_sum, items_count
FROM people

LEFT JOIN 
     (SELECT person_id, sum(amount) as income_sum, 
             sum(number_of_hours_for_amount) as work_hours_sum
      FROM income
      GROUP BY person_id) as income_subquery
ON people.id = income_subquery.person_id

LEFT JOIN
     (SELECT person_id, sum(amount) as expenses_sum, 
             sum(number_of_items_bought) as items_count
      FROM expenses
      GROUP BY person_id) as expenses_subquery
ON people.id = expenses_subquery.person_id

理想的には、優れたクエリ オプティマイザーは、元の SQL がこれと同等であることを認識するでしょう。しかし、MySQL を使用しているため、理想的な最適化は期待できません。

サブクエリでのグループ化が効率的になるように、インデックスがオンincome.person_idになっていることを確認してください。expenses.person_id

于 2013-07-04T17:38:00.177 に答える
0

おそらく、JOIN を完全にスキップできます。

SELECT person_id
     , MIN(name) AS name
     , SUM(income_sum) AS income_sum
     , SUM(work_hours_sum) AS work_hours_sum
     , SUM(expenses_sum) AS expenses_sum
     , SUM(items_count) AS items_count
FROM (
SELECT id AS person_id
     , name
     , NULL AS income_sum
     , NULL AS work_hours_sum
     , NULL AS expenses_sum
     , NULL AS items_count
  FROM people
UNION ALL
SELECT person_id
     , NULL AS name
     , sum(amount) AS income_sum
     , sum(number_of_hours_for_amount) AS work_hours_sum
     , NULL AS expenses_sum
     , NULL AS items_count
  FROM income
 GROUP BY person_id
UNION ALL
SELECT person_id
     , NULL AS name
     , NULL AS income_sum
     , NULL AS work_hours_sum
     , sum(amount) AS expenses_sum
     , sum(number_of_items_bought) AS items_count
  FROM expenses
 GROUP BY person_id
) as d
WHERE person_id IS NOT NULL -- my sql generates this row
 GROUP BY person_id
于 2013-07-07T16:56:17.853 に答える