SQL データ整形・変換 — 日付書式・正規表現の基礎

基礎データ変換CAST / COALESCECASE WHEN文字列・日付・数値関数STRING_AGG / REGEXPPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

SPLIT_PART / LEFT / SUBSTRING — 構造化コードから部分文字列を抽出する

SPLIT_PARTSUBSTRING文字列抽出コード解析
前提知識

構造化された文字列コード(例: 'FOOD-001-JP')から特定部分を取り出す関数群です。

SPLIT_PART('FOOD-001-JP', '-', 1)   -- → 'FOOD'  (1番目の区切り部分)
SPLIT_PART('FOOD-001-JP', '-', 2)   -- → '001'   (2番目)
SPLIT_PART('FOOD-001-JP', '-', 3)   -- → 'JP'    (3番目)
LEFT('FOOD-001-JP', 4)             -- → 'FOOD'  (先頭4文字)
RIGHT('FOOD-001-JP', 2)            -- → 'JP'    (末尾2文字)
SUBSTRING('FOOD-001-JP' FROM 6 FOR 3) -- → '001'   (位置6から3文字、1-indexed)
SPLIT_PART vs LEFT/SUBSTRING:SPLIT_PART は区切り文字が固定の場合に直感的でメンテしやすいです。LEFT/SUBSTRING は固定長フォーマットの場合に適しています。区切り文字の数が変わるとSUBSTRINGは壊れますが、SPLIT_PART は位置番号で安定します。
問題

products テーブルの product_code(形式: 'カテゴリ-品番-地域')から、SPLIT_PART を使って category(1番目)・item_no(2番目)・region(3番目)を抽出してください。また、LEFT と SUBSTRING による代替実装も category_altitem_no_alt として示してください。

使用テーブル
▸ products
product_idproduct_codeprice
1FOOD-001-JP980
2ELEC-042-US15800
3FOOD-007-JP1200
4CLOT-015-EU4500
期待出力
product_idproduct_codecategoryitem_noregioncategory_altitem_no_alt
1FOOD-001-JPFOOD001JPFOOD001
2ELEC-042-USELEC042USELEC042
3FOOD-007-JPFOOD007JPFOOD007
4CLOT-015-EUCLOT015EUCLOT015
模範解答コード
SELECT
  product_id,
  product_code,
  SPLIT_PART(product_code, '-', 1)   AS category,     -- '-'で分割、1番目 → カテゴリ
  SPLIT_PART(product_code, '-', 2)   AS item_no,      -- 2番目 → 品番
  SPLIT_PART(product_code, '-', 3)   AS region,       -- 3番目 → 地域コード
  LEFT(product_code, 4)             AS category_alt, -- 先頭4文字(固定長なら可)
  SUBSTRING(product_code FROM 6 FOR 3)
                                    AS item_no_alt   -- 位置6から3文字(1-indexed)
FROM   products
ORDER BY product_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM products                 → 全4行を読み込む
  2. SPLIT_PART(code, '-', 1/2/3)  → '-'を区切り文字として各部分を取得
  3. LEFT(code, 4)                 → 先頭4文字(F,O,O,D)
  4. SUBSTRING(code FROM 6 FOR 3)
  5. ORDER BY product_id
  */
