web-dev-qa-db-ja.com

MAX、GROUP BY、およびWHEREを使用してMySQLクエリを最適化する

かなり単純なクエリを使用していますが、大量のインデックスを追加しても十分に最適化できませんでした。

クエリは次のとおりです。

SELECT max(elapsed_seconds)
FROM sql_queries
WHERE created_at >= now() - interval 1 week
GROUP BY `sql`

次のインデックスを追加してみました。

  KEY `sql_queries_sql_index` (`sql`),
  KEY `sql_queries_elapsed_seconds_index` (`elapsed_seconds`),
  KEY `sql_queries_created_at_index` (`created_at`),
  KEY `sql_queries_sql_created_at_index` (`sql`,`created_at`),
  KEY `sql_queries_sql_elapsed_seconds_index` (`sql`,`elapsed_seconds`),
  KEY `sql_queries_created_at_sql_elapsed_seconds` (`created_at`,`sql`,`elapsed_seconds`)

明らかに、インデックスが多すぎる(そして冗長である)ので、クエリがより速く実行されることを期待して、インデックスを追加し続けました。

テーブルには2400万行あり、クエリには現在約4分かかります。

「説明」は示しています:

+----+-------------+-------------+------------+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------+---------+------+----------+----------+-----------------------------------------------------------+
| id | select_type | table       | partitions | type  | possible_keys                                                                                                                                                        | key                                        | key_len | ref  | rows     | filtered | Extra                                                     |
+----+-------------+-------------+------------+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------+---------+------+----------+----------+-----------------------------------------------------------+
|  1 | SIMPLE      | sql_queries | NULL       | range | sql_queries_sql_index,sql_queries_created_at_index,sql_queries_sql_created_at_index,sql_queries_sql_elapsed_seconds_index,sql_queries_created_at_sql_elapsed_seconds | sql_queries_created_at_sql_elapsed_seconds | 5       | NULL | 11574092 |   100.00 | Using where; Using index; Using temporary; Using filesort |
+----+-------------+-------------+------------+-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------+---------+------+----------+----------+-----------------------------------------------------------+

列の定義は次のとおりです。

  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `sql` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
  `elapsed_seconds` double DEFAULT NULL,
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,

各グループを平均して数えたいのですが、説明を簡単にするために省略しました。

どんなアイデア/ヒントも大歓迎です。

-

更新1:次のように日付をハードコーディングして、クエリを簡略化してみました。

select max(elapsed_seconds) from sql_queries where created_at >= '2018-4-22' group by `sql`;

クエリ時間(キャッシュをウォームアップする最初のクエリを実行した後)は、2分5秒から1分53秒に減少します。したがって、大幅な改善はありません。

アップデート2:

この簡略化されたクエリの説明ステートメントは次のとおりです。

mysql> explain select max(elapsed_seconds) from sql_queries where created_at >= '2018-4-22' group by `sql`;
+----+-------------+-------------+------------+-------+-----------------------------------------------------------------------------------------------+--------------------------------------------+---------+------+----------+----------+--------------------------+
| id | select_type | table       | partitions | type  | possible_keys                                                                                 | key                                        | key_len | ref  | rows     | filtered | Extra                    |
+----+-------------+-------------+------------+-------+-----------------------------------------------------------------------------------------------+--------------------------------------------+---------+------+----------+----------+--------------------------+
|  1 | SIMPLE      | sql_queries | NULL       | index | sql_queries_sql_index,sql_queries_created_at_index,sql_queries_sql_created_at_elapsed_seconds | sql_queries_sql_created_at_elapsed_seconds | 780     | NULL | 29773986 |    50.00 | Using where; Using index |
+----+-------------+-------------+------------+-------+-----------------------------------------------------------------------------------------------+--------------------------------------------+---------+------+----------+----------+--------------------------+

更新3:

健全性チェックとして、日付の制約を削除して、(sql、expeded_seconds)にインデックスを追加しました。その後、クエリは瞬時に行われました。

3
Dan Sandberg

深刻な候補は3つだけです。

(`created_at`,`sql`,`elapsed_seconds`) -- 1
(`created_at`,`elapsed_seconds`,`sql`) -- 2
(`sql`,`created_at`,`elapsed_seconds`) -- 3

どちらも「カバー」しています。つまり、クエリは完全にインデックスで処理できます。 EXPLAINは、Using indexと言ってそのことを示します。

分析:

(`created_at`,`sql`,`elapsed_seconds`) -- 1
(`created_at`,`elapsed_seconds`,`sql`) -- 2

最初にフィルタリングします。ただし、インデックスの残りの部分は、有用な順序ではありません。したがって、GROUP BYを実行するように並べ替え、最終的に最大値を見つけます。 できません「最後の」エントリに到達してMAXを取得します。これらのいずれかが2つのうちの他方よりも優れているとは思いません。

