string_agg とは、複数行の文字列を区切り文字でつないで1つの文字列にまとめる PostgreSQL の集約関数です。 string_agg(列, 区切り文字) と書き、GROUP BY と組み合わせれば「チームごとのメンバー名をカンマ区切りで1行に」といった結果が1つの SQL で得られます。

この記事では、サンプルテーブルの CREATE TABLE と INSERT から載せ、PostgreSQL 16.15 で実際に実行した結果をそのまま並べます。並び順の指定(ORDER BY)、重複の除去(DISTINCT)、条件付き集約(FILTER)、ウィンドウ関数、NULL の扱い、数値列のキャスト、そして DISTINCT と別の列での ORDER BY を併用したときのエラー文と回避策まで、コピーして試せる形で整理しました。

string_agg の早見表

やりたいこと 書き方 結果の例
カンマ区切りでつなぐ string_agg(name, ',') 佐藤,鈴木,高橋
区切りを変える string_agg(name, ' / ') 佐藤 / 鈴木 / 高橋
並び順を決める string_agg(name, ',' ORDER BY joined) 伊藤,鈴木,佐藤
降順・NULL を後ろに string_agg(name, ',' ORDER BY score DESC NULLS LAST) 高橋,伊藤,佐藤
重複を除く string_agg(DISTINCT skill, ',') Java,Python,SQL
グループごとにまとめる SELECT team, string_agg(name, ',') ... GROUP BY team A: 佐藤,鈴木,高橋
条件に合う行だけ string_agg(name, ',') FILTER (WHERE score >= 80) 佐藤,高橋
NULL を文字に置き換える string_agg(COALESCE(name, '(未登録)'), ',') 田中,(未登録),伊藤
数値・日付をつなぐ string_agg(score::text, ',') 65,70,75
行を残したまま横に並べる string_agg(name, ',') OVER (PARTITION BY team) 各行にチーム全員の名前
0 行のとき空文字にする COALESCE(string_agg(name, ','), '') (空文字)
配列で受け取る array_agg(name ORDER BY id) {佐藤,鈴木,高橋}

ORDER BY は関数のかっこの中、区切り文字のあとに書くのが string_agg の特徴です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQL string_aggの使い方|並び順とDISTINCT」

検証した環境とサンプルテーブル

項目 内容
PostgreSQL 16.15(Ubuntu 24.04 のパッケージ)
クライアント psql
仕様の確認 PostgreSQL 公式ドキュメント「Aggregate Functions」(開いた時点の表示は PostgreSQL 18 系)
他データベース MySQL 8.0.46 の GROUP_CONCAT のみ実行。Oracle と SQL Server は未実行

次の SQL をそのまま流すと、この記事と同じテーブルができます。名前が NULL の行(id 5)、スキルが NULL の行(id 7)、点数が NULL の行(id 5)をわざと混ぜています。

CREATE TABLE members (
  id     integer PRIMARY KEY,
  team   text    NOT NULL,
  name   text,
  skill  text,
  score  integer,
  joined date    NOT NULL
);

INSERT INTO members (id, team, name, skill, score, joined) VALUES
  (1, 'A', '佐藤', 'SQL',    80,   '2024-04-01'),
  (2, 'A', '鈴木', 'Java',   65,   '2023-10-01'),
  (3, 'A', '高橋', 'SQL',    90,   '2025-04-01'),
  (4, 'B', '田中', 'Python', 70,   '2024-10-01'),
  (5, 'B', NULL,   'Java',   NULL, '2025-01-15'),
  (6, 'B', '伊藤', 'Python', 85,   '2022-04-01'),
  (7, 'C', '渡辺', NULL,     75,   '2025-07-01');

基本:複数行を1つの文字列にまとめる

SELECT string_agg(name, ',') AS names FROM members;
             names
-------------------------------
 佐藤,鈴木,高橋,田中,伊藤,渡辺

7行のうち、名前が NULL の id 5 は結果に含まれず、6人分がつながりました。構文は次の形です。

string_agg(つなぐ値, 区切り文字 [ORDER BY 並び替えの式])
引数 型 説明
つなぐ値 text(または bytea) 文字列型の列や式。数値や日付は text に変換して渡す
区切り文字 text 2つ目以降の値の前に入る。最後には付かない
戻り値 text 対象の行が0件、または全部 NULL のときは NULL

