web-dev-qa-db-ja.com

このMySQLクエリをさらに最適化するにはどうすればよいですか?

クエリの実行に特に長い時間(15秒以上)を要するクエリがあり、データセットが大きくなるにつれ、時間の経過とともに悪化します。私は過去にこれを最適化し、インデックス、コードレベルの並べ替え、その他の最適化を追加しましたが、さらに改良する必要があります。

SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM `sounds` 
INNER JOIN ratings ON sounds.id = ratings.rateable_id 
WHERE (ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49") 
GROUP BY ratings.rateable_id

クエリの目的は、sound idと最新のリリース済みサウンドの平均評価。約1500の音と200万の評価があります。

soundsにはいくつかのインデックスがあります

mysql> show index from sounds;
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| Table  | Non_unique | Key_name                                 | Seq_in_index | Column_name          | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| sounds |          0 | PRIMARY                                  |            1 | id                   | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            1 | deployed             | A         |           5 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            2 | ready_for_deployment | A         |          12 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_name                              |            1 | name                 | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_description                       |            1 | description          | A         |        1388 |      128 | NULL   | YES  | BTREE      |         | 
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+

いくつかのratings

mysql> show index from ratings;
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| Table   | Non_unique | Key_name                                | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| ratings |          0 | PRIMARY                                 |            1 | id          | A         |     2008251 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            1 | rateable_id | A         |          18 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            2 | rating      | A         |        9297 |     NULL | NULL   | YES  | BTREE      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

こちらがEXPLAINです

mysql> EXPLAIN SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM sounds INNER JOIN ratings ON sounds.id = ratings.rateable_id WHERE (ratings.rateable_type = 'Sound' AND sounds.blacklisted = false AND sounds.ready_for_deployment = true AND sounds.deployed = true AND sounds.type = "Sound" AND sounds.created_at > "2011-03-26 21:25:49") GROUP BY ratings.rateable_id;
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
| id | select_type | table   | type   | possible_keys                                    | key                                     | key_len | ref                                     | rows    | Extra       |
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
|  1 | SIMPLE      | ratings | index  | index_ratings_on_rateable_id_and_rating          | index_ratings_on_rateable_id_and_rating | 9       | NULL                                    | 2008306 | Using where | 
|  1 | SIMPLE      | sounds  | eq_ref | PRIMARY,sounds_ready_for_deployment_and_deployed | PRIMARY                                 | 4       | redacted_production.ratings.rateable_id |       1 | Using where | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

一度取得した結果をキャッシュするので、サイトのパフォーマンスはそれほど問題ではありませんが、この呼び出しに時間がかかるため、キャッシュウォーマーの実行に時間がかかり、問題になり始めています。これは、1つのクエリで処理する数が多くないようです...

これをより良いパフォーマンスにするには、他に何ができますか

9
coneybeare

クエリ、テーブル、およびWHERE AND GROUP BY句を調べた後、次のことをお勧めします。

推奨事項#1)クエリをリファクタリングする

クエリを再構成して3つのことを行いました:

  1. 小さい一時テーブルを作成する
  2. それらの一時テーブルでWHERE句を処理する
  3. 最後まで参加を遅らせる

これが私の提案したクエリです:

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

推奨事項#2)WHERE句に対応するインデックスでサウンドテーブルにインデックスを付けます。

このインデックスの列には、静的値が最初で移動ターゲットが最後のWHERE句のすべての列が含まれます

ALTER TABLE sounds ADD INDEX support_index
(blacklisted,ready_for_deployment,deployed,type,created_at);

私はあなたが喜んで驚かれることを心から信じています。試してみる !!!

アップデート2011-05-21 19:04

カーディナリティを見たところです。痛い!!! rateable_idのカーディナリティーは1。少年、私は愚かだと思います!!!

アップデート2011-05-21 19:20

インデックスを作成するだけで、物事を改善できるでしょう。

アップデート2011-05-21 22:56

これを実行してください:

EXPLAIN SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

アップデート2011-05-21 23:34

もう一度リファクタリングしました。これを試してください:

EXPLAIN
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
;

アップデート2011-05-21 23:55

もう一度リファクタリングしました。これを試してください(前回):

EXPLAIN
  SELECT A.id,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) B
  ON A.id = B.rateable_id
  GROUP BY B.rateable_id;

更新2011-05-22 00:12

あきらめたくない!!!

