先日携わっているプロジェクトにおいてとある不具合に遭遇しました。DBから取得したデータが一部欠損していたのです。調査したところデータを取得する際のクエリにおいてGROUP_CONCAT
が使用されており、そちらの上限をオーバーした事が原因でした。今回は直接Laravelと関係する訳ではありませんが、LAMP環境を運用する上で遭遇するかもしれない事象という事で備忘録としてまとめます。
GROUP_CONCAT()
GROUP_CONCAT
はmysqlの関数です。GROUP BY
でまとめたデータをカンマ区切りなどで取得する際に使います。例えば、以下のようなfoodテーブルがあったとします。
# テーブル作成クエリ CREATE TABLE `food` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '', `category` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`) );
そして、そのテーブルに以下のようなデータが挿入されていたとします。
# データ挿入クエリ INSERT INTO `food` (name, category) VALUES ('みかん', '果物'), ('とまと', '野菜'), ('玉ねぎ', '野菜'), ('きゅうり', '野菜'), ('りんご', '果物'), ('ぶどう', '果物');
GROUP_CONCAT
を使うと、カテゴリ毎に食べ物の名前をカンマ区切りでまとめて取得できます。以下のように。
SELECT category, GROUP_CONCAT(name) FROM food GROUP BY category; >> +----------+----------------------+ | category | GROUP_CONCAT(name) | +----------+----------------------+ | 果物 | みかん,りんご,ぶどう | | 野菜 | とまと,玉ねぎ,きゅうり | +----------+----------------------+ 2 rows in set (0.02 sec)
カンマ区切りの文字列なので、explode()
などで配列に変換すれば使い勝手が良いですね。
GROUP_CONCATの上限
既にお気づきかと思いますがGROUP BY
でまとめたデータが多ければ多いほどGROUP_CONCAT
の結果も長くなります。前項の例で言えば、例えばカテゴリーが果物のレコードが1000件あったなら、1000件分の果物の名前をカンマ区切りで結合して文字列として取得する訳です。とは言え、そんな長い文字列を取得する訳にも行かないので、mysql側でgroup_concat_max_lenというシステム変数によって取得できる最大の長さが制限されています。group_concat_max_len
のデフォルトの設定値は1024バイトです。
SHOW VARIABLES LIKE 'group_concat_max_len'; >> +----------------------+-------+ | Variable_name | Value | +----------------------+-------+ | group_concat_max_len | 1024 | +----------------------+-------+ 1 row in set (0.04 sec)
この制限値を超えた場合、超えた部分のデータは切り捨てられます。group_concat_max_len
の設定値を変えて、実際にこの挙動を確認してみましょう。
まず、GROUP_CONCAT
の上限を20バイトに設定してみます。
SET group_concat_max_len = 20;
そして、前項で実行したクエリをもう一度実行してみましょう。
SELECT category, GROUP_CONCAT(name) FROM food GROUP BY category; >> +----------+----------------------+ | category | GROUP_CONCAT(name) | +----------+----------------------+ | 果物 | みかん,りんご, | | 野菜 | とまと,玉ねぎ, | +----------+----------------------+ 2 rows in set, 2 warnings (0.00 sec)
果物はぶどうが、野菜はきゅうりのデータが欠損しています。そしてwarningが発生しています。
SHOW WARNINGS; >> +---------+------+---------------------------------+ | Level | Code | Message | +---------+------+---------------------------------+ | Warning | 1260 | Row 3 was cut by GROUP_CONCAT() | | Warning | 1260 | Row 6 was cut by GROUP_CONCAT() | +---------+------+---------------------------------+ 2 rows in set (0.00 sec)
3行目と6行目のデータがGROUP_CONCAT
によって省かれた旨の警告でした。ここでいう3行目、6行目とはGROUP BY
で処理される前の結果セットであると推察します。
因みに文字列のバイト数を確認するにはLENGTH
関数が使えます。
SELECT LENGTH('みかん,りんご,'); >> +--------------------------------+ | LENGTH('みかん,りんご,') | +--------------------------------+ | 20 | +--------------------------------+ 1 row in set (0.00 sec)
先ほど途中でカットされたデータのバイト数を確認すると丁度group_concat_max_len
に設定した値である20バイトとなっていますね。内訳は平仮名が1文字3バイト*6文字、カンマが1バイト*2文字で合計20バイトという訳です。この様にgroup_concat_max_len
で設定したバイト数以上の文字列を取得しようとした場合、制限を超えた分の文字列が欠損して取得されてしまうのです。そして、これがまさに今回私が遭遇した不具合の原因です。
GROUP_CONCATの設定を変更する
GROUP_CONCAT
の返却値の長さが決まっているなら、group_concat_max_len
を余裕を持たせた充分な値に設定する事で取得するデータの欠損を予防できます。アプリ内の特定の箇所にのみ設定値の引き上げが必要な場合はDB::statement()
で一時的に設定すれば良さそうです。
// デフォルトの1024 byteから2倍に引き上げ DB::statement('SET group_concat_max_len = 2048;')
あるいはアプリ全体で上限を引き上げたいのであればmysqlの設定ファイル、my.cnfに設定を追加してあげればOKです。
[mysqld] group_concat_max_len=2048
設定を反映させるにはmysqlの再起動が必要なのでお忘れなく。
まとめ
原因が分かるとなんて事ない不具合ですが、直接的なエラーが発生しない為気付くのに少し時間が掛かりました。経験上GROUP_CONCAT
を使う頻度は多くありませんが、こういう事もあるということを頭の片隅においておきたいものです。