引数は2つとも必須です。区切り文字を省くと次のエラーになります。

SELECT string_agg(name) FROM members;
ERROR:  function string_agg(text) does not exist
LINE 1: SELECT string_agg(name) FROM members;
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

区切りなしでつなぎたいときは、空文字 '' を渡します。

区切り文字を変える(スラッシュ・区切りなし・改行)

SELECT string_agg(name, ' / ')  AS slash,
       string_agg(name, '')     AS no_sep,
       string_agg(name, E'\n')  AS newline
FROM members
WHERE team = 'A';
       slash        |    no_sep    | newline
--------------------+--------------+---------
 佐藤 / 鈴木 / 高橋 | 佐藤鈴木高橋 | 佐藤   +
                    |              | 鈴木   +
                    |              | 高橋
区切り 書き方 用途の例
カンマ ',' CSV 風の一覧
カンマ + 空白 ', ' 画面に表示する文章
スラッシュ ' / ' 見出しやラベル
区切りなし '' 文字をそのまま連結
改行 E'\n' 複数行のテキストにまとめる
タブ E'\t' TSV 風の出力

改行やタブは、先頭に E を付けたエスケープ文字列で書きます。psql の表示では、改行を含む値の行末に + が付きます。なお、区切り文字に NULL を渡した場合は区切りなしでつながりました(string_agg(name, NULL) の結果は 佐藤鈴木高橋)。公式ドキュメントの「区切り文字が NULL でなければ前に付ける」という説明どおりの動きです。

ORDER BY で並び順を指定する

ORDER BY を書かない場合、つながる順番は決まっていません。 公式ドキュメントにも「順序は既定では不定で、集約関数の呼び出しの中に ORDER BY を書くことで制御できる」とあります。順番に意味があるときは必ず指定します。

SELECT string_agg(name, ',' ORDER BY joined)                AS by_joined,
       string_agg(name, ',' ORDER BY score DESC NULLS LAST) AS by_score_desc
FROM members;
           by_joined           |         by_score_desc
-------------------------------+-------------------------------
 伊藤,鈴木,佐藤,田中,高橋,渡辺 | 高橋,伊藤,佐藤,渡辺,田中,鈴木

左は入社日の早い順、右は点数の高い順です。つなぐ列(name)と並び替えの列(joined や score)は別でかまいません。

並び替え 書き方
昇順 string_agg(name, ',' ORDER BY name)
降順 string_agg(name, ',' ORDER BY score DESC)
NULL を最後に string_agg(name, ',' ORDER BY score DESC NULLS LAST)
複数キー string_agg(name, ',' ORDER BY team, joined)

ORDER BY を書く位置

ORDER BY をかっこの外に書くと、構文エラーになります。

SELECT string_agg(name, ',') ORDER BY id FROM members;
ERROR:  syntax error at or near "FROM"
LINE 1: SELECT string_agg(name, ',') ORDER BY id FROM members;
                                                 ^
書き方 結果
string_agg(name, ',') ORDER BY id FROM members 構文エラー
string_agg(name, ',' ORDER BY id) 連結する順番を id 順にできる
SELECT ... FROM members GROUP BY team ORDER BY team 文末の ORDER BY は「結果の行」の順番で、連結の順番には関係しない

連結の順番は関数の中の ORDER BY、結果の行の順番は文末の ORDER BY、と役割が分かれています。

並び替えたサブクエリを FROM に置く書き方も、公式ドキュメントに「たいていはうまく動く」と書かれており、手元でも並びは保たれました。ただ、関数の中に ORDER BY を書く形のほうが意図が1行で伝わるので、この記事ではそちらを基本にしています。

GROUP BY でグループごとにまとめる

いちばんよく使う形です。チームごとに、人数とメンバー名を1行にまとめます。

SELECT team,
       count(*)                          AS cnt,
       string_agg(name, ',' ORDER BY id) AS names
FROM members
GROUP BY team
ORDER BY team;
 team | cnt |     names