解説(テーブル変化・ポイント)
SELECT product_id, product_code, SPLIT_PART(product_code, '-', 1) AS category, SPLIT_PART(product_code, '-', 2) AS item_no, SPLIT_PART(product_code, '-', 3) AS region, LEFT(product_code, 4) AS category_alt, SUBSTRING(product_code FROM 6 FOR 3) AS item_no_alt FROM products ORDER BY product_id;
LEGEND
データ取得・読込対象
① FROM — 構造化コードを含む元データ
FROM productsproduct_code は 'FOOD-001-JP' のように '-' 区切りで「カテゴリ-品番-地域」の構造を持ちます。SPLIT_PART でそれぞれの部分を取り出します。
1 / 4
product_idproduct_codeprice
1FOOD-001-JP980
2ELEC-042-US15800
3FOOD-007-JP1200
4CLOT-015-EU4500
4行
SELECT product_code, SPLIT_PART(product_code, '-', 2) AS split_res, SUBSTRING(product_code FROM 6 FOR 3) AS sub_res FROM ...
LEGEND
データ取得・読込対象
① 仮想データ
品番が4桁になった新データ品番が '042' から '0012' のように4桁に増えたレコードが混ざったとします。
1 / 3
product_code
'FOOD-0012-JP'
1行
学習ポイント
SPLIT_PARTはPostgreSQL専用:MySQLでは SUBSTRING_INDEX(str, delim, n)、SQL Serverでは STRING_SPLIT() を使います。標準SQLには区切り文字による分割関数がないため、DBMSごとの差異に注意しましょう。
SUBSTRING の位置は1-indexedが標準:SQL の SUBSTRING(str FROM pos FOR len) は位置1が先頭文字です(多くの言語の0-indexedとは異なる)。FROM 6 は6文字目を意味します。
区切り文字の位置を動的に見つけるにはPOSITION:POSITION('-' IN product_code) で最初の '-' の位置を取得できます。固定長でない場合は POSITION と SUBSTRING を組み合わせます。
アンチパターン
固定位置SUBSTRINGはフォーマット変更に脆弱:LEFT(code, 4) はカテゴリが常に4文字の前提です。将来5文字のカテゴリが追加されると全て壊れます。SPLIT_PART を使うことでフォーマットの桁数変更に強くなります。
非構造化データをSQLで無理に解析する:JSON配列やCSV文字列をSQLで正規表現解析するのは複雑でパフォーマンスも悪化します。設計段階でデータを正規化したテーブル構造にするか、JSONBカラムを使いましょう。
実務コラム:構造化コードの設計とDBの正規化
'FOOD-001-JP' のような複合コードは運用中に便利ですが、本来は category / item_no / region を別カラムに持つのが正規化されたDB設計です。既存システムのレガシーコードをSQLで解析するためにSPLIT_PARTが役立ちますが、新規設計ではこのような変換クエリが不要になるよう、各属性を独立したカラムに持つことを推奨します。
QUESTION 7

DATE_TRUNC / EXTRACT / TO_CHAR — 日付・時刻を集計・変換・書式化する

DATE_TRUNCTO_CHAR月次集計日付操作
前提知識

日付・時刻データの整形に使う主要関数です。

DATE_TRUNC('month', '2024-05-15 14:30:00'::TIMESTAMP)
                             -- → 2024-05-01 00:00:00(月の初日に切り捨て)
DATE_TRUNC('year', ts)  -- → 年初日 (2024-01-01 00:00:00)
DATE_TRUNC('day', ts)   -- → 当日 00:00:00
EXTRACT(MONTH FROM ts)  -- → 5  (月を数値で取得)
EXTRACT(YEAR FROM ts)   -- → 2024
TO_CHAR(ts, 'YYYY年MM月DD日') -- → '2024年05月15日'(任意書式へ変換)
月次集計のポイント:GROUP BY DATE_TRUNC('month', ordered_at) で同一月を同じグループに集約できます。EXTRACT(MONTH FROM ...) だけでは2024年5月と2025年5月が同じグループになってしまうため、月次集計には DATE_TRUNC が安全です。
問題

orders テーブルのタイムスタンプを月ごとに集計してください。DATE_TRUNC で月初日に切り捨てた month、TO_CHAR で 'YYYY年MM月' 形式の month_label、月ごとの件数 order_count、合計金額 total_amount を month 昇順で取得してください。