EXPLAIN
  SELECT A.*,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A,
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
    AND AAA.rateable_id = A.id
  ) B
  GROUP BY B.rateable_id;

更新2011-05-22 07:51

EXPLAINで200万行の評価が戻ってくるのが気になりました。その後、それは私を襲った。レート表には、rateable_typeで始まる別のインデックスが必要になる場合があります。

ALTER TABLE ratings ADD INDEX
rateable_type_rateable_id_ndx (rateable_type,rateable_id);

このインデックスの目標は、評価を操作する一時テーブルを200万回未満に減らすことです。その一時テーブルを大幅に小さく(少なくとも半分)できれば、クエリに期待が高まり、私の作業も速くなります。

そのインデックスを作成した後、元の提案されたクエリを再試行してください。

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

UPDATE 2011-05-22 18:39:ファイナルワード

私はストアドプロシージャのクエリをリファクタリングし、高速化に関する質問に答えるのに役立つインデックスを追加しました。私は6つの賛成票を獲得し、回答を受け入れて、200の賞金を獲得しました。

また、別のクエリをリファクタリングし(限界結果)、インデックスを追加しました(劇的な結果)。私は2つの賛成票を獲得し、回答を受け入れてもらいました。

さらに別のクエリチャレンジのインデックスを追加し、1回賛成されました

そして今あなたの質問

これらの(あなたを含む)のようなすべての質問に答えたいと思ったのは、リファクタリングクエリで見たYouTubeビデオから発想を得ています。

もう一度ありがとう、@ coneybeare !!!ポイントや賞賛だけではなく、可能な限りこの質問に答えたいと思いました。さて、ポイントが貯まった気がします!!!

7
RolandoMySQLDBA

EXPLAIN出力をありがとう。このステートメントからわかるように、時間がかかるのは評価テーブルの全テーブルスキャンが原因です。 200万行をフィルタリングするWHEREステートメントはありません。

Ratings.typeにインデックスを追加することもできますが、私の推測では、カーディナリティは非常に低くなり、ratingsのかなりの数の行をスキャンすることになります。

または、mysqlにサウンドインデックスを使用するように強制する index hints を使用することもできます。

更新しました:

私の場合、sounds.createdにインデックスを追加します。これは、行をフィルタリングする可能性が最も高く、MySQLクエリオプティマイザがサウンドテーブルインデックスを使用するように強制するためです。長く作成された時間フレームを使用するクエリに注意してください(1年、3か月、サウンドテーブルのサイズによって異なります)。

3
Derek Downey

これが"on-the-fly"の使用可能なクエリである必要がある場合は、オプションが少し制限されます。

私はこの問題を分割して征服することを提案します。

--
-- Create an in-memory table
CREATE TEMPORARY TABLE rating_aggregates (
rateable_id INT,
avg_rating NUMERIC,
votes NUMERIC
);
--
-- For now, just aggregate. 
INSERT INTO rating_aggregates
SELECT ratings.rateable_id, 
avg(ratings.rating) AS avg_rating, 
count(ratings.rating) AS votes FROM `sounds`  
WHERE ratings.rateable_type = 'Sound' 
GROUP BY ratings.rateable_id;
--
-- Now get your final product --
SELECT 
sounds.*, 
rating_aggregates.avg_rating, 
rating_aggregates.votes AS votes,
rating_aggregates.rateable_id 
FROM rating_aggregates 
INNER JOIN sounds ON (sounds.id = rating_aggregates.rateable_id) 
WHERE 
ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49";
2
randomx

サブクエリではなくJOINを使用します。あなたのサブクエリの試みは役に立ちましたか?

SHOW CREATE TABLE音\ G

SHOW CREATE TABLEの評価\ G

多くの場合、単一列のインデックスではなく、「複合」インデックスがあると効果的です。おそらくINDEX(type、created_at)

JOINの両方のテーブルでフィルタリングしています。これはパフォーマンスの問題である可能性があります。

約1500の音と200万の評価があります。

ratingsにauto_increment IDを設定し、サマリーテーブルを作成し、AI IDを使用して「中断」した場所を追跡することをお勧めします。ただし、平均を要約表に保管しないでください。

avg(ratings.rating)AS avg_rating、

代わりに、SUM(ratings.rating)を保持します。平均の平均は、平均を計算するために数学的に正しくありません。 (合計の合計)/(カウントの合計)は正しいです。

0
Rick James