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 では、同じ制限が届くのが遅すぎて、何も絞り込めません。
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 は二重に数えます。
症状は、誰にも説明できない倍率で大きすぎる合計です。
先に結合してから後でグループ化するのではなく、各側をそれぞれのサブクエリで集約し、
その結果を結合します。
行をまとめたくない場合
GROUP BY は行をグループごとに 1 行に置き換えます。各行の隣に集約が欲しい場合、
ウィンドウ関数は何もまとめずに、そしてたいていは一時テーブルなしでそれを行います。
トラブルシューティング
関連項目
- MySQL 8.4 のウィンドウ関数 — 行をまとめずに集約する
- MySQL の JOIN のパフォーマンスとよくある間違い — 一対多の結合が合計を膨らませる理由
- MySQL の CTE が派生テーブルに勝つとき — メインクエリの前に事前集約する
- よくある MySQL のエラー — 文脈の中の ERROR 1055 と ERROR 1111
- GROUP BY — まず句そのものを学びたい場合のチュートリアル