使用テーブル
▸ orders
order_idordered_atamount
1012024-05-03 14:30:008000
1022024-05-15 09:15:0012500
1032024-06-02 18:45:003200
1042024-06-20 11:00:006700
期待出力
monthmonth_labelorder_counttotal_amount
2024-05-01 00:00:002024年05月220500
2024-06-01 00:00:002024年06月29900
模範解答コード
SELECT
  DATE_TRUNC('month', ordered_at)               AS month,       -- 月初日 00:00:00 に切り捨て
  TO_CHAR(DATE_TRUNC('month', ordered_at),
           'YYYY年MM月')                          AS month_label,  -- 表示用フォーマット
  COUNT(*)                                      AS order_count,  -- 月ごとの注文件数
  SUM(amount)                                    AS total_amount  -- 月ごとの合計金額
FROM   orders
GROUP BY DATE_TRUNC('month', ordered_at)  -- 切り捨て後の月ごとにグループ化
ORDER BY month;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders                      → 全4行を読み込む
  2. DATE_TRUNC('month', ordered_at)  → 各行のTIMESTAMPを月初日
  3. GROUP BY DATE_TRUNC(...)         → 同じ月初日でグループ化
  4. COUNT(*), SUM(amount)            → 各グループを集計
  5. TO_CHAR(...)                     → 月ラベルを 'YYYY年MM月' にフォーマット
  6. ORDER BY month                   → 月の昇順でソート
  */
解説(テーブル変化・ポイント)
SELECT DATE_TRUNC('month', ordered_at) AS month, TO_CHAR(DATE_TRUNC('month', ordered_at), 'YYYY年MM月') AS month_label, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders GROUP BY DATE_TRUNC('month', ordered_at) ORDER BY month;
LEGEND
データ取得・読込対象
① FROM — TIMESTAMP型の元データ
FROM ordersordered_atは '2024-05-03 14:30:00' のようなTIMESTAMP型です。月ごとに集計するにはDATE_TRUNCで月初日に切り捨ててからGROUP BYします。
1 / 4
order_idordered_at (TIMESTAMP)amount
1012024-05-03 14:30:008000
1022024-05-15 09:15:0012500
1032024-06-02 18:45:003200
1042024-06-20 11:00:006700
4行
SELECT EXTRACT(MONTH FROM ordered_at) AS month_num, SUM(amount) AS total FROM orders_multi_year GROUP BY EXTRACT(MONTH FROM ordered_at);
LEGEND
データ取得・読込対象
① 仮想データ
年をまたぐデータ2024年5月と2025年5月のデータが混在しているとします。
1 / 3
ordered_atamount
'2024-05-03'8000
'2025-05-15'12500
2行
学習ポイント
DATE_TRUNCは様々な精度で使える:'year'/'quarter'/'month'/'week'/'day'/'hour'/'minute' 等が使えます。週次集計なら DATE_TRUNC('week', ordered_at)、年次なら 'year' と変えるだけです。
EXTRACTは数値を取り出す(集計キーには不向き):EXTRACT(MONTH FROM ts) は月を 1〜12 の数値で返します。これをGROUP BYすると2024年5月と2025年5月が同じグループになります。年をまたぐデータには DATE_TRUNC を使いましょう。
TO_CHARの主要フォーマットコード:'YYYY'=4桁年、'MM'=2桁月、'DD'=2桁日、'HH24'=24時間表記の時間、'MI'=分、'SS'=秒。TO_CHAR(ts, 'YYYY-MM-DD HH24:MI:SS') のように組み合わせます。
アンチパターン
EXTRACT(MONTH FROM ...)だけでGROUP BYする:複数年のデータがある場合、2024年5月と2025年5月が同じグループになります。月次集計では必ず DATE_TRUNC('month', ...) を使いましょう。
日付をTEXTで保存してLIKEで月を絞り込む:WHERE ordered_at LIKE '2024-05%'(TEXT型)はインデックスが効かずフルスキャンになります。TIMESTAMP型で保存し WHERE ordered_at >= '2024-05-01' AND ordered_at < '2024-06-01' にしましょう。
実務コラム:タイムゾーンとDATE_TRUNC
本番環境では DATE_TRUNC('month', ordered_at AT TIME ZONE 'Asia/Tokyo') のようにタイムゾーンを明示することが重要です。UTCで保存されたTIMESTAMP(9時間差)を正しく日本時間の「月」で集計するには、タイムゾーン変換が必須です。タイムゾーンを無視すると、日本時間の月初日がUTCでは前月末になり、集計がズレるバグが発生します。
QUESTION 8

