- MySQL
MySQL GROUP_CONCATの使い方|区切り文字・並び順・文字数上限
GROUP_CONCATとは、GROUP BYでまとめた複数行の値を、1つの文字列に連結して返すMySQLの集約関数です。 「会員ごとのタグをカンマ区切りで1行にしたい」「部署ごとのメンバー名を並べたい」といった、縦に並んだ値を横に1つにまとめる場面で使います。
この記事では、区切り文字(SEPARATOR)・並び順(ORDER BY)・重複除去(DISTINCT)の組み合わせを、MySQL 8.0.46で実際に実行した結果つきで並べました。NULLの扱い、既定の1024バイトで結果が切れる現象と警告の出力、group_concat_max_len をセッション・グローバル・設定ファイルで変える手順、JSON_ARRAYAGGとの使い分け、他のデータベースでの対応関数まで順に確認します。
GROUP_CONCATの構文早見表
構文は次のとおりです(MySQL 8.0リファレンスマニュアルの集約関数のページを今回開いて確認しました)。
GROUP_CONCAT([DISTINCT] expr [, expr ...]
[ORDER BY {unsigned_integer | col_name | expr} [ASC | DESC] [, col_name ...]]
[SEPARATOR str_val])
書く順番は DISTINCT → 式 → ORDER BY → SEPARATOR で固定です。下の表の結果は、この記事のサンプルデータ(member が sato の行)で実行して得た値です。
| やりたいこと | 書き方 | satoの結果 |
|---|---|---|
| カンマ区切りで連結(基本) | GROUP_CONCAT(tag) |
SQL,Java,SQL |
| 区切り文字を変える | GROUP_CONCAT(tag SEPARATOR ' / ') |
SQL / Java / SQL |
| 区切り文字なしで連結 | GROUP_CONCAT(tag SEPARATOR '') |
SQLJavaSQL |
| 並び順を指定 | GROUP_CONCAT(tag ORDER BY tag) |
Java,SQL,SQL |
| 別の列で降順に並べる | GROUP_CONCAT(tag ORDER BY score DESC) |
Java,SQL,SQL |
| 重複を除く | GROUP_CONCAT(DISTINCT tag) |
Java,SQL |
| 3つ全部を組み合わせる | GROUP_CONCAT(DISTINCT tag ORDER BY tag DESC SEPARATOR '|') |
SQL|Java |
| 複数の列をつなげてから連結 | GROUP_CONCAT(tag, ':', score ORDER BY score DESC SEPARATOR ', ') |
Java:95, SQL:80, SQL:70 |
| NULLを文字に置き換えて含める | GROUP_CONCAT(IFNULL(tag,'(none)') ORDER BY id) |
SQL,Java,SQL,(none) |
| 先頭2件だけ取り出す | SUBSTRING_INDEX(GROUP_CONCAT(tag ORDER BY score DESC), ',', 2) |
Java,SQL |
| 上限を広げる(セッション) | SET SESSION group_concat_max_len = 1000000; |
(設定変更) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MySQL GROUP_CONCATの使い方|区切り文字・並び順・文字数上限」
検証環境とサンプルデータ
| 項目 | 内容 |
|---|---|
| MySQL | 8.0.46(Ubuntu 24.04のパッケージ、x86_64) |
| クライアント | mysql コマンド(--table で表形式に出力) |
| sql_mode | 既定値(ONLY_FULL_GROUP_BY、STRICT_TRANS_TABLES などを含む) |
| group_concat_max_len | 既定値 1024 |
| max_allowed_packet | 67108864(64MB、既定値) |
MySQL 8.4や9.x系では今回は動かしていません(未実行)。以下の結果はすべて8.0.46のものです。
サンプルは、会員とタグの対応を持つテーブルです。NULLと重複をわざと混ぜてあります。
CREATE TABLE member_tags (
id INT AUTO_INCREMENT PRIMARY KEY,
member VARCHAR(20) NOT NULL,
tag VARCHAR(20) NULL,
score INT NULL
);
INSERT INTO member_tags (member, tag, score) VALUES
('sato', 'SQL', 80),
('sato', 'Java', 95),
('sato', 'SQL', 70),
('sato', NULL, 60),
('suzuki', 'Python', 88),
('suzuki', 'Go', 72),
('tanaka', NULL, NULL);
+----+--------+--------+-------+
| id | member | tag | score |
+----+--------+--------+-------+
| 1 | sato | SQL | 80 |
| 2 | sato | Java | 95 |
| 3 | sato | SQL | 70 |
| 4 | sato | NULL | 60 |
| 5 | suzuki | Python | 88 |
| 6 | suzuki | Go | 72 |
| 7 | tanaka | NULL | NULL |
+----+--------+--------+-------+
基本の使い方
グループごとにカンマ区切りで連結する
SELECT member, GROUP_CONCAT(tag) AS tags
FROM member_tags
GROUP BY member;
+--------+--------------+
| member | tags |
+--------+--------------+
| sato | SQL,Java,SQL |
| suzuki | Python,Go |
| tanaka | NULL |
+--------+--------------+
区切り文字を指定しない場合はカンマ(スペースなし)でつながります。satoのNULLの行は結果に現れず、タグがNULLだけのtanakaは結果そのものがNULLになりました(NULLの扱いは後の章で詳しく見ます)。
GROUP BYなしで全体を1行にする
SELECT GROUP_CONCAT(tag) AS all_tags FROM member_tags;
+------------------------+
| all_tags |
+------------------------+
| SQL,Java,SQL,Python,Go |
+------------------------+
GROUP BYを書かなければ、対象の行全体が1つのグループになります。
区切り文字を変える(SEPARATOR)
SEPARATORはかっこの中の最後に書きます。カンマは付けません。
SELECT member, GROUP_CONCAT(tag SEPARATOR ' / ') AS tags
FROM member_tags GROUP BY member;
+--------+------------------+
| member | tags |
+--------+------------------+
| sato | SQL / Java / SQL |
| suzuki | Python / Go |
| tanaka | NULL |
+--------+------------------+
空文字を指定すると、区切りなしでつながります。
SELECT member, GROUP_CONCAT(tag SEPARATOR '') AS tags
FROM member_tags GROUP BY member;
+--------+------------+
| member | tags |
+--------+------------+
| sato | SQLJavaSQL |
| suzuki | PythonGo |
| tanaka | NULL |
+--------+------------+
| 区切り文字 | 書き方 |
|---|---|
| カンマ(既定) | 省略、または SEPARATOR ',' |
| カンマとスペース | SEPARATOR ', ' |
| スラッシュ | SEPARATOR ' / ' |
| 読点 | SEPARATOR '、' |
| 改行 | SEPARATOR '\n' |
| なし | SEPARATOR '' |
読点と改行の行は書き方の紹介で、出力は掲載していません(読点は実行して連結されることを確認、改行は未実行)。
並び順を指定する(ORDER BY)
ORDER BYを書かない場合、連結される順番は決まっていません。 上の結果では挿入順に見えますが、行数が増えたり実行計画が変わったりすると順番が変わることがあります。順番に意味があるときは、関数の中にORDER BYを書きます。
SELECT member, GROUP_CONCAT(tag ORDER BY tag) AS tags
FROM member_tags GROUP BY member;
+--------+--------------+
| member | tags |
+--------+--------------+
| sato | Java,SQL,SQL |
| suzuki | Go,Python |
| tanaka | NULL |
+--------+--------------+
連結する列とは別の列で並べることもできます。scoreの高い順にタグを並べる例です。
SELECT member, GROUP_CONCAT(tag ORDER BY score DESC) AS tags
FROM member_tags GROUP BY member;
+--------+--------------+
| member | tags |
+--------+--------------+
| sato | Java,SQL,SQL |
| suzuki | Python,Go |
| tanaka | NULL |
+--------+--------------+
satoは Java(95)、SQL(80)、SQL(70) の順、suzukiは Python(88)、Go(72) の順になりました。
数値の列をそのまま連結した場合は、数値として並びます。
SELECT member, GROUP_CONCAT(score ORDER BY score) AS scores
FROM member_tags GROUP BY member;
+--------+-------------+
| member | scores |
+--------+-------------+
| sato | 60,70,80,95 |
| suzuki | 72,88 |
| tanaka | NULL |
+--------+-------------+
なお、クエリ全体の末尾に書くORDER BYは結果の行の並びを決めるもので、連結の中身の順番には関係しません。両方を指定したいときは、関数の中と外の両方に書きます。
重複を除く(DISTINCT)
SELECT member, GROUP_CONCAT(DISTINCT tag) AS tags
FROM member_tags GROUP BY member;
+--------+-----------+
| member | tags |
+--------+-----------+
| sato | Java,SQL |
| suzuki | Go,Python |
| tanaka | NULL |
+--------+-----------+
satoの「SQL」が1つにまとまりました。今回の結果では文字の昇順に並んでいますが、順番を保証したいときはORDER BYも書きます。
DISTINCT・ORDER BY・SEPARATORを全部使う
SELECT member,
GROUP_CONCAT(DISTINCT tag ORDER BY tag DESC SEPARATOR '|') AS tags
FROM member_tags GROUP BY member;
+--------+-----------+
| member | tags |
+--------+-----------+
| sato | SQL|Java |
| suzuki | Python|Go |
| tanaka | NULL |
+--------+-----------+
DISTINCTと「別の列でのORDER BY」は組み合わせに注意
DISTINCTを付けたまま、連結する列とは別の列で並べると、どの行の値を基準にするかがあいまいになります。実行するとエラーにはならず、次の結果になりました。
SELECT member, GROUP_CONCAT(DISTINCT tag ORDER BY score)
FROM member_tags GROUP BY member;
+--------+-------------------------------------------+
| member | GROUP_CONCAT(DISTINCT tag ORDER BY score) |
+--------+-------------------------------------------+
| sato | SQL,Java |
| suzuki | Go,Python |
| tanaka | NULL |
+--------+-------------------------------------------+
satoの「SQL」にはscoreが80と70の2行あり、どちらを使って並べるかは読み手にも伝わりにくくなります。DISTINCTを使うときは、ORDER BYにも同じ式を書く形にそろえると、結果が読み取りやすくなります。
複数の列をまとめて連結する
式をカンマで並べると、1行ぶんの値を先につなげてから連結します。
SELECT member,
GROUP_CONCAT(tag, ':', score ORDER BY score DESC SEPARATOR ', ') AS tags
FROM member_tags GROUP BY member;
+--------+-------------------------+
| member | tags |
+--------+-------------------------+
| sato | Java:95, SQL:80, SQL:70 |
| suzuki | Python:88, Go:72 |
| tanaka | NULL |
+--------+-------------------------+
satoには「tagがNULL、scoreが60」の行がありますが、結果に含まれていません。並べた式のうち1つでもNULLだと、その行はまるごと連結の対象から外れます。
CONCAT_WS(NULLを飛ばして区切り文字でつなぐ関数)を使うと、動きが変わります。
SELECT member,
GROUP_CONCAT(CONCAT_WS(':', tag, score) ORDER BY score DESC SEPARATOR ', ') AS tags
FROM member_tags GROUP BY member;
+--------+-----------------------------+
| member | tags |
+--------+-----------------------------+
| sato | Java:95, SQL:80, SQL:70, 60 |
| suzuki | Python:88, Go:72 |
| tanaka | |
+--------+-----------------------------+
CONCAT_WSはNULLの引数を飛ばして残りをつなぐので、satoには「60」だけの要素が加わり、tanakaはNULLではなく空文字になりました。どちらの動きがほしいかで使い分けます。
NULLの扱いを確認する
| 状況 | 結果 | 今回の確認 |
|---|---|---|
| グループの中にNULLと値が混ざっている | NULLは無視され、値だけが連結される | satoが SQL,Java,SQL |
| グループの値がすべてNULL | 結果はNULL | tanakaが NULL |
| 対象の行が0件 | 結果はNULL(1行返る) | WHERE 1=0 で NULL |
| 複数の式のうち1つがNULL | その行は連結されない | satoの tag=NULL, score=60 の行が出ない |
0件のときの結果です。
SELECT GROUP_CONCAT(tag) AS t FROM member_tags WHERE 1=0;
+------+
| t |
+------+
| NULL |
+------+
NULLも1要素として出したい
IFNULL(またはCOALESCE)で、連結する前に文字へ置き換えます。
SELECT member, GROUP_CONCAT(IFNULL(tag,'(none)') ORDER BY id) AS tags
FROM member_tags GROUP BY member;
+--------+---------------------+
| member | tags |
+--------+---------------------+
| sato | SQL,Java,SQL,(none) |
| suzuki | Python,Go |
| tanaka | (none) |
+--------+---------------------+
結果のNULLを別の表示にしたい
連結した結果のほうをIFNULLで包みます。件数を知りたいときは COUNT(tag) を並べると、NULLを除いた数が取れます。
SELECT member,
IFNULL(GROUP_CONCAT(tag), '-') AS tags,
COUNT(tag) AS cnt
FROM member_tags GROUP BY member;
+--------+--------------+-----+
| member | tags | cnt |
+--------+--------------+-----+
| sato | SQL,Java,SQL | 3 |
| suzuki | Python,Go | 2 |
| tanaka | - | 0 |
+--------+--------------+-----+
JOINと組み合わせる定番形
親テーブルにLEFT JOINして、子の値を1列にまとめる形です。タグを1つも持たない会員(ito)も1行残ります。
CREATE TABLE members (member VARCHAR(20) PRIMARY KEY, dept VARCHAR(20));
INSERT INTO members VALUES
('sato','dev'), ('suzuki','dev'), ('tanaka','admin'), ('ito','admin');
SELECT m.dept, m.member,
GROUP_CONCAT(DISTINCT t.tag ORDER BY t.tag SEPARATOR ' / ') AS tags,
COUNT(DISTINCT t.tag) AS cnt
FROM members m
LEFT JOIN member_tags t ON t.member = m.member
GROUP BY m.dept, m.member
ORDER BY m.dept, m.member;
+-------+--------+-------------+-----+
| dept | member | tags | cnt |
+-------+--------+-------------+-----+
| admin | ito | NULL | 0 |
| admin | tanaka | NULL | 0 |
| dev | sato | Java / SQL | 2 |
| dev | suzuki | Go / Python | 2 |
+-------+--------+-------------+-----+
部署ごとにメンバー名をまとめる形も同じ要領です。
SELECT dept, GROUP_CONCAT(member ORDER BY member) AS members
FROM members GROUP BY dept;
+-------+-------------+
| dept | members |
+-------+-------------+
| admin | ito,tanaka |
| dev | sato,suzuki |
+-------+-------------+
そのほかの応用
連結結果から先頭のN件だけを取り出すには、SUBSTRING_INDEXを組み合わせます。GROUP_CONCATの中にLIMITは書けません(8.0.46では構文エラーになりました)。
SELECT member,
SUBSTRING_INDEX(GROUP_CONCAT(tag ORDER BY score DESC), ',', 2) AS top2
FROM member_tags GROUP BY member;
+--------+-----------+
| member | top2 |
+--------+-----------+
| sato | Java,SQL |
| suzuki | Python,Go |
| tanaka | NULL |
+--------+-----------+
特定の値を含むグループだけに絞るには、HAVINGでFIND_IN_SETを使えます。
SELECT member FROM member_tags
GROUP BY member
HAVING FIND_IN_SET('Java', GROUP_CONCAT(tag)) > 0;
+--------+
| member |
+--------+
| sato |
+--------+
テーブルの列名をカンマ区切りで取り出すのにも使えます。SELECT文やINSERT文の列リストを作るときに便利です。
SELECT GROUP_CONCAT(CONCAT('`', column_name, '`')
ORDER BY ordinal_position SEPARATOR ', ') AS cols
FROM information_schema.columns
WHERE table_schema = 't234' AND table_name = 'member_tags';
+--------------------------------+
| cols |
+--------------------------------+
| `id`, `member`, `tag`, `score` |
+--------------------------------+
1024バイトで結果が切れる現象を再現する
GROUP_CONCATの結果には長さの上限があり、システム変数 group_concat_max_len で決まります。既定値は1024(バイト)で、超えた分は切り捨てられます。 SELECTではエラーにならず警告だけが出るため、気づかないまま途中までの文字列を使ってしまうことがあります。
再現用のデータ
1〜1000の数字を持つテーブルを作ります。
CREATE TABLE nums (n INT PRIMARY KEY);
INSERT INTO nums
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 1000)
SELECT n FROM s;
現在の設定値を確認します。
SELECT @@session.group_concat_max_len, @@global.group_concat_max_len, @@max_allowed_packet;
+--------------------------------+-------------------------------+----------------------+
| @@session.group_concat_max_len | @@global.group_concat_max_len | @@max_allowed_packet |
+--------------------------------+-------------------------------+----------------------+
| 1024 | 1024 | 67108864 |
+--------------------------------+-------------------------------+----------------------+
連結すると途中で切れる
1〜1000をカンマ区切りでつなぐと、本来は3892バイト必要です(SELECT SUM(LENGTH(n)) + COUNT(*) - 1 FROM nums; の結果が3892)。実行してみます。
SELECT LENGTH(GROUP_CONCAT(n ORDER BY n)) AS len FROM nums;
+------+
| len |
+------+
| 1024 |
+------+
1 row in set, 1 warning
長さが1024で止まり、末尾に 1 warning と表示されました。末尾20文字を見ると、283の後ろのカンマで終わっています。
SELECT RIGHT(GROUP_CONCAT(n ORDER BY n), 20) AS tail FROM nums;
+----------------------+
| tail |
+----------------------+
| 279,280,281,282,283, |
+----------------------+
SHOW WARNINGSで警告を見る
SHOW WARNINGS;
+---------+------+-----------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------+
| Warning | 1260 | Row 284 was cut by GROUP_CONCAT() |
+---------+------+-----------------------------------+
警告コード1260「Row 284 was cut by GROUP_CONCAT()」が、切り捨てが起きた合図です。「284行目の値を入れようとしたところで上限に達した」という意味です。件数だけを知りたいときは SHOW COUNT(*) WARNINGS; が使え、今回は1が返りました。
mysqlコマンドでは、1 row in set, 1 warning の表示がきっかけになります。アプリケーションから実行する場合は警告が画面に出ないことが多いので、長い連結が想定されるクエリでは、あらかじめ上限を広げておくのが確実です。
INSERT ... SELECTではエラーになって止まる
同じ連結を、厳密モード(既定のsql_modeに含まれるSTRICT_TRANS_TABLES)のままテーブルへ書き込もうとすると、警告ではなくエラーになりました。
CREATE TABLE summary (v TEXT);
INSERT INTO summary SELECT GROUP_CONCAT(n ORDER BY n) FROM nums;
ERROR 1260 (HY000): Row 284 was cut by GROUP_CONCAT()
SELECT COUNT(*) FROM summary;
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
行は入っていません。CREATE TABLE ... AS SELECT でも同じERROR 1260になりました。途中までの文字列が保存されずに済むので、書き込みの場面ではこの動きが助けになります。
| 場面 | 上限を超えたとき(8.0.46、既定のsql_mode) |
|---|---|
| SELECT | 1024バイトで切れた結果が返り、Warning 1260が出る |
| INSERT ... SELECT | ERROR 1260で止まり、行は入らない |
| CREATE TABLE ... AS SELECT | ERROR 1260で止まり、テーブルは作られない |
日本語(マルチバイト文字)のとき
上限の単位はバイトです。utf8mb4では、ひらがなや漢字は1文字3バイトなので、文字数で見ると上限はもっと早く来ます。「あ」10文字の行を連結して確かめました(クライアントは --default-character-set=utf8mb4 で接続)。
SELECT LENGTH(g) AS bytes, CHAR_LENGTH(g) AS chars
FROM (SELECT GROUP_CONCAT(REPEAT('あ',10) SEPARATOR '') AS g FROM nums) x;
SHOW WARNINGS;
+-------+-------+
| bytes | chars |
+-------+-------+
| 1023 | 341 |
+-------+-------+
+---------+------+----------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------+
| Warning | 1260 | Row 35 was cut by GROUP_CONCAT() |
+---------+------+----------------------------------+
341文字(1023バイト)で止まりました。 1024バイトちょうどではなく、文字の途中で切れない位置までになっています。日本語を連結する場合、既定値のままでは約340文字が上限と考えておくと見積もりやすくなります。
group_concat_max_lenを変更する手順
このセッションだけ変える(SET SESSION)
SET SESSION group_concat_max_len = 1000000;
SELECT LENGTH(GROUP_CONCAT(n ORDER BY n)) AS len,
RIGHT(GROUP_CONCAT(n ORDER BY n), 20) AS tail
FROM nums;
SHOW WARNINGS;
+------+----------------------+
| len | tail |
+------+----------------------+
| 3892 | 996,997,998,999,1000 |
+------+----------------------+
Empty set
3892バイトすべてが連結され、警告も出なくなりました。この設定はいまの接続だけに効き、接続を切ると元に戻ります。既定値に戻すには SET SESSION group_concat_max_len = DEFAULT; を実行します(1024に戻ることを確認しました)。
特定のクエリの直前でだけ広げたい場合に向いています。権限も特別なものは要りません。
サーバー全体の既定値を変える(SET GLOBAL)
SET GLOBAL group_concat_max_len = 1000000;
SELECT @@session.group_concat_max_len AS sess, @@global.group_concat_max_len AS glob;
+------+---------+
| sess | glob |
+------+---------+
| 1024 | 1000000 |
+------+---------+
SET GLOBALを実行しても、実行したセッション自身の値は1024のままでした。グローバルの値は「これから新しく接続するセッションの初期値」になります。すでにつながっているセッションには反映されないので、アプリケーションがコネクションプールを使っている場合は、接続の作り直しが必要です。
SET GLOBALには、SYSTEM_VARIABLES_ADMIN権限(または従来のSUPER権限)が必要です。今回はrootで実行しており、権限の無いユーザーでのエラーは未実行です。また、SET GLOBALの値はサーバーを再起動すると消えます。
再起動後も残す(SET PERSIST)
MySQL 8.0では、SET PERSISTで「いまのグローバル値の変更」と「再起動後への保存」を同時に行えます。保存先はデータディレクトリの mysqld-auto.cnf です。
SET PERSIST group_concat_max_len = 1000000;
SELECT variable_name, variable_value, variable_source
FROM performance_schema.variables_info
JOIN performance_schema.persisted_variables USING (variable_name);
+----------------------+----------------+-----------------+
| variable_name | variable_value | variable_source |
+----------------------+----------------+-----------------+
| group_concat_max_len | 1000000 | DYNAMIC |
+----------------------+----------------+-----------------+
取り消すときは RESET PERSIST group_concat_max_len; を実行します(今回、SET PERSISTからRESET PERSISTまでを実行し、persisted_variablesに記録されることを確認しました。PERSISTした状態での再起動は未実行です)。
設定ファイル(my.cnf)に書く
設定ファイルの [mysqld] セクションに1行足して、サーバーを再起動します。
[mysqld]
group_concat_max_len = 1000000
今回は、この2行を書いたファイルを --defaults-extra-file で読み込ませてmysqldを起動し直し、反映を確認しました。
SELECT @@session.group_concat_max_len AS sess, @@global.group_concat_max_len AS glob;
SELECT variable_name, variable_source, variable_path
FROM performance_schema.variables_info
WHERE variable_name = 'group_concat_max_len';
SELECT LENGTH(GROUP_CONCAT(n)) AS len FROM nums;
+---------+---------+
| sess | glob |
+---------+---------+
| 1000000 | 1000000 |
+---------+---------+
+----------------------+-----------------+-----------------+
| variable_name | variable_source | variable_path |
+----------------------+-----------------+-----------------+
| group_concat_max_len | EXTRA | /tmp/t234my.cnf |
+----------------------+-----------------+-----------------+
+------+
| len |
+------+
| 3892 |
+------+
再起動後は、新しいセッションの値も最初から1000000になっています。/tmp/t234my.cnf は今回の検証用に置いたファイルのパスです。variable_source と variable_path の列で、値がどこから来たか(EXTRAは追加の設定ファイル、DYNAMICはSETで変更、COMPILEDは既定値)を確かめられます。通常の /etc/mysql/ 配下の設定ファイルに書く場合の表示は、環境に合わせて変わります(/etc/mysql/ 配下への書き込みは、共有の検証環境のため未実行です)。
どの方法を選ぶか
| 方法 | 効く範囲 | 再起動後 | 向いている場面 |
|---|---|---|---|
SET SESSION |
いまの接続だけ | 残らない | 特定のクエリやバッチの直前で広げる |
SET GLOBAL |
以後の新しい接続 | 残らない | すぐに全体へ反映したい。恒久化は別途行う |
SET PERSIST |
以後の新しい接続 | 残る | 8.0以降で、SQLだけで恒久化したい |
| my.cnf | サーバー全体 | 残る | 設定をファイルで管理している |
値の範囲といくつにするか
| 項目 | 内容 | 確認方法 |
|---|---|---|
| 既定値 | 1024 | 実行して確認 |
| 最小値 | 4(3を指定すると4に丸められ、Warning 1292が出た) | 実行して確認 |
| 設定できる最大値 | 64ビット環境で18446744073709551615 | 実行して設定できることを確認 |
| 実際に返せる長さの上限 | max_allowed_packet の値まで |
公式ドキュメントで確認 |
公式ドキュメントでは、必要な長さを見積もって設定すること、むやみに大きな値にするとメモリの使用量が増える場合があることが案内されています。「想定する最大の件数 ×(1件の最大バイト数 + 区切り文字のバイト数)」を目安に、余裕を持たせた値にするのがおすすめです。見積もりには、今回使った SUM(LENGTH(列)) + COUNT(*) - 1 のようなSQLが役立ちます。
なお、group_concat_max_len が512以下のときは結果の型がVARCHAR、512を超えるとTEXT(バイナリ文字列ならBLOB)になります。512に設定して CREATE TABLE ... AS SELECT GROUP_CONCAT(...) を実行したところ、列の型は varchar(512) になりました。
JSON_ARRAYAGGとの使い分け
GROUP_CONCATの結果は「区切り文字でつないだ1本の文字列」です。受け取った側で分割して使うなら、値そのものに区切り文字が含まれていると元に戻せなくなります。 この場合は、JSONの配列にまとめるJSON_ARRAYAGGが向いています。
INSERT INTO member_tags (member, tag, score) VALUES
('yamada', 'C,C++', 50), ('yamada', 'He said "hi"', 40);
SELECT member, GROUP_CONCAT(tag) AS gc, JSON_ARRAYAGG(tag) AS ja
FROM member_tags WHERE member = 'yamada' GROUP BY member;
+--------+--------------------+-----------------------------+
| member | gc | ja |
+--------+--------------------+-----------------------------+
| yamada | C,C++,He said "hi" | ["C,C++", "He said \"hi\""] |
+--------+--------------------+-----------------------------+
GROUP_CONCATの結果は、カンマで分けると3つに見えてしまいます。JSON_ARRAYAGGは要素が2つであることが保たれ、ダブルクォートもエスケープされています。
NULLと上限の違い
SELECT member, JSON_ARRAYAGG(tag) AS tags
FROM member_tags GROUP BY member;
+--------+------------------------------+
| member | tags |
+--------+------------------------------+
| sato | ["SQL", "Java", "SQL", null] |
| suzuki | ["Python", "Go"] |
| tanaka | [null] |
+--------+------------------------------+
(yamadaを追加する前の結果です。)JSON_ARRAYAGGはNULLを無視せず、null として配列に入れます。 また、1〜1000を JSON_ARRAYAGG(n) でまとめると長さは4893バイトで、group_concat_max_len が1024のままでも切れず、警告も出ませんでした。
オブジェクトの配列にすることもできます。
SELECT member, JSON_ARRAYAGG(JSON_OBJECT('tag', tag, 'score', score)) AS tags
FROM member_tags GROUP BY member;
[{"tag": "Python", "score": 88}, {"tag": "Go", "score": 72}]
(横に長いため、出力のうちsuzukiのtags列の値だけを抜き出しています。)
JSON_ARRAYAGGは並び順を指定できない
8.0.46では、JSON_ARRAYAGGの中にORDER BYを書くと構文エラーになりました。
SELECT member, JSON_ARRAYAGG(tag ORDER BY tag) FROM member_tags GROUP BY member;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'ORDER BY tag) FROM member_tags GROUP BY member' at line 1
公式ドキュメントにも、配列の要素の順序は未定義と書かれています。順番を決めたJSON配列がほしいときは、GROUP_CONCATとJSON_QUOTEを組み合わせる方法があります。
SELECT member,
CAST(CONCAT('[', GROUP_CONCAT(JSON_QUOTE(tag) ORDER BY tag), ']') AS JSON) AS sorted_json
FROM member_tags GROUP BY member;
+--------+-----------------------------+
| member | sorted_json |
+--------+-----------------------------+
| sato | ["Java", "SQL", "SQL"] |
| suzuki | ["Go", "Python"] |
| tanaka | NULL |
| yamada | ["C,C++", "He said \"hi\""] |
+--------+-----------------------------+
この形はGROUP_CONCATを通るので、group_concat_max_len の上限とNULLが無視される性質はそのまま当てはまります。
比較表
| 比べる点 | GROUP_CONCAT | JSON_ARRAYAGG |
|---|---|---|
| 結果 | 区切り文字でつないだ文字列 | JSONの配列 |
| NULL | 無視する | null として入る |
| 並び順の指定 | ORDER BYを書ける | 書けない(8.0.46で構文エラー) |
| 重複除去 | DISTINCTを書ける | 今回は未確認 |
| 長さの上限 | group_concat_max_len(既定1024バイト) |
この上限の対象外(今回4893バイトでも切れず) |
| 値に区切り文字が含まれるとき | 分割で崩れる | 崩れない |
| ウィンドウ関数(OVER) | 使えない(8.0.46でERROR 1235) | 使える |
| 向いている場面 | 画面や帳票にそのまま出す文字列、IN句の材料 | アプリケーションで配列として受け取る |
ウィンドウ関数として使ったときの結果です。
SELECT member, GROUP_CONCAT(tag) OVER (PARTITION BY member) FROM member_tags;
ERROR 1235 (42000): This version of MySQL doesn't yet support 'group_concat as window function'
SELECT member, JSON_ARRAYAGG(tag) OVER (PARTITION BY member ORDER BY id) AS running
FROM member_tags WHERE member = 'suzuki';
+--------+------------------+
| member | running |
+--------+------------------+
| suzuki | ["Python"] |
| suzuki | ["Python", "Go"] |
+--------+------------------+
連結した文字列を行に戻す
逆方向の操作(カンマ区切りの文字列を複数行に分ける)は、JSON_TABLEで行えます。
SELECT jt.tag
FROM JSON_TABLE(
CONCAT('["', REPLACE('SQL,Java,Go', ',', '","'), '"]'),
'$[*]' COLUMNS (tag VARCHAR(20) PATH '$')
) jt;
+------+
| tag |
+------+
| SQL |
| Java |
| Go |
+------+
他のデータベースでの対応関数
同じ「グループの値を1つの文字列にまとめる」機能は、データベースごとに関数名と書き方が違います。
| データベース | 関数 | 書き方の例 | 今回の確認 |
|---|---|---|---|
| MySQL | GROUP_CONCAT | GROUP_CONCAT(DISTINCT tag ORDER BY tag SEPARATOR ',') |
8.0.46で実行 |
| PostgreSQL | STRING_AGG | string_agg(tag, ',' ORDER BY tag) |
16.15で実行 |
| SQLite | GROUP_CONCAT | group_concat(tag, ',') |
3.45.1で実行 |
| SQL Server | STRING_AGG | STRING_AGG(tag, ',') WITHIN GROUP (ORDER BY tag) |
未実行 |
| Oracle Database | LISTAGG | LISTAGG(tag, ',') WITHIN GROUP (ORDER BY tag) |
未実行 |
PostgreSQL 16.15で同じデータを使って実行した結果です。
SELECT member,
string_agg(tag, ',' ORDER BY tag) AS tags,
string_agg(DISTINCT tag, ',') AS uniq,
json_agg(tag) AS j
FROM member_tags GROUP BY member ORDER BY member;
member | tags | uniq | j
--------+--------------+-----------+------------------------------
sato | Java,SQL,SQL | Java,SQL | ["Java", "SQL", "SQL", null]
suzuki | Go,Python | Go,Python | ["Go", "Python"]
tanaka | | | [null]
MySQLと比べたときの違いをまとめます。
| 比べる点 | MySQL GROUP_CONCAT | PostgreSQL STRING_AGG |
|---|---|---|
| 区切り文字 | 省略可(既定はカンマ)。SEPARATORで指定 | 第2引数で必ず指定 |
| NULL | 無視する | 無視する(今回の結果でも同じ) |
| 全部NULLのとき | NULL | NULL(psqlでは空欄で表示) |
| 数値の連結 | そのまま渡せる | n::text のように文字列へキャストする |
| 既定の長さ上限 | 1024バイト | 1〜1000の連結で3892文字がそのまま返った |
SQLite 3.45.1では、group_concat(tag) が SQL,Java,SQL、group_concat(tag, ' / ') が SQL / Java / SQL、group_concat(DISTINCT tag) が SQL,Java を返し、NULLは無視されました。関数の中の ORDER BY tag も3.45.1で動作しました。SQL ServerとOracle Databaseの書き方は一般に知られた構文で、今回は実行していません。
つまずきやすい点の一覧
| 症状 | 原因 | 対処 |
|---|---|---|
| 結果が途中で終わっている | group_concat_max_len(既定1024バイト)を超えた |
SET SESSION group_concat_max_len = ... で広げる |
1 row in set, 1 warning と出る |
切り捨ての警告(1260)が出ている | SHOW WARNINGS; で内容を確認する |
ERROR 1260: Row N was cut by GROUP_CONCAT() |
厳密モードで切り捨てが起きる連結を書き込もうとした | 上限を広げてから実行する |
| SET GLOBALしたのに変わらない | 既存のセッションには反映されない | 接続し直す、またはSET SESSIONも実行する |
| 再起動したら元に戻った | SET GLOBALは再起動で消える | SET PERSISTか設定ファイルに書く |
| 並び順が実行のたびに違う | ORDER BYを書いていない | 関数の中にORDER BYを書く |
| NULLの行が出てこない | NULLは無視される仕様 | IFNULL(列, '文字') で置き換えてから連結 |
| 複数列の連結で行が減る | 式の1つがNULLだと行ごと外れる | 各列をIFNULLで包む、またはCONCAT_WSを使う |
| 分割したら要素数が合わない | 値に区切り文字が含まれている | 値に現れない区切り文字にするか、JSON_ARRAYAGGを使う |
| 関数の中にLIMITを書いたら構文エラー | GROUP_CONCATはLIMITを受け付けない | SUBSTRING_INDEXで先頭N件を取り出す |
よくある質問
Q. GROUP_CONCATの上限は何文字ですか?
A. 既定では1024バイトです。半角英数字なら1024文字、utf8mb4の日本語(1文字3バイト)なら今回の確認では341文字で止まりました。group_concat_max_len を変更すれば広げられ、実際に返せる長さは max_allowed_packet までです。
Q. 結果が切れているかどうかは、どうすればわかりますか?
A. 実行直後に SHOW WARNINGS; を実行し、コード1260の「Row N was cut by GROUP_CONCAT()」が出ていれば切れています。LENGTH(結果) が group_concat_max_len の値とちょうど同じ(または直前)になっているかを見るのも手がかりになります。
Q. SEPARATORとORDER BYはどちらを先に書きますか? A. ORDER BYが先、SEPARATORが最後です。DISTINCTも使う場合は「DISTINCT 式 ORDER BY ... SEPARATOR ...」の順になります。
Q. カンマ区切りの結果をWHERE句のINに使えますか?
A. 文字列としてそのままINに渡しても、1つの文字列として比較されるだけです。連結した結果で検索したいときは FIND_IN_SET(値, 連結結果) を使うか、連結せずにサブクエリやJOINで書く形にします。
まとめ
- GROUP_CONCATは、グループの値を1つの文字列に連結する集約関数。既定の区切りはカンマ
- 書く順番は「DISTINCT → 式 → ORDER BY → SEPARATOR」。並び順を保証したいときはORDER BYを必ず書く
- NULLは無視され、全部NULLまたは0件なら結果はNULL。含めたいときはIFNULLで置き換える
- 既定の上限は1024バイト。SELECTでは切れた結果とWarning 1260、INSERT ... SELECTではERROR 1260になる
- 上限の変更は、SET SESSION(その接続だけ)・SET GLOBAL(新しい接続から)・SET PERSISTやmy.cnf(再起動後も残る)から選ぶ
- 値に区切り文字が含まれる、配列として受け取りたいときはJSON_ARRAYAGG。NULLは
nullとして入り、並び順は指定できない - 他のDBでは、PostgreSQLとSQL ServerがSTRING_AGG、Oracle DatabaseがLISTAGG
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MySQL GROUP_CONCATの使い方|区切り文字・並び順・文字数上限」
株式会社RJCでは、MySQLの集約関数やSQLの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。