SQLで複数の値を1つの文字列にまとめる「文字列結合」は、あらゆる場面で使われます。氏名の結合、住所の組み立て、CSVの生成、ログメッセージの構築——用途は幅広いですが、RDBMS ごとに使える関数・演算子が異なるのが厄介なポイントです。
本記事では MySQL・PostgreSQL・SQL Server・Oracle を横断して、文字列結合のあらゆる方法を体系的に解説します。
この記事で分かること
- CONCAT 関数の基本と RDBMS 別の引数仕様の違い
- || 演算子と + 演算子による文字列結合
- CONCAT_WS で区切り文字付きの結合を簡潔に書く方法
- NULLが混ざったときの挙動と対策
- 数値・日付と文字列を結合するときの型変換
- GROUP_CONCAT / STRING_AGG / LISTAGG で複数行を1行にまとめる方法
- 実務でよく使う結合パターン(氏名・住所・CSV生成・動的SQL)
RDBMS 別の結合方法一覧
文字列結合の書き方はRDBMSごとに異なります。まず全体像を把握しておきましょう。
| 結合方法 | MySQL | PostgreSQL | SQL Server | Oracle |
|---|---|---|---|---|
| CONCAT(a, b) | ○(引数は無制限) | ○(2引数のみ) | ○(2引数のみ) | ○(2引数のみ) |
| || 演算子 | ×(デフォルト) | ○ | × | ○ |
| + 演算子 | × | × | ○ | × |
| CONCAT_WS | ○ | ○ | ○(2017+) | × |
| 行集約 | GROUP_CONCAT | STRING_AGG | STRING_AGG(2017+) | LISTAGG |
CONCAT 関数の基本
最も広く使える文字列結合の方法がCONCAT()関数です。
-- 2つの列を結合(全RDBMSで共通)
SELECT CONCAT(last_name, first_name) AS full_name
FROM employees;
-- 結果: '山田太郎'
-- リテラル文字列の結合
SELECT CONCAT('Hello', ' World');
-- 結果: 'Hello World'
MySQL の CONCAT — 引数が無制限
MySQL の CONCAT は 引数をいくつでも渡せる唯一の RDBMS です。
-- 3つ以上の文字列を1回のCONCATで結合
SELECT CONCAT(last_name, ' ', first_name, ' (', department, ')') AS display_name
FROM employees;
-- 結果: '山田 太郎 (営業部)'
-- 列とリテラルを組み合わせてメール本文を組み立てる
SELECT CONCAT('ID:', employee_id, ' / 氏名:', last_name, first_name)
FROM employees;
PostgreSQL・SQL Server・Oracle の CONCAT — 2引数のみ
これらのRDBMSでは CONCAT は2つの引数しか受け取れません。3つ以上を結合するには CONCAT をネストするか、||演算子(PostgreSQL/Oracle)や+演算子(SQL Server)を使います。
-- CONCATネスト(冗長だが動く) SELECT CONCAT(CONCAT(last_name, ' '), first_name) AS full_name FROM employees; -- || 演算子(推奨:こちらが簡潔) SELECT last_name || ' ' || first_name AS full_name FROM employees;
-- CONCATネスト SELECT CONCAT(CONCAT(last_name, ' '), first_name) AS full_name FROM employees; -- + 演算子(SQL Server 独自) SELECT last_name + ' ' + first_name AS full_name FROM employees;
-- CONCATネスト SELECT CONCAT(CONCAT(last_name, ' '), first_name) AS full_name FROM employees; -- || 演算子(Oracle では最も一般的な書き方) SELECT last_name || ' ' || first_name AS full_name FROM employees;
|| 演算子と + 演算子
CONCAT関数の代わりに演算子で文字列を結合する方法があります。コードが簡潔になるため、各RDBMSの開発者はこちらを好む傾向にあります。
| 演算子 | 対応RDBMS | NULLの扱い |
|---|---|---|
| || | PostgreSQL, Oracle | NULLが混ざると結果がNULLになる |
| + | SQL Server | NULLが混ざると結果がNULLになる |
-- PostgreSQL / Oracle
-- 列 + リテラル + 列
SELECT '商品名: ' || product_name || ' (' || price || '円)' AS label
FROM products;
-- 結果: '商品名: りんご (150円)'
-- SQL Server
-- 数値は明示的にキャストが必要
SELECT '商品名: ' + product_name + ' (' + CAST(price AS VARCHAR) + '円)' AS label
FROM products;
MySQL の || は論理演算子:MySQL ではデフォルトで || はOR演算子として扱われます。SET sql_mode = 'PIPES_AS_CONCAT' を設定すると文字列結合として使えますが、移植性に問題があるため MySQL では CONCAT 関数を使うのが安全です。
NULL が混ざったときの挙動と対策
最も重要なポイント:文字列結合でNULLが1つでも混ざると、演算子(|| や +)の結果は NULL になります。CONCAT関数はRDBMSによって挙動が異なります。
| 結合方法 | NULLの扱い |
|---|---|
| MySQL CONCAT(a, NULL, b) | 結果がNULLになる |
| PostgreSQL CONCAT(a, NULL, b) | NULLを無視して結合する |
| SQL Server CONCAT(a, NULL, b) | NULLを空文字列として結合する |
| Oracle CONCAT(a, NULL) | NULLを空文字列として結合する |
| || 演算子 (PG/Oracle) | 結果がNULLになる(PostgreSQL)/ 空文字列扱い(Oracle) |
| + 演算子 (SQL Server) | 結果がNULLになる |
-- 例: middle_name が NULL の場合
-- MySQL: CONCAT は全体が NULL になる
SELECT CONCAT(first_name, ' ', middle_name, ' ', last_name)
FROM employees;
-- middle_name が NULL → 結果: NULL(全体が消える!)
-- 対策1: COALESCE で NULL を空文字に置換
SELECT CONCAT(first_name, ' ', COALESCE(middle_name, ''), ' ', last_name)
FROM employees;
-- middle_name が NULL → 結果: '太郎 山田'(スペースが2つ入る)
-- 対策2: CONCAT_WS なら NULL を自動スキップ(MySQL推奨)
SELECT CONCAT_WS(' ', first_name, middle_name, last_name)
FROM employees;
-- middle_name が NULL → 結果: '太郎 山田'(区切り文字が1つに)
-- || 演算子は NULL が混ざると全体が NULL SELECT first_name || ' ' || middle_name || ' ' || last_name FROM employees; -- middle_name が NULL → 結果: NULL -- 対策: COALESCE で NULL を空文字に SELECT first_name || ' ' || COALESCE(middle_name || ' ', '') || last_name FROM employees; -- middle_name が NULL → 結果: '太郎 山田'
-- + 演算子は NULL が混ざると全体が NULL SELECT first_name + ' ' + middle_name + ' ' + last_name FROM employees; -- middle_name が NULL → 結果: NULL -- 対策1: COALESCE SELECT first_name + ' ' + COALESCE(middle_name + ' ', '') + last_name FROM employees; -- 対策2: CONCAT 関数を使う(NULL は空文字扱い) SELECT CONCAT(first_name, ' ', middle_name, ' ', last_name) FROM employees; -- middle_name が NULL → 結果: '太郎 山田'(SQL Server のCONCATはNULLを空文字にする)
CONCAT_WS — 区切り文字付き結合
CONCAT_WS(With Separator)は、第1引数に区切り文字を指定し、残りの引数を結合します。NULLの引数を自動的にスキップするのが最大のメリットです。
-- MySQL / PostgreSQL / SQL Server(2017+)
-- 第1引数 = 区切り文字、第2引数以降 = 結合対象
SELECT CONCAT_WS(', ', last_name, first_name, department) AS info
FROM employees;
-- 結果: '山田, 太郎, 営業部'
-- NULLは自動でスキップされる
SELECT CONCAT_WS(' - ', 'A', NULL, 'B', NULL, 'C');
-- 結果: 'A - B - C'(NULLの部分は区切り文字ごと消える)
-- 住所を1行にまとめる(NULLの項目を自動スキップ)
SELECT CONCAT_WS(' ',
prefecture, -- 都道府県
city, -- 市区町村
street, -- 番地
building -- 建物名(NULL可)
) AS full_address
FROM addresses;
-- building が NULL → '東京都 千代田区 丸の内1-1'(建物名はスキップ)
-- CSVの1行を生成
SELECT CONCAT_WS(',', employee_id, last_name, first_name, department)
FROM employees;
-- 結果: '101,山田,太郎,営業部'
Oracle は CONCAT_WS 未対応:Oracle で区切り文字付き結合をするには、|| と CASE WHEN を組み合わせるか、リライトが必要です。
-- Oracle: || と NVL2 で区切り文字付き結合を模倣
SELECT
last_name
|| NVL2(first_name, ' ' || first_name, '')
|| NVL2(department, ' (' || department || ')', '')
AS display_name
FROM employees;
-- first_name が NULL → '山田'
-- first_name が '太郎' → '山田 太郎 (営業部)'
数値・日付と文字列を結合する
文字列と数値や日付を結合する場合、暗黙的な型変換に頼るかどうかはRDBMSによって異なります。
| RDBMS | 暗黙変換 | 備考 |
|---|---|---|
| MySQL | ○ CONCAT内で自動変換 | 数値・日付いずれもそのまま渡せる |
| PostgreSQL | ○ CONCAT内で自動変換 | || 演算子は明示キャストが必要 |
| SQL Server | ○ CONCAT内で自動変換 | + 演算子は明示キャスト必須 |
| Oracle | ○ || で自動変換 | TO_CHAR で書式指定がベストプラクティス |
-- MySQL: CONCAT は暗黙変換
SELECT CONCAT('価格: ', price, '円') FROM products;
-- 結果: '価格: 1500円'
-- PostgreSQL: || は暗黙変換しないためキャストが必要
SELECT '価格: ' || price::TEXT || '円' FROM products; -- ::TEXT でキャスト
SELECT '価格: ' || CAST(price AS TEXT) || '円' FROM products; -- 標準SQL
-- SQL Server: + は暗黙変換しないためキャストが必要
SELECT '価格: ' + CAST(price AS VARCHAR) + '円' FROM products;
SELECT '価格: ' + FORMAT(price, '#,##0') + '円' FROM products; -- 桁区切り付き
-- Oracle: || は暗黙変換するがTO_CHARが安全
SELECT '価格: ' || TO_CHAR(price, 'FM999,999') || '円' FROM products;
-- MySQL
SELECT CONCAT('注文日: ', order_date) FROM orders;
SELECT CONCAT('注文日: ', DATE_FORMAT(order_date, '%Y年%m月%d日')) FROM orders;
-- PostgreSQL
SELECT '注文日: ' || TO_CHAR(order_date, 'YYYY年MM月DD日') FROM orders;
-- SQL Server
SELECT '注文日: ' + FORMAT(order_date, 'yyyy年MM月dd日') FROM orders;
SELECT '注文日: ' + CONVERT(VARCHAR, order_date, 111) FROM orders; -- 2024/03/15 形式
-- Oracle
SELECT '注文日: ' || TO_CHAR(order_date, 'YYYY"年"MM"月"DD"日"') FROM orders;
複数行を1つの文字列に集約する
1つの列の値を複数行にわたってカンマ区切りなどでまとめたい場合は、集約関数を使います。
| RDBMS | 関数 |
|---|---|
| MySQL | GROUP_CONCAT() |
| PostgreSQL | STRING_AGG() |
| SQL Server(2017+) | STRING_AGG() |
| Oracle | LISTAGG() |
-- 部署ごとにメンバー名をカンマ区切りでまとめる
SELECT
department,
GROUP_CONCAT(last_name ORDER BY last_name SEPARATOR ', ') AS members
FROM employees
GROUP BY department;
-- 結果: '営業部' | '伊藤, 山田, 鈴木'
-- 重複排除
SELECT
department,
GROUP_CONCAT(DISTINCT skill SEPARATOR ', ') AS skills
FROM employee_skills
GROUP BY department;
GROUP_CONCAT の最大長:デフォルトで 1024 バイトで切り捨てられます。長い結果が必要な場合は SET group_concat_max_len = 10000; で上限を変更してください。
-- 部署ごとにメンバー名をカンマ区切りでまとめる
SELECT
department,
STRING_AGG(last_name, ', ' ORDER BY last_name) AS members
FROM employees
GROUP BY department;
-- 結果: '営業部' | '伊藤, 山田, 鈴木'
-- 重複排除
SELECT
department,
STRING_AGG(DISTINCT skill, ', ' ORDER BY skill) AS skills
FROM employee_skills
GROUP BY department;
-- 部署ごとにメンバー名をカンマ区切りでまとめる
SELECT
department,
STRING_AGG(last_name, ', ') WITHIN GROUP (ORDER BY last_name) AS members
FROM employees
GROUP BY department;
-- 結果: '営業部' | '伊藤, 山田, 鈴木'
-- 部署ごとにメンバー名をカンマ区切りでまとめる
SELECT
department,
LISTAGG(last_name, ', ') WITHIN GROUP (ORDER BY last_name) AS members
FROM employees
GROUP BY department;
-- 結果: '営業部' | '伊藤, 山田, 鈴木'
-- DISTINCT(Oracle 19c+)
SELECT
department,
LISTAGG(DISTINCT skill, ', ') WITHIN GROUP (ORDER BY skill) AS skills
FROM employee_skills
GROUP BY department;
Oracle LISTAGG の4000バイト制限:結果が VARCHAR2 の上限(4000バイト)を超えると ORA-01489 エラーが発生します。ON OVERFLOW TRUNCATE(12c R2+)で回避できます。
-- ON OVERFLOW TRUNCATE で4000バイトを超えたら末尾を '...' にする
SELECT
department,
LISTAGG(skill, ', ' ON OVERFLOW TRUNCATE '...')
WITHIN GROUP (ORDER BY skill) AS skills
FROM employee_skills
GROUP BY department;
実務でよく使うパターン
パターン1:氏名の結合
-- MySQL
SELECT CONCAT_WS(' ', last_name, first_name) AS full_name FROM employees;
-- PostgreSQL
SELECT last_name || ' ' || first_name AS full_name FROM employees;
-- SQL Server
SELECT CONCAT(last_name, ' ', first_name) AS full_name FROM employees;
-- Oracle
SELECT last_name || ' ' || first_name AS full_name FROM employees;
パターン2:住所の組み立て
-- MySQL / PostgreSQL
SELECT CONCAT_WS(' ', prefecture, city, street, building) AS address
FROM addresses;
-- building が NULL → '東京都 千代田区 丸の内1-1'
-- Oracle(CONCAT_WS がないため手動対応)
SELECT prefecture || ' ' || city || ' ' || street
|| NVL2(building, ' ' || building, '')
AS address
FROM addresses;
パターン3:表示用ラベルの生成
-- MySQL SELECT CONCAT(product_name, ' (¥', FORMAT(price, 0), ')') AS label FROM products; -- 結果: 'りんご (¥150)' -- PostgreSQL SELECT product_name || ' (¥' || TO_CHAR(price, 'FM999,999') || ')' AS label FROM products; -- SQL Server SELECT product_name + ' (¥' + FORMAT(price, '#,##0') + ')' AS label FROM products;
パターン4:条件付き結合(CASE WHEN)
-- ステータスによって表示文言を変えてユーザー名に付与
SELECT
user_name || ' ' ||
CASE status
WHEN 'admin' THEN '[管理者]'
WHEN 'editor' THEN '[編集者]'
ELSE ''
END AS display_name
FROM users;
-- 結果: '山田太郎 [管理者]'
パターン5:タグ・カテゴリの一覧表示
-- MySQL: GROUP_CONCAT で記事ごとのタグ一覧
SELECT
a.title,
GROUP_CONCAT(t.tag_name ORDER BY t.tag_name SEPARATOR ', ') AS tags
FROM articles a
JOIN article_tags at ON a.id = at.article_id
JOIN tags t ON at.tag_id = t.id
GROUP BY a.id, a.title;
-- 結果: 'SQL入門' | 'SQL, データベース, 初心者向け'
-- PostgreSQL: STRING_AGG で同様のことを行う
SELECT
a.title,
STRING_AGG(t.tag_name, ', ' ORDER BY t.tag_name) AS tags
FROM articles a
JOIN article_tags at ON a.id = at.article_id
JOIN tags t ON at.tag_id = t.id
GROUP BY a.id, a.title;
パフォーマンスの注意点
| 注意点 | 対処方法 |
|---|---|
| WHERE句での文字列結合 | インデックスが効かなくなる。結合前の個別列で条件を書く |
| GROUP_CONCAT / STRING_AGG の長大結果 | 結合結果が膨大になると一時メモリを消費する。LIMIT や WHERE で行数を絞る |
| SELECT内での大量CONCAT | アプリ側で結合した方がDBの負荷を下げられることもある |
-- NG: インデックスが効かない SELECT * FROM employees WHERE CONCAT(last_name, first_name) = '山田太郎'; -- OK: 個別列で条件を書く SELECT * FROM employees WHERE last_name = '山田' AND first_name = '太郎';
まとめ
文字列結合の方法をRDBMS別に整理します。
| 目的 | MySQL | PostgreSQL | SQL Server | Oracle |
|---|---|---|---|---|
| 基本結合 | CONCAT(a, b, c) | a || b || c | a + b + c | a || b || c |
| NULLを安全に無視 | CONCAT_WS | CONCAT_WS | CONCAT / CONCAT_WS | NVL2 + || |
| 区切り文字付き | CONCAT_WS | CONCAT_WS | CONCAT_WS | 手動で || |
| 複数行→1行 | GROUP_CONCAT | STRING_AGG | STRING_AGG | LISTAGG |
- MySQL は
CONCATを使う(引数無制限・最も直感的) - PostgreSQL・Oracle は
||演算子が自然(ただしNULLに注意) - SQL Server は
CONCAT関数がNULLに強い(+はNULLで全体がNULLに) - NULLが入り得る列の結合は
CONCAT_WS(MySQL/PG/SQL Server)またはCOALESCEで対策する - 複数行を集約する場合は GROUP_CONCAT / STRING_AGG / LISTAGG を用途に応じて使い分ける
- LIKE演算子と組み合わせたパターンマッチや、スペース削除(TRIM/REPLACE)と組み合わせて使うケースも多い