ROUND / CEIL / FLOOR — 数値を丸める(税込計算で比較)

ROUNDCEIL/FLOOR数値整形税込計算
前提知識

数値の小数点以下を処理する3種類の丸め関数です。

ROUND(524.88)     -- → 525   四捨五入(0.5以上で繰り上げ)
ROUND(90.20)      -- → 90    (0.2は切り捨て)
ROUND(9.999, 2)  -- → 10.00 小数点2桁に四捨五入
CEIL(90.20)       -- → 91    切り上げ(天井)小数点以下があれば必ず+1
CEIL(90.00)       -- → 90    ちょうど整数なら変化なし
FLOOR(90.80)      -- → 90    切り捨て(床)小数点以下を常に切り捨て
消費税計算でどれを使うか:日本の消費税法上は切り捨て・切り上げ・四捨五入どれも合法です。一般的には端数を切り捨て(FLOOR)が多く使われますが、システム仕様に合わせて選択します。金額計算には FLOAT でなく NUMERIC を使い精度を保証しましょう
問題

products テーブルの税抜 pricetax_rate から税込金額を計算してください。端数あり(tax_raw)、ROUND(tax_round)、CEIL(tax_ceil)、FLOOR(tax_floor)の4種類の列を付与して比較できるようにしてください。

使用テーブル
▸ products
product_idnamepricetax_rate
1コーヒー4860.08
2ノート820.10
3ペン950.10
期待出力
product_idnamepricetax_ratetax_rawtax_roundtax_ceiltax_floor
1コーヒー4860.08524.88525525524
2ノート820.1090.20909190
3ペン950.10104.50105105104
模範解答コード
SELECT
  product_id,
  name,
  price,
  tax_rate,
  price * (1 + tax_rate)                AS tax_raw,   -- 税込(端数あり)
  ROUND(price * (1 + tax_rate))        AS tax_round,  -- 四捨五入(0.5以上で繰り上げ)
  CEIL( price * (1 + tax_rate))        AS tax_ceil,   -- 切り上げ(小数点以下があれば必ず+1)
  FLOOR(price * (1 + tax_rate))        AS tax_floor   -- 切り捨て(小数点以下を常に除去)
FROM   products
ORDER BY product_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM products           → 全3行を読み込む
  2. price * (1 + tax_rate)  → 税込金額を計算(端数あり)
  3. ROUND(x)                → 四捨五入
  4. CEIL(x)                 → 切り上げ(小数点以下があれば+1)
  5. FLOOR(x)                → 切り捨て(小数点以下を除去)
  6. ORDER BY product_id     → product_id 昇順で出力
  */
