ひとり勉強ログ

ITエンジニアの勉強したことメモ

MySQL講座#07 集約した結果に条件を指定する

HAVING句

GROUP BY句によって、元のテーブルをグループ分けして結果を得ることができるようになった。ここでは、さらにsのグループに対して条件を指定して選択する方法を考える。例えば、「集約した結果がちょうど7行になるようなグループ」を選択するにはどうすれば良いか。

条件指定といえばWHERE句が真っ先に思い浮かぶかもしれない。しかし、WHERE句はあくまで「レコード(行)」に対してのみしか条件を指定できないため、グループに対する条件指定には使えないという制限がある。

したがって、そういう集合に対する条件を指定する句が、別に必要となる。それがHAVING句なのである。

HAVING句を書く位置は、GROUP BY句の後ろである必要がある。DBMS内部での実行順序も、GROUP BY句の後ろになる。

SELECT → FROM → WHERE → GROUP BY → HAVING

ではHAVING句を実際に使用してみる。例えば、categoryで集約したグループに対して、「含まれる行数が9行」という条件を指定すると以下のようになる。

[sql] SELECT category, COUNT(<em>) FROM chojin GROUP BY category HAVING COUNT(</em>) = 9; [/sql]

結果

[sql] +--------------+----------+ | category | COUNT(*) | +--------------+----------+ | 正義超人 | 9 | +--------------+----------+ [/sql]

HAVING句を使用する例をもう1つ挙げてみる。今度も同じくcategoryでグループ化するが、条件を「身長(height)の平均が210以上」に変更する。

HAVING句なしで選択した場合。

[sql] SELECT category, AVG(height) FROM chojin GROUP BY category; [/sql]

結果

[sql] +--------------+-------------+ | category | AVG(height) | +--------------+-------------+ | 完璧超人 | 230.6364 | | 悪魔超人 | 215.4615 | | 正義超人 | 199.4286 | +--------------+-------------+ [/sql]

categoryの3つのグループすべてが結果に表示されていることが分かる。ここにHAVING句で条件を設定する。と以下のようになる。

[sql] SELECT category, AVG(height) FROM chojin GROUP BY category HAVING AVG(height) &gt;= 210; [/sql]

結果

[sql] +--------------+-------------+ | category | AVG(height) | +--------------+-------------+ | 完璧超人 | 230.6364 | | 悪魔超人 | 215.4615 | +--------------+-------------+ [/sql]

身長の平均が199.4286だった正義超人が、結果から消えている。

HAVING句にかける要素

HAVING句も、GROUP BY句を使ったときのSELECT句と同様に、書くことができる要素が制限される。制限内容は全く同じで、HAVING句に書くことができる要素は次の3つになる。

  • 定数
  • 集約関数
  • GROUP BY句で指定した列名(集約キー)

HAVING句でエラーになる例。

[sql] SELECT category, COUNT(*) FROM chojin GROUP BY category HAVING name = 'キン肉マン'; [/sql]

結果

[sql] ERROR 1054 (42S22): Unknown column 'name' in 'having clause' [/sql]

nameという列は、GROUP BY句に含まれていない。したがってHAVING句に書くことも許されない。HAVING句の使い方を考えるときは、「一度集約が終わった段階のテーブルを出発点にしている」と考えると分かりやすい。

このことは、もちろんGROUP BY句を使った場合のSELECT句を考えるときにも当てはまる。テーブルにない列は指定することができない。

HAVING句よりもWHERE句に書いたほうが良い条件

HAVING句にもWHERE句にも書ける条件というのが存在する。それは、「集約キーに対する条件」である。元のテーブルの列のうち、集約キーとして使っているものは、HAVING句にも書くことができる。したがって、以下のSELECT文は正しい構文である。

[sql] SELECT category, COUNT(*) FROM chojin GROUP BY category HAVING category = '悪魔超人'; [/sql]

結果

[sql] +--------------+----------+ | category | COUNT(*) | +--------------+----------+ | 悪魔超人 | 13 | +--------------+----------+ [/sql]

このSELECT文は、以下のように書いた場合と同じ結果を返す。

[sql] SELECT category, COUNT(*) FROM chojin WHERE category = '悪魔超人' GROUP BY category; [/sql]

結果

[sql] +--------------+----------+ | category | COUNT(*) | +--------------+----------+ | 悪魔超人 | 13 | +--------------+----------+ [/sql]

条件を書く場所がWHERE句かHAVING句かの違いだけで、条件の内容は同じ、返す結果も同じ。したがって、どちらの書き方をしても良いのではないか、と思われるかもしれない。

選択される結果を見るだけなら、その通りである。しかし、こういった集約キーに対する条件はWHERE句に書くべきだと考えられる。

理由は、WHERE句とHAVING句の役割の違いという、根本的なものである。HAVING句というものは、「グループ」に対する条件を指定するものである。したがって、単なる「行」に対する条件は、WHERE句で書くようにしたほうが、互いの機能をはっきりさせることができて、読みやすいコードになる。

WHERE句 = 行に対する条件指定
HAVING句 = グループに対する条件指定

WHERE句とHAVING句の実行速度

WHERE句とHAVING句のどちらにも書ける条件を、あえてWHERE句に書くべきもう1つの理由は、パフォーマンス、つまり実行速度に関するものである。一般的に、同じ結果を得られるにせよ、HAVING句よりもWHERE句に条件を記述するほうが、処理速度が速い。

その理由を説明するには、DBMS内部で行われている処理について考える必要がある。COUNT関数などを使って、テーブルのデータを集約する場合、DBMS内部では「ソート」という行の並べ替えが行われている。このソート処理は、かなりマシンに負担をかける、いわゆる「重い」処理である。そのため、なるべくソートする行が少ないほうが、処理が速くなるのである。

WHERE句を使って条件を指定すると、ソート前の行を絞り込むため、ソート対象の行数を減らすことができる。一方、HAVING句はソートが終わってグループ化された後に実行されるため、WHERE句で条件を指定する場合よりソート行数が多くなってしまうのである。こうした内部動作はDBMSによっても異なるが、このソート処理に関しては、多くのDBMSに共通していると言える。

そしてWHERE句が速度の面で有利なもう1つの理由は、WHERE句の条件で指定する列に「索引(インデックス)」を作成することで、処理を大幅に高速化することが可能なことである、この索引という技術は、DBMSのパフォーマンス向上の方法として非常にポピュラーで、かつ効果の高いものであるから、WHERE句にとって非常に有利な材料なのである。