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つに分かれます。

  1. その列もグループの単位にしたい場合は、GROUP BY に加える
  2. グループ内の代表値がほしい場合は、MAX(post_title) や MIN(post_date) のように集計関数で包む
  3. どの値でもよいと分かっている場合は、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() では文字の % を %% にする

お気軽にご相談ください

お見積りへ お問い合わせへ
▲