解説(テーブル変化・ポイント)
SELECT product_id, name, price, tax_rate, price * (1 + tax_rate) AS tax_raw, ROUND(price * (1 + tax_rate)) AS tax_round, CEIL( price * (1 + tax_rate)) AS tax_ceil, FLOOR(price * (1 + tax_rate)) AS tax_floor FROM products ORDER BY product_id;
LEGEND
データ取得・読込対象
① FROM — 税抜価格と税率
FROM productspriceは税抜、tax_rateは0.08(8%)や0.10(10%)です。税込金額を計算すると小数点以下の端数が発生します。
1 / 4
product_idnamepricetax_rate
1コーヒー4860.08
2ノート820.1
3ペン950.1
3行
SELECT name, price, ROUND(price, -1) AS round_10, ROUND(price, -2) AS round_100 FROM products;
LEGEND
データ取得・読込対象
① FROM
FROM products元の価格データです。
1 / 3
nameprice
コーヒー486
ノート82
ペン95
3行
学習ポイント
ROUNDは小数桁を指定できる:ROUND(9.875, 2) → 9.88 のように第2引数で小数点以下の桁数を指定できます。ROUND(1234.5, -2) → 1200 のように負数で100の位以上を丸めることもできます。
CEILは「必ず損をしない」丸め:料金計算でユーザーに有利な方向(売上を確保する方向)に丸めたい場合はCEIL、ユーザー有利(切り捨て)はFLOOR、中立はROUNDが定番です。システム仕様書で「端数処理: 切り上げ」と指定されていればCEILを使います。
FLOATで金額計算すると精度エラーが起きる:0.1 + 0.2 がFLOAT型で0.30000000000000004になる問題が有名です。金額計算には必ず NUMERIC または DECIMAL 型を使いましょう。
アンチパターン
FLOAT型で金額を保存する:FLOAT型は内部的に2進数浮動小数点表現を使うため、0.08のような税率も厳密に表現できません。金額・税率には必ず NUMERIC(10,2) 等の固定小数点型を使いましょう。
アプリ側でROUNDせずDBに保存する:端数のある金額をそのままDBに保存すると、集計時に合計がずれることがあります。「個別の端数処理後の値」をDBに保存するか、「保存は端数あり・表示時にROUND」かを設計段階で統一しましょう。
実務コラム:日本の消費税計算の実装パターン
日本の消費税では1円未満の端数処理について法令上の強制はありませんが、インボイス制度(適格請求書)では適格請求書ごとに端数処理を行うと規定されています。SQLでの実装としては FLOOR(price * 1.10)(切り捨て)が多く採用されています。重要なのは「どの粒度で・どの方向で端数を処理するか」をシステム設計時に明示的に決定し、コードにコメントで残すことです。
QUESTION 9

STRING_AGG — グループごとに文字列を1行に集約する

STRING_AGGGROUP BY文字列集約注文明細
前提知識

STRING_AGG(列, 区切り文字) は GROUP BY と組み合わせて、グループ内の文字列値を1つの文字列に連結します。MySQL の GROUP_CONCAT に相当します。

STRING_AGG(product_name, ', ')
  -- → 'コーヒー, ケーキ, 紅茶'  (順序は不定)

STRING_AGG(product_name, ', ' ORDER BY product_name)
  -- → 'ケーキ, コーヒー, 紅茶'  (ORDER BYで順序を指定)

ARRAY_AGG(product_name ORDER BY product_name)
  -- → ARRAY['ケーキ','コーヒー','紅茶']  (配列として集約)
ORDER BY を内包できる:STRING_AGG の中に ORDER BY 列 を指定すると、グループ内の連結順序を制御できます。ORDER BY を省略すると順序は非決定的(実行のたびに変わる可能性がある)になります。
問題

order_items テーブルから、注文ごとに商品名をカンマ区切りで集約した products(product_name の辞書順)と、合計数量 total_qty を取得してください。order_id 昇順で出力してください。

使用テーブル
▸ order_items
order_idproduct_namequantity
101コーヒー2
101サンドイッチ1
102コーヒー1
102ケーキ1
102紅茶2
103サンドイッチ3
期待出力
order_idproductstotal_qty
101コーヒー, サンドイッチ3
102ケーキ, コーヒー, 紅茶4
103サンドイッチ3
模範解答コード
SELECT
  order_id,
  STRING_AGG(product_name, ', '
    ORDER BY product_name)    AS products,  -- 辞書順で連結(ORDER BY必須で順序を確定)
  SUM(quantity)              AS total_qty   -- グループ内の数量合計
FROM   order_items
GROUP BY order_id             -- order_id ごとにグループ化
ORDER BY order_id;            -- 結果を order_id 昇順で出力

/*
  実行順序(SQLの論理的な評価順):
  1. FROM order_items             → 全6行を読み込む
  2. GROUP BY order_id            → order_idごとにグループ化
  3. STRING_AGG(...ORDER BY ...)  → 各グループ内でproduct_nameを辞書順に並べて', 'で連結
  4. SUM(quantity)                → 各グループの数量合計
  5. ORDER BY order_id            → order_id 昇順で出力
  */
