結論からいうと、SQLの文字列結合は「同じ1行の複数カラムをつなぐ」のか、「複数行の値を1行へ集約する」のかを最初に分けてください。同じ行ならCONCAT、CONCAT_WS、||など、複数行ならSTRING_AGG、GROUP_CONCAT、LISTAGGを使います。関数名、NULLの扱い、並び順、長さ上限はDBMSごとに異なります。
姓・名・ミドルネームを表示用の氏名にするだけなら、区切り文字とNULL処理を一緒に書ける関数が便利です。ただし、NULLを飛ばす関数でも空文字や空白だけの値まで自動で除外するとは限りません。先に「NULLは飛ばす」「空欄も飛ばす」「一つでも欠けたら結果をNULLにする」のどれが正しいか決めるのが、余分なスペースや欠損を防ぐ近道です。
最初に選ぶ方法
| DBMS | 同じ行のカラム結合 | 複数行の集約 | NULLで特に注意する点 |
|---|---|---|---|
| SQL Server | CONCAT_WSまたはCONCAT | STRING_AGG | CONCATはNULLを空文字扱い。+はNULLを返す |
| PostgreSQL | concat_wsまたはconcat | string_agg | 関数はNULLを飛ばすが、||はNULLを伝播 |
| MySQL | CONCAT_WS | GROUP_CONCAT | CONCATは引数にNULLがあると結果もNULL |
| Oracle Database | || | LISTAGG | 19cのCONCATは2引数、AI Database 26は2個以上。空文字はNULL扱い |
記事中では次の列を持つcustomersテーブルを例にします。customer_idは一意、departmentは部署、first_name、middle_name、last_nameは名前です。実際の列名と型に読み替え、まず更新を伴わないSELECTで確認してください。
カラム結合と行集約は別の処理
同じ行のカラムをつなぐ
たとえば1人分のfirst_nameとlast_nameをつないでfull_nameを作る処理です。元の行数は変わりません。1人1行のまま、表示用の列が一つ増えます。
SELECT customer_id,
CONCAT(first_name, ' ', last_name) AS full_name
FROM customers;
複数行を一つの文字列へ集約する
部署ごとのメンバー名をカンマ区切りで1行にする処理です。GROUP BYと集約関数を使うため、結果の行数が変わります。この二つを同じ「結合」として扱うと、必要のないユーザー定義関数や古いXML方式を選びやすくなります。
SELECT department,
STRING_AGG(last_name, ', ') AS members
FROM customers
GROUP BY department;
上のSTRING_AGGはSQL Serverの例です。MySQLには同名関数がないため、そのまま移植できません。以降のDBMS別例を使ってください。
SQL Serverで複数カラムを結合する
SQL Server 2017以降で区切り文字を入れるならCONCAT_WSが分かりやすい方法です。区切り文字以外のNULL引数は無視されます。空文字は残るので、空文字も欠損扱いにする列へNULLIFを適用します。
SELECT customer_id,
CONCAT_WS(
' ',
NULLIF(first_name, ''),
NULLIF(middle_name, ''),
NULLIF(last_name, '')
) AS full_name
FROM customers;
前後の空白も欠損とみなすなら、列のデータ品質を確認したうえでNULLIF(TRIM(first_name), '')のようにします。元データを変更する処理ではありませんが、意図的に空白を含む識別子には使わないでください。
CONCATは2個以上の引数を文字列へ変換して連結し、NULLを空文字として扱います。一方、first_name + ' ' + last_nameのような+演算子は、現行SQL ServerではNULLが一つあると結果全体がNULLです。古いCONCAT_NULL_YIELDS_NULL OFFへ依存する設計は使わないでください。SQL Server 2017以降ではOFF設定は非推奨で、常にONです。
PostgreSQLで複数カラムを結合する
PostgreSQLのconcat_wsも、区切り文字以外のNULL引数を無視します。空白だけの値も除外したい場合はbtrimとNULLIFを組み合わせます。
SELECT customer_id,
concat_ws(
' ',
NULLIF(btrim(first_name), ''),
NULLIF(btrim(middle_name), ''),
NULLIF(btrim(last_name), '')
) AS full_name
FROM customers;
first_name || ' ' || last_nameも使えますが、いずれかがNULLなら結果はNULLです。この挙動が要件なら簡潔な選択肢です。欠損項目だけを飛ばしたいのに||を使う場合は、coalesceだけで空文字へ変えると余分な区切りが残るため、完成形をNULL・空文字・空白だけの値で確認します。
MySQLで複数カラムを結合する
MySQLのCONCATは、引数の一つでもNULLなら結果全体をNULLにします。欠損項目を飛ばしながら区切りたい用途ではCONCAT_WSを使います。
SELECT customer_id,
CONCAT_WS(
' ',
NULLIF(TRIM(first_name), ''),
NULLIF(TRIM(middle_name), ''),
NULLIF(TRIM(last_name), '')
) AS full_name
FROM customers;
CONCAT_WSの第1引数である区切り文字がNULLなら結果はNULLです。それ以降のNULLは飛ばしますが、空文字は飛ばしません。また、MySQL 8.4の複数行集約はGROUP_CONCATであり、STRING_AGGではありません。
Oracle Databaseで複数カラムを結合する
Oracle Database 19cのCONCATは2引数です。一方、Oracle AI Database 26のCONCATは2個以上の引数を受け取り、公式資料には3引数の例があります。したがって「OracleのCONCATは常に2引数」とは一般化できません。19cと26の両方で使う例や対象バージョンが未確定のSQLでは||を推奨し、CONCATを使う場合は対象バージョンの仕様を確認してください。19cで3項目以上をつなぐならCONCAT(CONCAT(first_name, ' '), last_name)のようにネストできますが、通常は||の方が読みやすくなります。NULLを空相当として表示したい場合はNVLを明示します。
SELECT customer_id,
TRIM(
NVL(first_name, '') || ' ' ||
NVL(last_name, '')
) AS full_name
FROM customers;
Oracleでは長さ0の文字列がNULLとして扱われます。この点は、空文字とNULLを区別できるPostgreSQLやSQL Serverなどと異なります。単純なサンプルで同じ見た目になっても、空文字の保存や比較まで同じとは限りません。
NULL・空文字・型変換を先に決める
| 要件 | 基本方針 | 確認する例 |
|---|---|---|
| 欠損項目だけ飛ばす | CONCAT_WS系。空文字も飛ばすならNULLIF | ミドルネームだけNULL |
| 一つでも欠けたら結果をNULL | NULLが伝播する||や+、またはCASE | 姓だけNULL |
| 固定幅の区切りを維持 | COALESCE/NVLで空相当にする | CSV仕様で空フィールドが必要 |
| 日付・数値を結合 | 表示書式を明示してから連結 | 小数点、桁区切り、日付言語 |
| 長い本文を結合 | 戻り型と上限を確認 | 最大長、マルチバイト文字 |
日付や数値を文字列へ暗黙変換すると、セッションの言語、日付形式、小数点、型優先順位に結果が左右されます。表示形式が契約になっているCSV、帳票、APIでは、SQL ServerのCONVERT、PostgreSQLのto_char、MySQLのDATE_FORMAT、OracleのTO_CHARなど、対象DBMSの明示変換を使います。別DBMSへそのまま移せる共通書式ではないため、変換部分を分離してテストしてください。
複数行を一つの文字列へまとめる
行集約では、関数名だけでなく文字列内部の順序を必ず指定します。外側のORDER BY departmentは結果行の並びを決めるだけで、メンバー名の順序を決めません。同姓がある場合にも順序が揺れないよう、最後に一意なcustomer_idを加えます。
SQL Server
SELECT department,
STRING_AGG(last_name, ', ')
WITHIN GROUP (ORDER BY last_name, customer_id) AS members
FROM customers
GROUP BY department;
STRING_AGGはNULLの式を無視し、その位置の区切り文字も追加しません。プレースホルダーが必要なら、集約する式をISNULL(last_name, '(未登録)')のようにします。入力型によって戻り型と長さが変わるため、長文ではnvarchar(max)など必要な型への明示変換を検討します。
PostgreSQL
SELECT department,
string_agg(last_name, ', ' ORDER BY last_name, customer_id) AS members
FROM customers
GROUP BY department;
PostgreSQLのstring_aggも非NULL入力を結合します。順序は集約呼び出しの中に書きます。並べ替え済みサブクエリへ依存すると、外側の結合などで入力順が変わる場合があるため、集約内のORDER BYが明確です。
MySQL
SELECT department,
GROUP_CONCAT(
last_name
ORDER BY last_name, customer_id
SEPARATOR ', '
) AS members
FROM customers
GROUP BY department;
GROUP_CONCATの結果長はgroup_concat_max_lenで制限されます。長い一覧が途中で切れて見えるときに、安易に上限だけを大きくするのではなく、一覧を1セルへ詰め込む設計が必要か、アプリ側へ行のまま返せないかも確認してください。
Oracle Database
SELECT department,
LISTAGG(
last_name,
', ' ON OVERFLOW TRUNCATE '...' WITH COUNT
) WITHIN GROUP (ORDER BY last_name, customer_id) AS members
FROM customers
GROUP BY department;
ON OVERFLOW TRUNCATEを付けると、上限超過時にエラーではなく省略表示へ切り替えられます。ただし、これはデータが全部入ったことを意味しません。完全な一覧が必要な処理では切り捨てを使わず、結果型と取得方法を設計し直してください。
うまくいかないときの診断表
| 症状 | 主な原因 | 確認方法 |
|---|---|---|
| 結果全体がNULL | NULL伝播する演算子、MySQLのCONCAT | 各入力列を同じSELECTで表示 |
| スペースやカンマが二重 | 空文字をNULLと誤認 | TRIM後の長さとNULLIFを確認 |
| 人名の順序が毎回違う | 集約内のORDER BYなし、同順位 | 主キーまで含む順序を指定 |
| 日付表示が環境で変わる | 暗黙変換、言語設定 | 明示書式で変換 |
| 一覧が途中で切れる | 戻り型または集約上限 | 入力型、設定値、実際の文字数を確認 |
| MySQLでSTRING_AGGがエラー | 関数非対応 | GROUP_CONCATへ書き換え |
| Oracle CONCATの引数数でエラー | 19cの2引数仕様へ3引数以上を指定。AI Database 26は2個以上を受理 | 対象バージョンを確認し、版共通なら||、19cならCONCATをネスト |
安全に導入する手順と戻し方
- DBMS名とメジャーバージョンを確認します。クラウド互換DBでも、元製品と同じ関数がすべて使えるとは限りません。
- 列結合か行集約かを決め、NULL、空文字、空白だけの値、重複、並び順の要件を書き出します。
- 更新を伴わないSELECTで、通常値、NULL、空文字、日本語、最大長付近の値を確認します。
- 行集約では一意な順序を指定し、想定件数と集約前後の件数を比較します。
- アプリや帳票へ反映する前に、ステージングで型、文字コード、最大長、CSVエスケープを確認します。
ここまでのSELECTはデータを変更しないため、SQL上のロールバックは不要です。ビュー、計算列、関数へ組み込む場合は、変更前DDLと依存先を保存し、元DDLへ戻せる状態で実施します。単純な氏名結合のためだけにユーザー定義関数を追加すると、権限、配布、依存関係が増えます。複数箇所で共有する複雑な業務規則でない限り、まず組み込み関数で十分です。
よくある質問
CONCATとCONCAT_WSはどちらを使えばよいですか?
区切り文字を一定に入れ、NULL項目を飛ばしたいならCONCAT_WSが適しています。ただし空文字は飛ばさないため、必要ならNULLIFで正規化します。固定した文字を自由に挟むだけ、またはSQL Serverで数値もまとめて変換したいならCONCATが簡潔です。
COALESCEで全部空文字にすればよいですか?
必ずしもそうではありません。COALESCE(middle_name, '')の前後へ固定スペースを置くと、ミドルネームがない人だけスペースが二重になります。欠損項目と区切りをまとめて飛ばすならCONCAT_WS系が扱いやすく、空欄を列位置として残すCSVならCOALESCEが合います。
重複した値を一度だけ集約できますか?
DBMSとバージョンによりDISTINCTを集約関数内で使えますが、構文は共通ではありません。重複の定義も、大文字小文字、末尾空白、照合順序で変わります。まず重複を除くサブクエリを作り、残す代表行と順序が明確か確認してから集約してください。
結合結果を検索条件やJOINキーに使ってもよいですか?
表示用には便利ですが、通常は元の列を個別に比較する方が索引を使いやすく、区切りやNULLの曖昧さも避けられます。永続的な識別子が必要なら、人名や住所の連結ではなく主キーを使ってください。計算列へ索引を付ける設計はDBMS固有なので、実行計画と更新コストを確認します。
まとめ
同じ行の複数カラムなら、SQL Server・PostgreSQL・MySQLではNULLと空文字を整えたCONCAT_WS、Oracleでは||が実用的です。複数行ならSQL ServerとPostgreSQLはSTRING_AGG、MySQLはGROUP_CONCAT、OracleはLISTAGGを使います。関数名だけを置換せず、NULL、順序、戻り型、上限を一緒に移植してください。
公式資料
- Microsoft Learn: CONCAT (Transact-SQL)
- Microsoft Learn: STRING_AGG (Transact-SQL)
- PostgreSQL: String Functions and Operators
- PostgreSQL: Aggregate Functions
- MySQL 8.4: String Functions and Operators
- MySQL 8.4: Aggregate Functions
- Oracle Database 19c: CONCAT
- Oracle AI Database 26: CONCAT
- Oracle Database: LISTAGG

コメント