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


9

クエリの実行に非常に長い時間(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回のクエリで処理する数が多くないようです...

これをより良くするために私はこれ以上何ができますか?


EXPLAIN出力を表示できますか?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
デレク・ダウニー、

@coneybeareこれは今日私にとって非常に興味深い挑戦でした!!! 質問の+1。近い将来、このような質問が増えることを願っています。
RolandoMySQLDBA、

@coneybeare新しいEXPLAINは2,008,306ではなく21540行(359 X 60)しか読み取らないようです。最初に回答で提案したクエリに対してEXPLAINを実行してください。その行の数を確認したいと思います。
RolandoMySQLDBA、

@RolandoMySQLDBA新しいは確かにインデックスを持つ行の少ない量は、しかし、クエリを実行するための時間が全く改善を示さない、15秒程度残っていたことを示していEXPLAIN
coneybeare

@coneybeareクエリを微調整しました。新しいクエリでEXPLAINを実行してください。私はそれを私の答えに追加しました。
RolandoMySQLDBA、

回答:


7

クエリ、テーブル、および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つの賛成票を獲得し、回答を受け入れました。

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

そして今、あなたの質問

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

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


インデックスを追加しましたが、時間通りの改善はありません。新しいEXPLAINは次のとおり
coneybeare

レコメンデーション1のクエリに関するEXPLAIN:cloud.coneybeare.net/6xZ2このクエリの実行には約30秒かかりました
corybeare

何らかの理由で構文を少し編集する必要がありました(最初のクエリの前にFROMを追加し、AAAエイリアスを削除する必要がありました)。EXPLAINは次のとおりです。cloud.coneybeare.net / 6xlq 実際のクエリの実行には約30秒かかりました
coneybeare '22年

@RolandoMySQLDBA:あなたの夜11時55分更新にEXPLAIN:cloud.coneybeare.net/6wrN私はプロセスを殺したように、分を超える実際のクエリRANを
coneybeare

2番目の内部選択はA選択テーブルにアクセスできないため、A.idはエラーをスローします。
corybeare

3

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

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

あるいは、インデックスヒントを使用して、mysqlにサウンドインデックスを強制的に使用させることもできます。

更新しました:

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


あなたの提案は@coneybeareにとって注目に値するようです。私からの+1も。
RolandoMySQLDBA、

作成されたインデックスは、いつの間にか消えませんでした。これが更新されたEXPLAINです。cloud.coneybeare.net/6xvc
coneybeare

2

これが「オンザフライ」で使用可能なクエリである必要がある場合は、オプションが少し制限されます。

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

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

@coneybeareがあなたの提案に何かを見たようです。私からの+1 !!!
RolandoMySQLDBA、

これを実際に機能させることはできませんでした。アプローチ方法がわからないSQLエラーが発生しました。私は本当に一時テーブルで働いたことはない
coneybeare

私は(私がFROM追加する必要がありました、最終的にそれを手に入れたsoundsratings真ん中のクエリに)、それは私のSQLボックスをロックアップし、私はプロセスを強制終了する必要がありました。
corybeare

0

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

SHOW CREATE TABLE音\ G

SHOW CREATE TABLEの評価\ G

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

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

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

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

avg(ratings.rating)AS avg_rating、

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.