解説(テーブル変化・ポイント)
SELECT order_id, STRING_AGG(product_name, ', ' ORDER BY product_name) AS products, SUM(quantity) AS total_qty FROM order_items GROUP BY order_id ORDER BY order_id;
LEGEND
データ取得・読込対象
① FROM — 複数商品行の元データ
FROM order_items1つのorder_idに対して複数の商品行があります。STRING_AGGで order_idごとに商品名を1行に集約します。
1 / 4
order_idproduct_namequantity
101コーヒー2
101サンドイッチ1
102コーヒー1
102ケーキ1
102紅茶2
103サンドイッチ3
6行 (3注文)
SELECT order_id, ARRAY_AGG(product_name ORDER BY product_name) AS product_array FROM order_items GROUP BY order_id;
LEGEND
データ取得・読込対象
① FROM
FROM order_items同じ order_id に複数行あるデータです。
1 / 2
order_idproduct_name
101コーヒー
101サンドイッチ
102コーヒー
102ケーキ
102紅茶
103サンドイッチ
6行
学習ポイント
ORDER BY を省略すると順序が非決定的になる:ORDER BY なしの STRING_AGG は実行計画やデータの物理順序によって結果が変わる可能性があります。テスト環境と本番で異なる順序が返ってくるバグの原因になります。ORDER BY を明示しましょう。
ARRAY_AGGは配列として集約できる:ARRAY_AGG(product_name ORDER BY product_name) は文字列配列 ARRAY['ケーキ','コーヒー','紅茶'] を返します。JSONに変換して API レスポンスとして返す用途などに便利です。
MySQLの対応関数はGROUP_CONCAT:MySQLでは GROUP_CONCAT(product_name ORDER BY product_name SEPARATOR ', ') を使います。書き方は異なりますが機能は同等です。
アンチパターン
非常に長い文字列が生成される:グループ内の行数が多い場合、STRING_AGGの結果が数MBになることがあります。DBの最大文字列長やネットワーク帯域に注意し、必要に応じて LEFT(STRING_AGG(...), 500) などで上限を設けましょう。
NULLがある場合の挙動:STRING_AGGはNULL値をスキップします(集計からNULLが除外される)。NULLを '(なし)' として含めたい場合は COALESCE(product_name, '(なし)') を使いましょう。
実務コラム:APIレスポンスのネストデータをSQLで作る
REST APIで「注文一覧に商品リストをネストして返す」ようなレスポンスは、アプリ側でN+1クエリを発行して組み立てることも多いですが、STRING_AGGやARRAY_AGG + JSON関数(json_agg等)を使えば1クエリで完結できます。PostgreSQLの json_agg(row_to_json(items)) のようなパターンと組み合わせると、複雑なネスト構造をSQL単体で生成することも可能です。
QUESTION 10

REPLACE / REGEXP_REPLACE — パターン置換で電話番号を正規化する

REPLACEREGEXP_REPLACE正規表現データ正規化
前提知識

文字列内の特定のパターンを別の文字列に置き換える2種類の関数です。

REPLACE('03-1234-5678', '-', '')
  -- → '0312345678'   固定文字列 '-' を '' に全置換

REGEXP_REPLACE('(090) 9876-5432', '[^0-9]', '', 'g')
  -- → '09098765432'  正規表現: 数字以外の文字を全て除去 ('g'=global)

REGEXP_REPLACE('hello world', '\s+', '_', 'g')
  -- → 'hello_world'  空白を '_' に置換
REPLACEとREGEXP_REPLACEの違い:REPLACEは固定文字列の置換のみです。REGEXP_REPLACEは正規表現パターンを使えるため、「数字以外の文字全て」「2文字以上の空白」「特定のフォーマット全般」など柔軟な置換が可能です。
問題

contacts テーブルには書式バラバラの電話番号があります。①REPLACE でハイフンのみ除去した replace_hyphen、②REGEXP_REPLACE で数字以外の文字を全て除去した regexp_digits_only を取得し、2つの結果の違いを確認してください。