------+-----+----------------
 A    |   3 | 佐藤,鈴木,高橋
 B    |   3 | 田中,伊藤
 C    |   1 | 渡辺

チーム B は count(*) が 3 ですが、名前は2人分です。名前が NULL の行が string_agg では読み飛ばされるためで、件数と連結結果の個数が合わないときは NULL を確認します(対応は後述します)。

GROUP BY を付け忘れると、次のエラーになります。

SELECT team, string_agg(name, ',') FROM members;
ERROR:  column "members.team" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT team, string_agg(name, ',') FROM members;
               ^

集約関数と一緒に SELECT に書いた列は、GROUP BY にも書く、という決まりです。

DISTINCT で重複を除く

スキルの一覧のように、同じ値が何度も出る列をまとめるときに使います。

SELECT string_agg(skill, ',' ORDER BY skill) AS all_skills,
       string_agg(DISTINCT skill, ',')       AS distinct_skills
FROM members;
           all_skills            | distinct_skills
---------------------------------+-----------------
 Java,Java,Python,Python,SQL,SQL | Java,Python,SQL

DISTINCT と ORDER BY を一緒に書くこともできます。並び替えの式が、つなぐ値そのものであれば問題なく動きます。

SELECT team,
       string_agg(DISTINCT skill, ',' ORDER BY skill DESC) AS skills
FROM members
GROUP BY team
ORDER BY team;
 team |   skills
------+-------------
 A    | SQL,Java
 B    | Python,Java
 C    |

チーム C はスキルが NULL の1行だけなので、結果は NULL(空欄)です。

DISTINCT と別の列での ORDER BY を併用するとエラーになる

「重複を除きつつ、入社日の早い順に並べたい」と考えて次のように書くと、エラーになります。

SELECT string_agg(DISTINCT skill, ',' ORDER BY joined) FROM members;
ERROR:  in an aggregate with DISTINCT, ORDER BY expressions must appear in argument list
LINE 1: SELECT string_agg(DISTINCT skill, ',' ORDER BY joined) FROM ...
                                                       ^

DISTINCT を付けた集約では、ORDER BY に書けるのは引数に出てくる式だけ、という決まりです。理由は、重複を1つにまとめたあとでは「どの行の joined で並べるか」が決まらないからです。たとえば SQL は 2024-04-01 の行と 2025-04-01 の行の両方にあり、どちらを使うかを SQL 側で指定する必要があります。

回避策1:サブクエリで先に重複をまとめる

「スキルごとの最も早い入社日」を先に求め、その日付で並べます。

SELECT string_agg(skill, ',' ORDER BY first_joined) AS skills_by_first_joined
FROM (
  SELECT skill, min(joined) AS first_joined
  FROM members
  WHERE skill IS NOT NULL
  GROUP BY skill
) AS s;
 skills_by_first_joined
------------------------
 Python,Java,SQL

GROUP BY で重複をなくし、並び替えに使う値を min や max で1つに決めるのがポイントです。「最も遅い日付で並べたい」なら max に変えるだけです。

回避策2:DISTINCT ON と array_agg を使う

PostgreSQL の DISTINCT ON で「スキルごとに先頭の1行」を選び、array_agg で並べてから文字列にする方法です。

SELECT array_to_string(array_agg(skill ORDER BY first_joined), ',') AS skills
FROM (
  SELECT DISTINCT ON (skill) skill, joined AS first_joined
  FROM members
  WHERE skill IS NOT NULL
  ORDER BY skill, joined
) AS s;
     skills
-----------------
 Python,Java,SQL
方法 向いている場面
string_agg(DISTINCT x, ',' ORDER BY x) つなぐ値そのもので並べればよいとき(そのまま書ける)
サブクエリ + GROUP BY + min / max 別の列で並べたいとき。どの行の値で並べるかを明示できる
DISTINCT ON + array_agg 重複のうち「先頭の1行」の他の列も一緒に使いたいとき

FILTER で条件に合う行だけを集約する

1つの SELECT の中で、条件の違う連結結果を横に並べたいときは FILTER が便利です。

SELECT team,
       string_agg(name, ',' ORDER BY id) FILTER (WHERE score >= 80) AS high,
       string_agg(name, ',' ORDER BY id) FILTER (WHERE score <  80) AS others