(`sql`,`created_at`,`elapsed_seconds`) -- 3

sqlの値は一度に1つになるため、ソートが回避される場合があります。また、オプティマイザーmightは、目的のcreated_atsqlごと)のインデックスの開始点にジャンプできます。繰り返しますが、それはcannot「最後の」エントリに到達してMAXを取得するだけです。

#3に投票します。ただし、これは最適化の改善があった領域です。つまり、MySQLの古いバージョンでは、たとえば跳躍ができない場合があります。

1
Rick James

あなたの問題は、これが SARG-able ではないということです

where created_at >= now() - interval 1 week group by `sql`

Created_atにはすでにインデックスがあります。余分なINDEXesはINSERTsとDELETEsのパフォーマンスにのみ影響し、SELECTsには影響しませんが、多すぎても言い訳にはなりません!

MySQLのオプティマイザが、さまざまなクエリについて何と言っているかを見てみましょう。警告の言葉-しかし、MySQLオプティマイザは(エヘム...)悪名高い気まぐれなソフトウェアなので、YMMVです!

これは 記事 から始めるのが良いでしょう!

このセクションは、私たちの場合に特に重要です。

しかし、特定の日付がないとします。代わりに、今日から特定の日数以内の日付を持つレコードを見つけることに関心があるかもしれません。

私たちはここでお金を受け取っています!

このタイプの比較を表現するにはいくつかの方法があります—すべてが同等に効率的であるわけではありません。 3つの可能性があります。

WHERE TO_DAYS(date_col)-TO_DAYS(CURDATE())<カットオフ

WHERE TO_DAYS(date_col)<カットオフ+ TO_DAYS(CURDATE())

WHERE date_col <DATE_ADD(CURDATE()、INTERVAL cutoff DAY)

最初の行では、TO_DAYS(date_col)の値を計算できるように各行の列を取得する必要があるため、インデックスは使用されません。

わかりましたので、それをスクラップしてください!

2行目が優れています。カットオフとTO_DAYS(CURDATE())はどちらも定数なので、比較の右側は、行ごとにではなく、クエリを処理する前に1回オプティマイザによって計算できます。ただし、date_col列は引き続き関数呼び出しに表示されるため、インデックスを使用できません。

そして、あれ!

3行目が最も優れています。繰り返しになりますが、比較の右側は、クエリを実行する前に定数として一度計算できますが、現在の値は日付です。その値は、日数に変換する必要がなくなったdate_col値と直接比較できます。この場合、インデックスを使用できます。

したがって、クエリは次のようなものでうまく機能するでしょう(テストできるサーバーがない!)

WHERE created_at > DATEADD(NOW() - INTERVAL 7 DAY)

また、リンク here も確認できます。

1
Vérace

私は専門家ではありませんが、しばらくの間SQLを使用しているので、ここでお手伝いできると思います。

このクエリの正しいインデックスを作成すると、次のようになります。

何でグループ化していますか? 「sql」なので、これがインデックスの最初のフィールドになります。

クエリは何に基づいてフィルタリングされますか? 「created_at」なので、これは2番目のフィールドにする必要があります

最後に、実際の計算に必要な残りのフィールドをすべて追加します。これはオプションですが、データベースエンジンがインデックス内のすべてを見つけることができれば、テーブル自体にアクセスする必要がないため、パフォーマンスが向上します。

したがって、理想的なインデックスは(必要に応じて、 'elapsed_seconds'の後にフィールドを追加する可能性があります):

KEY `sql_queries_sql_created_at_elapsed_seconds` (`sql`,`created_at`,`elapsed_seconds`)

これはMAX()計算であるため、クエリは基本的にソートされたインデックスの単純な検索になり、理論的には非常に高速になる可能性があります。

0
cpcodes

私が目にした主な問題は、WHERE句が非定数比較を実行することです。

11574092行のそれぞれについて、同じ値now() - interval 1 weekが何度も計算され、オーバーヘッドが発生します。さらに、now()が2つの結果として生じる呼び出しで異なる結果を返す可能性がある非決定的関数である限り、インデックスはそのような種類の比較に役立ちません。したがって、エンジンはすべての行とすべての行で目的の条件をチェックする必要があり、最初からnow() - interval 1 weekを計算します。

この非常に一般的なトラップを回避するのは簡単です。値を1回計算し、ユーザー定義変数に格納します。

SET @starting_point = now() - interval 1 week;

SELECT max(elapsed_seconds)
  FROM sql_queries
 WHERE created_at >= @starting_point
 GROUP BY `sql`
;

オプティマイザが選択した最適なインデックスがすでにあります(created_at,sql,elapsed_seconds)定数比較でどのように機能するか確認してください。

0
Kondybas