【SQL】文字列結合完全ガイド|CONCAT・||演算子・CONCAT_WS・GROUP_CONCAT・STRING_AGG・NULL対策まで

【SQL】文字列結合完全ガイド|CONCAT・||演算子・CONCAT_WS・GROUP_CONCAT・STRING_AGG・NULL対策まで SQL

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()関数です。

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 です。

MySQL — 3つ以上の引数
-- 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)を使います。

PostgreSQL — CONCATネストまたは || 演算子
-- CONCATネスト(冗長だが動く)
SELECT CONCAT(CONCAT(last_name, ' '), first_name) AS full_name
FROM employees;

-- || 演算子(推奨:こちらが簡潔)
SELECT last_name || ' ' || first_name AS full_name
FROM employees;
SQL Server — CONCATネストまたは + 演算子
-- 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;
Oracle — CONCATネストまたは || 演算子
-- 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)
-- 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になる
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つに)
PostgreSQL — || 演算子のNULL対策
-- || 演算子は 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 → 結果: '太郎 山田'
SQL Server — + 演算子の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の引数を自動的にスキップするのが最大のメリットです。

CONCAT_WS の基本
-- 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の部分は区切り文字ごと消える)
CONCAT_WS の実務活用
-- 住所を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 で CONCAT_WS を代替する
-- 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()
MySQL — GROUP_CONCAT
-- 部署ごとにメンバー名をカンマ区切りでまとめる
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; で上限を変更してください。

PostgreSQL — STRING_AGG
-- 部署ごとにメンバー名をカンマ区切りでまとめる
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;
SQL Server(2017+) — STRING_AGG
-- 部署ごとにメンバー名をカンマ区切りでまとめる
SELECT
    department,
    STRING_AGG(last_name, ', ') WITHIN GROUP (ORDER BY last_name) AS members
FROM employees
GROUP BY department;
-- 結果: '営業部' | '伊藤, 山田, 鈴木'
Oracle — LISTAGG
-- 部署ごとにメンバー名をカンマ区切りでまとめる
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+)で回避できます。

Oracle LISTAGG の長さ制限対策
-- 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:住所の組み立て

NULLの項目を安全にスキップ
-- 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:タグ・カテゴリの一覧表示

複数タグを1つのセルに表示
-- 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の負荷を下げられることもある
WHERE句での結合は避ける
-- 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)と組み合わせて使うケースも多い