2026年7月22日
2026年7月22日
MySQLのGROUP BYとHAVINGの使い方 集計の基本とONLY_FULL_GROUP_BYエラーの直し方
GROUP BY は指定した列の値ごとに行をまとめて件数や合計を出す句で、HAVING はまとめた後の結果を条件で絞り込む句です。WHERE が「まとめる前の行」を絞り、HAVING が「まとめた後のグループ」を絞る、という違いを押さえれば、使い分けで迷うことはほとんどなくなります。この記事では WordPress のテーブルを題材に、集計関数の基本、WHERE と HAVING の使い分け、MySQL 5.7 以降でよく出る only_full_group_by のエラーの読み方と直し方、実務で役立つ集計の例を説明します。行の絞り込みや JOIN といった SELECT の基本は別記事で扱います。
GROUP BY の基本形
SELECT post_type, COUNT(*) AS cnt
FROM wp_posts
WHERE post_status = 'publish'
GROUP BY post_type
ORDER BY cnt DESC;
公開済みのデータを post_type ごとにまとめ、投稿・固定ページ・カスタム投稿タイプなどがそれぞれ何件あるかを数えます。結果は post_type の種類と同じ行数になります。
よく使う集計関数は次のとおりです。
COUNT(*)行数を数えるCOUNT(列)その列が NULL でない行だけを数えるCOUNT(DISTINCT 列)重複を除いた値の種類を数えるSUM(列)とAVG(列)合計と平均MAX(列)とMIN(列)最大と最小GROUP_CONCAT(列)グループ内の値をカンマ区切りの1つの文字列にまとめる
COUNT(*) と COUNT(列) の違いは見落とされやすい点です。NULL を含む列を数えると件数が少なく出るため、「行数」を知りたいときは COUNT(*) を使います。
複数の列を GROUP BY に並べると、その組み合わせごとにまとまります。
-- 著者ごと・投稿タイプごとの公開数
SELECT post_author, post_type, COUNT(*) AS cnt
FROM wp_posts
WHERE post_status = 'publish'
GROUP BY post_author, post_type;
WHERE と HAVING の使い分け
MySQL は WHERE → GROUP BY → HAVING → SELECT → ORDER BY の順で処理します。WHERE の時点ではまだグループができていないため、COUNT(*) のような集計の結果は WHERE では使えません。集計結果で絞り込みたいときに HAVING を使います。
-- 公開済み投稿が20件以上ある著者だけ
SELECT post_author, COUNT(*) AS cnt
FROM wp_posts
WHERE post_type = 'post'
AND post_status = 'publish'
GROUP BY post_author
HAVING COUNT(*) >= 20
ORDER BY cnt DESC;
ここで post_type = 'post' を HAVING に書くこともできそうに見えますが、WHERE に書くほうが適切です。WHERE は集計の前に行を減らすため処理量が少なく、インデックスも使われやすくなります。集計結果を使わない条件は WHERE、集計結果を使う条件は HAVING、と分けて考えます。
MySQL では HAVING の中で SELECT の別名(上の例の cnt)も使えますが、ほかのデータベースでは使えないことがあるため、移植を考えるなら COUNT(*) と書いておくと無難です。
only_full_group_by のエラーを読む
MySQL 5.7.5 以降は、sql_mode に ONLY_FULL_GROUP_BY がデフォルトで含まれています。次のようなクエリを実行するとエラーになります。
SELECT post_author, post_title, COUNT(*)
FROM wp_posts
WHERE post_type = 'post'
GROUP BY post_author;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause
and contains nonaggregated column 'wordpress.wp_posts.post_title' which is not
functionally dependent on columns in GROUP BY clause; this is incompatible
with sql_mode=only_full_group_by
メッセージは「SELECT の2番目の式 post_title は、GROUP BY に無く、集計関数にも入っていない」という意味です。著者1人に投稿が何十件もあるとき、その著者の行に表示する post_title はどれなのかが決まらないため、MySQL は結果を返さずにエラーにしています。古い MySQL はこの場合に任意の1件を黙って返していたため、昔のコードを新しいサーバーへ移すとこのエラーが表に出てきます。
直し方は、何を表示したいかで3つに分かれます。
- その列もグループの単位にしたい場合は、GROUP BY に加える
- グループ内の代表値がほしい場合は、
MAX(post_title)やMIN(post_date)のように集計関数で包む - どの値でもよいと分かっている場合は、
ANY_VALUE(post_title)で「任意の1件で構わない」と明示する
「各著者の最新の投稿のタイトル」のように、特定の1行の別の列がほしい場合は、集計関数では表せません。MySQL 8.0 以降ならウィンドウ関数の ROW_NUMBER() で著者ごとに番号を振り、1番目だけを取り出すのが素直な書き方です。
sql_mode から ONLY_FULL_GROUP_BY を外してエラーを消す方法も紹介されることがありますが、サーバー全体の設定を変えると、ほかのクエリが不定の値を返しても気付けなくなります。プラグインのクエリが原因なら、まずプラグインの更新や作者への報告を検討し、自分のコードなら上の3つのどれかで直します。
並び順は ORDER BY で指定する
MySQL 5.7 までは GROUP BY の結果がその列の順に並んで返ることが多く、それに頼ったコードもありました。MySQL 8.0 ではこの暗黙の並べ替えが無くなっています。並び順が必要なら、ORDER BY を明示してください。
WordPress の運用で役立つ集計の例
月別の公開数
SELECT DATE_FORMAT(post_date, '%Y-%m') AS ym, COUNT(*) AS cnt
FROM wp_posts
WHERE post_type = 'post' AND post_status = 'publish'
GROUP BY ym
ORDER BY ym;
更新ペースの推移を見るのに使えます。post_date はサイトのタイムゾーン、post_date_gmt は UTC の日時です。
autoload されるオプションの量
SELECT autoload, COUNT(*) AS cnt, SUM(LENGTH(option_value)) AS bytes
FROM wp_options
GROUP BY autoload;
wp_options のうち自動読み込みされるデータの量は、ページ表示の速さに影響します。autoload 列に入る値は WordPress 6.6 で見直され、on・off・auto などの値が加わりました(2026年10月現在、古い yes・no が残っているサイトもあります)。どの値が読み込まれるかはバージョンにより異なるため、結果は値ごとの内訳として眺めるのが無難です。
重複したカスタムフィールド
SELECT post_id, meta_key, COUNT(*) AS cnt
FROM wp_postmeta
GROUP BY post_id, meta_key
HAVING COUNT(*) > 1
ORDER BY cnt DESC
LIMIT 50;
同じ投稿に同じキーのメタが複数ある組み合わせを探します。WordPress は1つのキーに複数の値を持てる設計なので、重複がすべて異常とは限りません。見つかったものを消す前に、どのプラグインのキーかを確かめ、バックアップを取ってから対処します。wp_postmeta が大きいサイトでは時間がかかるため、ステージングやアクセスの少ない時間帯に実行します。
GROUP_CONCAT の長さ制限
GROUP_CONCAT の結果は、group_concat_max_len(デフォルトは 1024 バイト)を超えると黙って切り詰められます。エラーにならないため気付きにくく、ID の一覧などを作るときは SET SESSION group_concat_max_len = 100000; のようにセッション単位で広げてから実行します。
PHP から実行するときの % に注意
$wpdb->prepare() は %s や %d をプレースホルダーとして扱うため、DATE_FORMAT の %Y-%m のような文字そのものの % は %% と書く必要があります。
global $wpdb;
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT DATE_FORMAT(post_date, '%%Y-%%m') AS ym, COUNT(*) AS cnt
FROM {$wpdb->posts}
WHERE post_type = %s AND post_status = 'publish'
GROUP BY ym ORDER BY ym",
'post'
)
);
foreach ( $rows as $row ) {
printf( '<li>%s: %d</li>', esc_html( $row->ym ), (int) $row->cnt );
}
集計は件数が多いほど重くなります。管理画面のダッシュボードなどで毎回実行する場合は、結果を Transients API(set_transient())で一定時間キャッシュすると、データベースの負荷を抑えられます。
まとめ
- GROUP BY は値ごとに行をまとめ、
COUNT(*)・SUM()・MAX()などで集計する。COUNT(列)は NULL を数えない - 集計前の絞り込みは WHERE、集計結果での絞り込みは HAVING。集計を使わない条件は WHERE に書く
only_full_group_byのエラーは、GROUP BY に加える・集計関数で包む・ANY_VALUE()のどれかで直す。サーバー全体のsql_mode変更は避ける- MySQL 8.0 では GROUP BY の暗黙の並べ替えが無い。ORDER BY を明示する
GROUP_CONCATは長さ制限で黙って切れる。$wpdb->prepare()では文字の%を%%にする