九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、これは開発者がたまに遭遇する「あるある」な話かもしれませんが、DB
からGROUP BY
でデータ取得するとき以下のようなエラーが発生したことはないでしょうか。
Syntax error or access violation: 1055 Expression #(数字) of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘(フィールド名)’ which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by (SQL: select (フィールド名), id from (テーブル名) group by (グループ化フィールド名))
このことを個人的に「ONLY_FULL_GROUP_BY問題」と呼んでいるのですが、なぜこのエラーが発生するのかを知っておかないと、長時間ハマってしまうこともあったりします。
そこで!
今回はGROUP BY
したときのエラーについてご紹介します。
ぜひ皆さんのお役に立てると嬉しいです😊✨
「かく言う私もよくハマってました😂」
目次
どんな時にエラーになる?
例えば、次のようなテーブルがあるとします。
このテーブルには、各ユーザーが投稿した内容が保存されています。
では、「ユーザーごとの最新投稿」を取得する場合を考えてみましょう。
おそらく一番に思いつくのはこんなSQLではないでしょうか。
SELECT *, MAX(id) FROM posts GROUP BY user_id
しかし、残念ながらこの場合はエラーになります。(注:MySQLの設定にもよります)
では、次の場合はどうでしょう。
SELECT user_id, MAX(id) FROM posts GROUP BY user_id
実はこの場合エラーは発生せず次のような結果を取得できます。
つまり、グループ化したときにエラーが発生するのは次の場合です。
- “GROUP BY” で指定していないカラムが “SELECT” に入っている
- ただし、MAX() や MIN() などの集計関数を使っていればどのカラムでもOK
なぜエラーになるのか?
ざっくり言うとMySQL
が優柔不断で、
「いやいや、僕にはどのデータか決めらんないよ・・・😫」
となっています。
では、実際の例を見てみましょう。
例えば次のようなテーブルです。
見ていただくと分かるとおり、ユーザーID1
のデータを赤、2
を緑で色分けしています。
では、user_id
でグループ化します。
(分かりやすくデータはid
、content
だけ)
では、この状態でcontent
を取得しようとすると、MySQL
はどう思うでしょうか。
「グループ化したのはいいけど、contentはいっぱいあるからどれを取ってくればいいか分からない・・・😥」となります。
これがエラーの全貌です。
MAX()
やMIN()
を使うとエラーが出なくなるのもこれが理由です。(「どれ???」がなくなるからです)
解決するには
では、以下のテーブルで「ユーザーごとの最新記事を取得する」ためにはどうすればいいかを見ていきましょう。
※なお、最後のもの以外は、Laravel 7.x
を使ったコードと実際のSQL文の2つをご紹介します。
コードを分ける
まず最もシンプルなのが、コードを分割する方法です。
つまり、
- ユーザーごとの最新IDを取得する
- そのIDを元にpostsデータを取得する
となります。
// 1.ユーザーごとの最新IDを取得 $ids = \App\Post::selectRaw('MAX(id) AS id') ->groupBy('user_id') ->pluck('id'); // 2.IDを元にデータを取得 $posts = \App\Post::whereIn('id', $ids) ->orderBy('id', 'DESC') ->get();
実際のSQL文はこうなります。
SELECT MAX(id) AS id FROM posts GROUP BY user_id SELECT * FROM posts WHERE id IN ((ここに取得したID)) ORDER BY id DESC
WHERE句にサブクエリを使う
WHERE *** IN
句の中にサブクエリを作って実装する方法です。
$posts = \App\Post::whereIn('id', function($q) { // 👇 サブクエリ return $q->from('posts') ->selectRaw('MAX(id) as max_id') ->groupBy('user_id'); }) ->orderBy('id', 'DESC') ->get();
実行されるSQL文はこうなります。
SELECT * FROM posts WHERE id IN ( SELECT MAX(id) AS max_id FROM posts GROUP BY user_id ) ORDER BY id DESC
サブクエリで結合する
こちらもサブクエリを使いますが、使う場所はINNER JOIN
の中です。
$posts = \App\Post::join( \DB::raw('(SELECT MAX(id) AS max_id FROM posts GROUP BY user_id) AS latest'), 'posts.id', '=', 'latest.max_id' ) ->orderBy('id', 'DESC') ->get();
実行されるSQL文はこうなります。
SELECT * FROM posts INNER JOIN ( SELECT MAX(id) AS max_id FROM posts GROUP BY user_id ) AS latest ON posts.id = latest.max_id ORDER BY id DESC
only_full_group_byを解除する
MySQL
が厳格なSQL
を要求しないようにすることができます。
Laravel
の場合、config
ファイルで設定します。
⚠ ただし、この方法はあまりおすすめしません。行のデータ整合性がくずれる可能性があるからです。(例:id
は最新のものなのに、content
は別の行から取得されてしまう)
/config/database.php
'mysql' => [ 'driver' => 'mysql', 'url' => env('DATABASE_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'prefix_indexes' => true, 'strict' => false, // 👈 ここを "false" にする 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ],
おわりに
ということで、今回はMySQL
でGROUP BY
のエラーについてご紹介しました。
なお、その昔はこのような問題は発生していませんでしたので、もしかするとMySQL
をアップグレードして急にエラーが発生することがあるかもしれません(MySQL 5.7以降、ONLY_FULL_GROUP_BY
は初期状態で有効です)
ただ、やはり新しいバージョンで有効になっているということは、そちらの方があるべき姿だと判断されたということですので、ONLY_FULL_GROUP_BY
は有効にして開発した方がいいかと思います。
ぜひ皆さんも一度この問題を頭の中で整理してみてくださいね。
ではでは〜❗
「一回ちゃんと覚えても、
時間が経つと忘れちゃうんですよね(笑)」