FROM members
GROUP BY team
ORDER BY team;
 team |   high    | others
------+-----------+--------
 A    | 佐藤,高橋 | 鈴木
 B    | 伊藤      | 田中
 C    |           | 渡辺
書き方 特徴
WHERE score >= 80 行そのものを絞る。条件に合う行が0件のグループは結果から消える
FILTER (WHERE score >= 80) 集約ごとに絞る。グループの行は残り、該当なしは NULL になる
string_agg(CASE WHEN score >= 80 THEN name END, ',') FILTER と同じ結果になる書き方。NULL が読み飛ばされる性質を使う

チーム C の high が空欄(NULL)なのは、80点以上の人がいないためです。空文字にしたいときは COALESCE(..., '') で包みます。

NULL の扱いと COALESCE

string_agg は NULL の値を読み飛ばします。 公式ドキュメントの説明も「NULL でない入力値を連結する」です。挙動を3つの場面で確かめました。

値に NULL が混ざっているとき

SELECT team,
       string_agg(name, ',' ORDER BY id)                       AS skip_null,
       string_agg(COALESCE(name, '(未登録)'), ',' ORDER BY id) AS with_coalesce
FROM members
GROUP BY team
ORDER BY team;
 team |   skip_null    |   with_coalesce
------+----------------+--------------------
 A    | 佐藤,鈴木,高橋 | 佐藤,鈴木,高橋
 B    | 田中,伊藤      | 田中,(未登録),伊藤
 C    | 渡辺           | 渡辺

読み飛ばされるので、田中,,伊藤 のように区切りが2つ続くことはありません。NULL の行も位置を残したいときは、COALESCE で文字に置き換えてから渡します。

対象が0行、または全部 NULL のとき

SELECT string_agg(name, ',')               AS no_rows,
       string_agg(name, ',') IS NULL       AS is_null,
       COALESCE(string_agg(name, ','), '') AS empty
FROM members
WHERE team = 'Z';
 no_rows | is_null | empty
---------+---------+-------
         | t       |

対象が0行のとき、string_agg は空文字ではなく NULL を返します。 名前が NULL の行だけを集めた場合も、同じく NULL になりました。アプリケーション側で文字列として扱うなら、COALESCE(string_agg(...), '') と外側を包んでおくと安心です。

NULL がある場所 結果 対応
つなぐ値の一部 その行だけ読み飛ばす 位置を残すなら値を COALESCE で置き換える
つなぐ値の全部、または0行 結果が NULL 関数全体を COALESCE で包む
区切り文字 区切りなしで連結 区切り文字は定数で書く

複数の列を組み合わせてつなぐときの NULL

「名前(点数)」の形でつなぐとき、連結の方法で NULL の行の扱いが変わります。

SELECT string_agg(name || '(' || score || ')', ', ' ORDER BY score DESC)      AS concat_op,
       string_agg(format('%s(%s)', name, score), ', ' ORDER BY score DESC)   AS with_format
FROM members;
                         concat_op                          |                          with_format
------------------------------------------------------------+----------------------------------------------------------------
 高橋(90), 伊藤(85), 佐藤(80), 渡辺(75), 田中(70), 鈴木(65) | (), 高橋(90), 伊藤(85), 佐藤(80), 渡辺(75), 田中(70), 鈴木(65)
連結の方法 NULL を含む行 結果
|| 演算子 式全体が NULL になる その行は読み飛ばされる
format('%s(%s)', ...) NULL を空文字として扱う () という要素が残る
concat(name, '(', score, ')') NULL を空文字として扱う format と同じく () が残る

どちらが正しいということはなく、NULL の行を出したいかどうかで選びます。 先頭に () が来ているのは、ORDER BY score DESC の既定では NULL が先頭に並ぶためで、NULLS LAST を付けると末尾に移ります。

数値や日付の列をつなぐ(キャスト)

string_agg の第1引数は文字列型です。integer の列をそのまま渡すとエラーになります。

SELECT string_agg(score, ',') FROM members;
ERROR:  function string_agg(integer, unknown) does not exist
LINE 1: SELECT string_agg(score, ',') FROM members;
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