使用テーブル
▸ contacts
contact_idphone
103-1234-5678
2(090) 9876-5432
30120.000.111
期待出力
contact_idoriginalreplace_hyphenregexp_digits_only
103-1234-567803123456780312345678
2(090) 9876-5432(090) 9876543209098765432
30120.000.1110120.000.1110120000111
模範解答コード
SELECT
  contact_id,
  phone                                          AS original,          -- 元の電話番号
  REPLACE(phone, '-', '')                        AS replace_hyphen,    -- '-'のみ除去(固定文字列)
  REGEXP_REPLACE(phone, '[^0-9]', '', 'g')       AS regexp_digits_only -- 数字以外を全除去
FROM   contacts
ORDER BY contact_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM contacts            → 全3行を読み込む
  2. REPLACE(phone, '-', '')  → 各行の '-' を '' (空文字) に全置換
  3. REGEXP_REPLACE(phone,'[^0-9]','','g')
  4. ORDER BY contact_id
  */
解説(テーブル変化・ポイント)
SELECT contact_id, phone AS original, REPLACE(phone, '-', '') AS replace_hyphen, REGEXP_REPLACE(phone, '[^0-9]', '', 'g') AS regexp_digits_only FROM contacts ORDER BY contact_id;
LEGEND
データ取得・読込対象
① FROM — 書式バラバラの電話番号
FROM contacts電話番号が '03-1234-5678'、'(090) 9876-5432'、'0120.000.111' と異なる書式で混在しています。統一された数字列に正規化します。
1 / 4
contact_idphone (元)
103-1234-5678
2(090) 9876-5432
30120.000.111
3行 (書式混在)
SELECT '0312345678' AS phone_raw, REGEXP_REPLACE( '0312345678', '^(\d{2})(\d{4})(\d{4})$', '\1-\2-\3' ) AS formatted_phone;
LEGEND
データ取得・読込対象
① FROM
数字のみの電話番号ハイフンなしで保存された10桁の電話番号があるとします。
1 / 2
phone_raw
'0312345678'
1行
学習ポイント
正規表現の基本パターン:[^0-9] は「0-9以外の任意の文字」、\d は「数字」、\s+ は「1つ以上の空白」、^\d{11}$ は「11桁の数字列(先頭から末尾)」。REGEXP_REPLACE で置換、regexp_matches で抽出、~ 演算子でパターンマッチングができます。
'g' フラグで全置換:'g'(global)フラグなしだと最初の1マッチだけ置換されます。電話番号のハイフン除去のように複数の置換が必要な場合は必ず 'g' を付けます。
REGEXP_REPLACEで書式変換もできる:REGEXP_REPLACE('0312345678', '^(\d{2})(\d{4})(\d{4})$', '\1-\2-\3') → '03-1234-5678' のように、グループ化(括弧)を使った書式変換も可能です。
アンチパターン
複数回のREPLACEを連鎖する:REPLACE(REPLACE(REPLACE(phone,'-',''),'(',''),')','') ... のように REPLACE を何重にもネストするのはコードが煩雑になります。REGEXP_REPLACE で1回で処理しましょう。
複雑すぎる正規表現:メールアドレスの完全なバリデーション正規表現などは数十文字になり、メンテが困難です。SQLではシンプルなパターンに留め、複雑なバリデーションはアプリ層で行う設計が保守性を高めます。
実務コラム:データ正規化はINSERT時にやる方がいい
電話番号の書式を SELECT 時に毎回 REGEXP_REPLACE するのは CPU コストがかかります。理想的には「INSERTまたはUPDATE時に正規化した値を保存し、SELECTではそのまま取り出す」設計です。CHECK制約 CHECK (phone ~ '^\d{10,11}$') でDBレベルで書式を強制するか、トリガーや GENERATED COLUMN で自動正規化する方法が実務で使われます。REGEXP_REPLACEはレガシーデータの一括クリーニングやバッチ処理での変換には非常に便利です。