はじめに
本記事は、G's ACADEMY Advent Calendar 2025 に寄せて投稿した記事です。
メリクリ!🎄
元メガネ屋さんのひよっこエンジニアです。
G's Advent Calendar ということで何か面白い記事を書こうと思っていたのですが……
結局ネタが思いつかなかったので、今年仕事で「なるほど!」となったSQLの書き方を1つアウトプットします。
今回のテーマは、「SQLで、複数行のデータを1行にまとめて、列として表示する方法」です。
先輩方からすると「そんなことも知らんのか」と思われるかもしれませんが……
「しょーがねーだろ、赤ちゃんなんだから」(CV. ポプ子)
ということで、ツラツラ書き殴っていきたいと思います。
この記事に書いてあること
- 1人につき複数行あるデータを、1行にまとめる方法
- CASE文と集約関数(MAX)をなぜ組み合わせるのか
例題
従業員の緊急連絡先を管理するテーブルがあります。
このテーブルには、本人用の電話番号と家族用の電話番号が、それぞれ別の行として登録されています。
従業員ごとにデータを集約し、本人の電話番号と家族の電話番号を横に並べて1行で表示するSQLを作成してください。
※補足条件
- 本人用・家族用の連絡先は、それぞれ最大1件まで
- 該当する連絡先が存在しない場合は「設定なし」と表示する
<!--テーブル-->
EMP_ID | CONTACT_TYPE | TEL
-------+--------------+---------------
E001 | 01 | 090-1111-1111
E001 | 02 | 080-2222-2222
E002 | 01 | 090-3333-3333
E003 | 02 | 070-4444-4444
//テーブル名:EMP_EMERGENCY_CONTACT
//EMP_ID:従業員ID
//CONTACT_TYPE:緊急連絡先のタイプ(01:本人連絡先,02:家族連絡先)
//TEL:電話番号
<!--表示させたい結果-->
従業員ID | 本人電話 | 家族電話
---------+----------------+----------------
E001 | 090-1111-1111 | 080-2222-2222
E002 | 090-3333-3333 | 設定なし
E003 | 設定なし | 070-4444-4444
会社のベテランエンジニアの先輩に「こういうケースはよくあるから、スラーっと書けるようになってね」と言われたのですが、ひよっこの私は正直「???」な状態でした...( ;∀;)
SQLの書き方
まずは完成形として、下記のように書くことができます。
SELECT
EMP_ID AS '従業員ID',
COALESCE(
MAX(CASE WHEN CONTACT_TYPE = 01 THEN TEL END),
'設定なし'
) AS '本人電話',
COALESCE(
MAX(CASE WHEN CONTACT_TYPE = 02 THEN TEL END),
'設定なし'
) AS '家族電話'
FROM EMP_EMERGENCY_CONTACT
GROUP BY EMP_ID;
ここから順を追って、整理していきましょう。
STEP1:まずは「従業員ごと」にまとめる
今回の目的は「従業員ごとにまとめて、1行で表示する」 ことです。
そのためベースになるSQLは、SELECTとGROUPBYを用いた次の文になります。
SELECT
EMP_ID
FROM EMP_EMERGENCY_CONTACT
GROUP BY EMP_ID;
これで、従業員IDごとに1行で表示することができます。
STEP2:列として表示したい項目を考える
最終的に表示したい列は、「従業員ID」「本人の電話番号」「家族の電話番号」の3つなので、下記のようなSQL文を考えることができます。
SELECT
EMP_ID,
本人連絡先,
家族連絡先
FROM EMP_EMERGENCY_CONTACT
GROUP BY EMP_ID;
あとは、「本人電話」「家族電話」をどうやって取り出すか?を考えればOKです。
STEP3:CASE文で「どの電話番号か」を振り分ける
CONTACT_TYPE の値によって、「01 → 本人の電話番号」「02 → 家族の電話番号」を取り出したいので、CASE文を使います。
<!--CONTACT_TYPE = 01 のとき → 本人の電話番号を取り出したい-->
CASE WHEN CONTACT_TYPE = 01 THEN TEL END
このCASE文は、条件に一致する場合は電話番号を返し、一致しない場合はNULLを返すという動きをします。
CASE文だけを評価するのであれば、下記のようなデータ取得が行われます。
CASE WHEN CONTACT_TYPE = 01 THEN TEL END
<!--
[結果]
EMP_ID | CONTACT_TYPE | TEL
-------+--------------+---------------
E001 | 01 | 090-1111-1111
E001 | 02 | NULL
E002 | 01 | 090-3333-3333
E003 | 02 | NULL
-->
STWP4:集約関数MAXを用いて、適切な値を返す
ここで注意したいのが、GROUPBYを用いているということです。
GROUPBY を使う場合、SELECT句には 集約関数(MAX / MIN など) を使う必要があります。
CASE文の結果は、下記のように複数行の値を返すため、そのままではSELECTすることができません。
CASE WHEN CONTACT_TYPE = 01 THEN TEL END
<!--EMP_ID=01にグループ化した場合の、本人電話列の取得結果-->
EMP_ID | CONTACT_TYPE | TEL
-------+--------------+---------------
E001 | 01 | 090-1111-1111
E001 | 02 | NULL
そこで MAX関数を使います。
今回は「最大1件まで」という前提があるため、MAX関数を用いることで、取得結果の「NULL」「電話番号」を比較し、最大値として電話番号の値を返すことになります。
MAX(CASE WHEN CONTACT_TYPE = 01 THEN TEL END)
<!--EMP_ID=01にグループ化した場合の、本人電話列の取得結果-->
EMP_ID | CONTACT_TYPE | TEL
-------+--------------+---------------
E001 | 01 | 090-1111-1111
つまり、MAX関数とCASE文を用いることで、下記のように従業員ごとにデータを集約し、本人の電話番号と家族の電話番号を横並びで1行に表示することができます。
SELECT
EMP_ID AS '従業員ID',
MAX(CASE WHEN CONTACT_TYPE = 01 THEN TEL END) AS '本人電話',
MAX(CASE WHEN CONTACT_TYPE = 02 THEN TEL END) AS '家族電話'
FROM EMP_EMERGENCY_CONTACT
GROUP BY EMP_ID;
<!--実行結果
従業員ID | 本人電話 | 家族電話
---------+----------------+----------------
E001 | 090-1111-1111 | 080-2222-2222
E002 | 090-3333-3333 | NULL
E003 | NULL | 070-4444-4444
-->
STEP5:COALESCEでNULLを「設定なし」に置き換える
ここまでくれば、あとはシンプルで、COALESCEを用いてNULLを「設定なし」に置換してあげればよいということになります。
結果完成した、最終的なSQLを再掲しておきます。
SELECT
EMP_ID AS '従業員ID',
COALESCE(
MAX(CASE WHEN CONTACT_TYPE = 01 THEN TEL END),
'設定なし'
) AS '本人電話',
COALESCE(
MAX(CASE WHEN CONTACT_TYPE = 02 THEN TEL END),
'設定なし'
) AS '家族電話'
FROM EMP_EMERGENCY_CONTACT
GROUP BY EMP_ID;
最後に...ひとりごと
日々勉強して、精進しないといけないなぁと思う今日この頃です...( ;∀;)
ただやっぱり新しいことを知ったり、コードを書くことは楽しい!!
G'sでもらった種火に、しっかり薪をくべて大きな炎にしていきたいなと思います。
最後に来年の抱負として、LAB17のBuzzワードを掲げたいと思います。
「燃えろ、俺の小宇宙!!」