::text を付けて文字列に変換すると通ります。日付は to_char で書式を決めると、表示が安定します。

SELECT string_agg(score::text, ',' ORDER BY score)                 AS scores,
       string_agg(to_char(joined, 'YYYY/MM'), ',' ORDER BY joined) AS months
FROM members;
      scores       |                         months
-------------------+---------------------------------------------------------
 65,70,75,80,85,90 | 2022/04,2023/10,2024/04,2024/10,2025/01,2025/04,2025/07

ORDER BY には元の列(score や joined)を使っている点がポイントです。文字列に変換した値で並べると「100」が「65」より前に来るような辞書順になるため、並び替えは元の型の列で行います。

列の型 渡し方
integer / bigint / numeric score::text
date / timestamp to_char(joined, 'YYYY/MM/DD') または joined::text
boolean flag::text(true / false になる)
varchar / char そのまま渡せる

ウィンドウ関数として使う(行を残したまま連結)

GROUP BY は行をまとめますが、OVER を付けると、元の行を残したまま各行に連結結果を付けられます。

SELECT id, team, name,
       string_agg(name, ',') OVER (PARTITION BY team)             AS team_all,
       string_agg(name, ',') OVER (PARTITION BY team ORDER BY id) AS running
FROM members
ORDER BY id;
 id | team | name |    team_all    |    running
----+------+------+----------------+----------------
  1 | A    | 佐藤 | 佐藤,鈴木,高橋 | 佐藤
  2 | A    | 鈴木 | 佐藤,鈴木,高橋 | 佐藤,鈴木
  3 | A    | 高橋 | 佐藤,鈴木,高橋 | 佐藤,鈴木,高橋
  4 | B    | 田中 | 田中,伊藤      | 田中
  5 | B    |      | 田中,伊藤      | 田中
  6 | B    | 伊藤 | 田中,伊藤      | 田中,伊藤
  7 | C    | 渡辺 | 渡辺           | 渡辺
OVER の書き方 結果
OVER (PARTITION BY team) 同じチームの全員分が、各行に入る
OVER (PARTITION BY team ORDER BY id) その行までの累積(1人目、1〜2人目、…)になる

ウィンドウ関数として使うときは、並び順を OVER の中の ORDER BY で指定します。 関数のかっこの中に ORDER BY を書くと、次のエラーになります。

