メインコンテンツへスキップ

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
テストテーブルではミリ秒で返る GROUP BY が、実際のテーブルでは数分かかることが あります。クエリの文面は同じです。違いは、サーバーがインデックスをグループ順に たどれるか、先に一時テーブルを作らなければならないかにあります。このガイドは その違いと、どちらになるかを決める選択について説明します。 句そのものを学んでいる場合は、チュートリアルがサンプルデータベースを使って 教えています。集約関数、 GROUP BY、HAVING、 COUNT DISTINCT、 GROUP_CONCAT、および WITH ROLLUP です。 このページのすべてのクエリとプランは、Sakila サンプルデータベースに対する 1 台の サーバーから取得したものです。コストと行数の見積もりは InnoDB のサンプリング 統計と optimizer_switch の設定に由来するため、数値は環境によって異なります。 各プランの形が、そのまま当てはまる部分です。

インデックスのないグループ化は一時テーブルを作る

行をグループ化するには、サーバーは各グループの行を 1 か所に集めなければ なりません。グループ化する列にインデックスがない場合、サーバーは一時テーブルを 作ってそれを行います。
Using temporary が探すべき行です。1,000 行では何のコストもかかりません。 このテーブルは収まる間はメモリに保持され、収まらなくなるとディスクに退避 されます。グループ化したレポートが速くなくなるのはそこです。 そのメモリの上限をどの設定が決めるかは、サーバーが内部一時テーブルに使う エンジンによります。何かを変更する前に、自分の設定を確認してください。
これらは異なる役割を持ち、TempTable ではすべてが適用されます。 temptable_max_ram と temptable_max_mmap は、エンジンがすべてのセッションに わたって保持できる量の上限です。tmp_table_size は個々の内部一時テーブルの 上限です。1 KB では 1,000 行に対するグループ化クエリで Created_tmp_disk_tables が 0 から 1 になり、64 MB では同じクエリがメモリ内に収まりました。推測するのでは なく、このカウンターを監視してください。
グループ化する列にインデックスがあると、インデックスがすでにその順序に なっているため、この手順は完全になくなります。
Using temporary も Using filesort もありません。サーバーはインデックスを順に 読み、等しい値の連続ごとに数え、1 行を出力します。Using index は、テーブル そのものに一度も触れなかったことを意味します。 したがって、遅い GROUP BY で最初に確認すべきことは、グループ化した列と同じ 順序で、その列から始まるインデックスがあるかどうかです。(a, b) の複合 インデックスは GROUP BY a と GROUP BY a, b に使えます。GROUP BY b では、 サーバーはそれをカバリングインデックスとしてスキャンでき、type: index と Using index として報告されますが、行はグループ順には届かないため、 Using temporary が一緒に戻ってきます。

WHERE と HAVING はスタイルの選択ではない

どちらもフィルタを表現できます。しかしコストは異なります。WHERE はグループ化の 前に実行され、HAVING はその後に実行されるためです。 グループ化する列そのものに対する条件はどちらの書き方もでき、意味も同じなので、 公平な比較になります。WHERE では、サーバーはまずインデックスの範囲を絞り、 読んだものだけをグループ化します。
HAVING では、同じ制限が届くのが遅すぎて、何も絞り込めません。
出力される行は同じで、見積もりコストは 4 倍です。WHERE 版は見積もりで 2,710 件の インデックスエントリを読みます。HAVING 版はインデックス全体を読み、その後で ほとんどのグループを捨てます。 これらの見積もりは InnoDB のサンプリング統計と mysql.server_cost のコスト定数に 由来するため、数値は環境によって異なります。2 つのプランの形が、そのまま当てはまる 部分です。 ルールはこうです。行を検査する条件は常に WHERE に置きます。HAVING は グループを検査するものにだけ使います。実際には、集約を含むものすべてです。 集約を含まない HAVING 句は、ほぼ常に置き場所を間違えた WHERE 句であり、 集約を含む WHERE は ERROR 1111 (HY000): Invalid use of group function に なります。

ONLY_FULL_GROUP_BY と抜け道

only_full_group_by はデフォルトで有効であり、グループ化も集約もされていない列を 選択するクエリを ERROR 1055 で拒否します。理由は、そのようなクエリには定義された 答えがないからです。グループは多くの行を持ち、どれを表示するかを何も指定して いません。 ANY_VALUE() は、任意の 1 つを受け入れることをサーバーに伝え、クエリは実行 されます。
ここでは意図的に出力を示していません。どのタイトルが返るかは定義されていない ため、このページに印字したどの結果も、再現できない値になるからです。 ANY_VALUE() は、列がグループ化列に関数従属しているのにサーバーがそれを証明 できない場合に使います。これが正当なケースです。グループ内で本当に値が変わる列の エラーを黙らせるために使ってはいけません。得られる値は最初のものでも最小のものでも なく、頼りにできるものは何もなく、プランが変わると変わることがあります。 MIN() や MAX() なら意図を表現でき、コストは同じです。 only_full_group_by をグローバルに無効にすると、これらのエラーのすべてが、誰も 気づかない誤った答えに変わります。これは誤った修正です。 このチェックは、グループ化された列の上に組み立てた式を見通すため、 SELECT COALESCE(rating, 'unrated') ... GROUP BY rating は受け入れられます。 グループ化された式の上に組み立てた式は見通しません。 DATE_FORMAT(payment_date, '%Y-%m') でグループ化して COALESCE(DATE_FORMAT(payment_date, '%Y-%m'), 'ALL') を選択すると、同じ ERROR 1055 が payment_date を指して出ます。サーバーは内部の列ではなく 2 つの式を 比較するためです。これはレポートでよくあるケースであり、回避策は派生テーブルの中で グループ化し、その外側で結果を包むことです。

一対多の結合を集約する

グループ化の前に結合すると行が増えるため、その結果に対する SUM は二重に数えます。 症状は、誰にも説明できない倍率で大きすぎる合計です。 先に結合してから後でグループ化するのではなく、各側をそれぞれのサブクエリで集約し、 その結果を結合します。
各サブクエリの集約は、それが属する行に対して行われます。先に両方のテーブルを 結合してから後でグループ化すると、支払いがレンタルの数だけ掛け合わされ、両方の 件数が膨らみます。MySQL の JOIN のパフォーマンスとよくある間違い では、この増加そのものを扱っています。

行をまとめたくない場合

GROUP BY は行をグループごとに 1 行に置き換えます。各行の隣に集約が欲しい場合、 ウィンドウ関数は何もまとめずに、そしてたいていは一時テーブルなしでそれを行います。
MySQL 8.4 のウィンドウ関数で詳しく説明しています。

トラブルシューティング

関連項目