SELECT string_agg(name, ',' ORDER BY id) OVER (PARTITION BY team) FROM members;
ERROR:  aggregate ORDER BY is not implemented for window functions
LINE 1: SELECT string_agg(name, ',' ORDER BY id) OVER (PARTITION BY ...
               ^

応用:グループごとに上位 N 件だけをつなぐ

string_agg 自体には件数を絞る指定がないため、row_number で順位を付けてから絞り込みます。

SELECT team, string_agg(name, ',' ORDER BY score DESC) AS top2
FROM (
  SELECT team, name, score,
         row_number() OVER (PARTITION BY team ORDER BY score DESC NULLS LAST) AS rn
  FROM members
) AS r
WHERE rn <= 2
GROUP BY team
ORDER BY team;
 team |   top2
------+-----------
 A    | 高橋,佐藤
 B    | 伊藤,田中
 C    | 渡辺

つないだ文字列を行に戻したいときは、string_to_array と unnest を組み合わせます。

SELECT unnest(string_to_array('SQL,Java,Python', ',')) AS skill;
 skill
--------
 SQL
 Java
 Python

長さについても確かめました。1 から 100000 までの数字をカンマでつないだ結果は 588,894 文字になり、途中で切られることなく全体が返りました。

SELECT length(string_agg(g::text, ',')) AS len FROM generate_series(1, 100000) AS g;
  len
--------
 588894

array_agg・array_to_string・json_agg との使い分け

「複数行を1つにまとめる」関数は string_agg のほかにもあります。同じデータで結果を並べました。

SELECT team,
       array_agg(name ORDER BY id)                            AS arr,
       array_to_string(array_agg(name ORDER BY id), ',')      AS arr_str,
       array_to_string(array_agg(name ORDER BY id), ',', '-') AS arr_str_null,
       json_agg(name ORDER BY id)                             AS js
FROM members
GROUP BY team
ORDER BY team;
 team |       arr        |    arr_str     |  arr_str_null  |            js
------+------------------+----------------+----------------+--------------------------
 A    | {佐藤,鈴木,高橋} | 佐藤,鈴木,高橋 | 佐藤,鈴木,高橋 | ["佐藤", "鈴木", "高橋"]
 B    | {田中,NULL,伊藤} | 田中,伊藤      | 田中,-,伊藤    | ["田中", null, "伊藤"]
 C    | {渡辺}           | 渡辺           | 渡辺           | ["渡辺"]
関数 戻り値の型 NULL の扱い 向いている用途
string_agg(x, ',') text 読み飛ばす 画面表示、CSV 風の文字列、ログ
array_agg(x) 配列(text[] など) NULL も要素として残す アプリケーションで配列として受け取る、添字で取り出す、ANY で検索する
array_to_string(array_agg(x), ',') text 読み飛ばす(第3引数で置き換え文字を指定できる) 配列を経由して文字列にしたいとき、NULL を記号で残したいとき
json_agg(x) / jsonb_agg(x) json / jsonb null として残す API のレスポンス、入れ子の構造を返すとき

選び方の目安は次のとおりです。

受け取る側 選ぶ関数
人が読む文字列として表示する string_agg
プログラムで1件ずつ処理する array_agg(区切り文字が値に含まれていても分解で困らない)
先頭の1件だけ取り出したい (array_agg(x ORDER BY ...))[1]
キーと値の組で返したい json_agg と json_build_object

配列なら添字で取り出せます。点数の最も高い人を取り出す例と、JSON オブジェクトの配列を作る例です。

SELECT (array_agg(name ORDER BY score DESC NULLS LAST))[1] AS top1,
       cardinality(array_agg(name))                        AS n
FROM members;

SELECT json_agg(json_build_object('name', name, 'score', score) ORDER BY id) AS objs
FROM members
WHERE team = 'A';
 top1 | n
------+---
 高橋 | 7

                                                objs
-----------------------------------------------------------------------------------------------------
 [{"name" : "佐藤", "score" : 80}, {"name" : "鈴木", "score" : 65}, {"name" : "高橋", "score" : 90}]

array_agg は NULL も数えるため、n は 7 になっています。値にカンマが含まれる可能性があるデータは、文字列でつなぐより配列や JSON で受け取るほうが、あとで分解するときに迷いません。

他のデータベースとの対応表

同じ目的の関数は、データベースごとに名前と書き方が違います。移行や読み替えのときの対応表です。

データベース 関数 基本の書き方 並び順の指定
PostgreSQL string_agg string_agg(name, ',' ORDER BY id) 関数のかっこの中
MySQL GROUP_CONCAT GROUP_CONCAT(name ORDER BY id SEPARATOR ',') 関数のかっこの中。区切りは SEPARATOR で指定
Oracle Database LISTAGG LISTAGG(name, ',') WITHIN GROUP (ORDER BY id) WITHIN GROUP 句
SQL Server STRING_AGG STRING_AGG(name, ',') WITHIN GROUP (ORDER BY id) WITHIN GROUP 句

このうち MySQL は、8.0.46 で同じデータを入れて実行しました。

-- MySQL 8.0.46
SELECT team,
       GROUP_CONCAT(name ORDER BY id SEPARATOR ',')               AS names,
       GROUP_CONCAT(DISTINCT skill ORDER BY skill SEPARATOR '/')  AS skills
FROM m
GROUP BY team;
+------+----------------+-------------+
| team | names          | skills      |
+------+----------------+-------------+
| A    | 佐藤,鈴木,高橋 | Java/SQL    |
| B    | 田中,伊藤      | Java/Python |
+------+----------------+-------------+
比べる点 PostgreSQL string_agg MySQL GROUP_CONCAT
区切り文字 第2引数(必須) SEPARATOR(省略するとカンマ)
NULL の値 読み飛ばす 読み飛ばす(上の結果で確認)
数値の列 ::text が必要 今回は未確認
長さの上限 約59万文字まで全体が返ることを確認 group_concat_max_len の既定値は 1024(SHOW VARIABLES で確認)

Oracle Database の LISTAGG と SQL Server の STRING_AGG は今回は未実行で、表の書き方は一般に知られた構文です。細かな挙動(DISTINCT の可否、長さの上限、NULL の扱い)は、使うバージョンのドキュメントで確かめてください。

つまずきやすいエラーと直し方

この記事で再現したエラーを、原因と直し方でまとめます。

エラー文(抜粋) 原因 直し方
function string_agg(text) does not exist 区切り文字を省いた 第2引数に ',' や '' を渡す
function string_agg(integer, unknown) does not exist 数値の列をそのまま渡した score::text と変換する
in an aggregate with DISTINCT, ORDER BY expressions must appear in argument list DISTINCT と別の列での ORDER BY を併用した サブクエリで先に重複をまとめ、min / max で並び替えの値を決める
aggregate ORDER BY is not implemented for window functions OVER を付けた集約の中に ORDER BY を書いた OVER の中に ORDER BY を書く
must appear in the GROUP BY clause or be used in an aggregate function GROUP BY を書いていない SELECT に書いた列を GROUP BY に入れる
syntax error at or near "FROM" ORDER BY を関数のかっこの外に書いた 区切り文字のあと、かっこの中に書く

エラーにはならないけれど結果が想定と違う、というときのチェックリストです。

症状 見るポイント
実行のたびに順番が変わる 関数の中に ORDER BY があるか
件数より要素が少ない つなぐ列に NULL がないか。残すなら COALESCE
結果が NULL になる 対象が0行、または全部 NULL。外側を COALESCE で包む
数字の並びが 100, 65, 70 になる 文字列に変換した値で並べている。元の列で ORDER BY する
同じ値が何度も出る DISTINCT を付ける

よくある質問

Q. string_agg の結果の順番は、何も指定しないとどうなりますか? A. 決まっていません。小さなテーブルでは登録順に見えることが多いですが、公式ドキュメントでは既定の順序は不定とされています。順番に意味があるときは string_agg(name, ',' ORDER BY id) のように関数の中で指定します。

Q. DISTINCT と ORDER BY は一緒に使えますか? A. 使えます。ただし ORDER BY に書けるのは、つなぐ値と同じ式だけです。string_agg(DISTINCT skill, ',' ORDER BY skill) は動き、別の列で並べようとするとエラーになります。別の列で並べたいときは、サブクエリで先に GROUP BY して min や max で並び替えの値を決めます。

Q. 該当する行がないとき、空文字にするにはどうすればよいですか? A. COALESCE(string_agg(name, ','), '') と書きます。string_agg は対象が0行のとき NULL を返すためです。

Q. string_agg と array_agg はどちらを使えばよいですか? A. 人が読む文字列が欲しいなら string_agg、プログラムで要素ごとに処理するなら array_agg が向いています。array_agg は NULL も要素として残る点が違います。

まとめ

  • string_agg(値, 区切り文字) は複数行を1つの文字列にまとめる集約関数。引数は2つとも必須
  • 並び順は関数のかっこの中に ORDER BY を書いて指定する。書かない場合の順番は決まっていない
  • DISTINCT を付けたときの ORDER BY は、つなぐ値と同じ式だけ。別の列で並べたいときはサブクエリで重複を先にまとめる
  • NULL の値は読み飛ばされ、対象が0行なら結果は NULL。値は COALESCE で置き換え、結果は COALESCE で包む
  • 数値や日付は ::text や to_char で文字列にしてから渡し、並び替えは元の列で行う
  • FILTER で条件別の連結を横に並べ、OVER で行を残したまま連結できる。OVER を使うときの並び順は OVER の中に書く
  • 配列で受け取るなら array_agg、JSON なら json_agg。値に区切り文字が含まれるデータは配列や JSON が扱いやすい

参考・出典

  • PostgreSQL 公式ドキュメント「9.21. Aggregate Functions」(string_agg・array_agg・json_agg の説明、入力順序と0行のときの戻り値の注記を確認)https://www.postgresql.org/docs/current/functions-aggregate.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PostgreSQL string_aggの使い方|並び順とDISTINCT」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?