SPLIT_PART / LEFT / SUBSTRING — 構造化コードから部分文字列を抽出する
構造化された文字列コード(例: '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)
products テーブルの product_code(形式: 'カテゴリ-品番-地域')から、SPLIT_PART を使って category(1番目)・item_no(2番目)・region(3番目)を抽出してください。また、LEFT と SUBSTRING による代替実装も category_alt・item_no_alt として示してください。
| product_id | product_code | price |
|---|---|---|
| 1 | FOOD-001-JP | 980 |
| 2 | ELEC-042-US | 15800 |
| 3 | FOOD-007-JP | 1200 |
| 4 | CLOT-015-EU | 4500 |
| product_id | product_code | category | item_no | region | category_alt | item_no_alt |
|---|---|---|---|---|---|---|
| 1 | FOOD-001-JP | FOOD | 001 | JP | FOOD | 001 |
| 2 | ELEC-042-US | ELEC | 042 | US | ELEC | 042 |
| 3 | FOOD-007-JP | FOOD | 007 | JP | FOOD | 007 |
| 4 | CLOT-015-EU | CLOT | 015 | EU | CLOT | 015 |
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 */
LEGEND
① FROM — 構造化コードを含む元データ
FROM productsproduct_code は 'FOOD-001-JP' のように '-' 区切りで「カテゴリ-品番-地域」の構造を持ちます。SPLIT_PART でそれぞれの部分を取り出します。| product_id | product_code | price |
|---|---|---|
| 1 | FOOD-001-JP | 980 |
| 2 | ELEC-042-US | 15800 |
| 3 | FOOD-007-JP | 1200 |
| 4 | CLOT-015-EU | 4500 |
LEGEND
① 仮想データ
品番が4桁になった新データ品番が '042' から '0012' のように4桁に増えたレコードが混ざったとします。| product_code |
|---|
| 'FOOD-0012-JP' |
SUBSTRING_INDEX(str, delim, n)、SQL Serverでは STRING_SPLIT() を使います。標準SQLには区切り文字による分割関数がないため、DBMSごとの差異に注意しましょう。SUBSTRING(str FROM pos FOR len) は位置1が先頭文字です(多くの言語の0-indexedとは異なる)。FROM 6 は6文字目を意味します。POSITION('-' IN product_code) で最初の '-' の位置を取得できます。固定長でない場合は POSITION と SUBSTRING を組み合わせます。LEFT(code, 4) はカテゴリが常に4文字の前提です。将来5文字のカテゴリが追加されると全て壊れます。SPLIT_PART を使うことでフォーマットの桁数変更に強くなります。DATE_TRUNC / EXTRACT / TO_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 昇順で取得してください。
| order_id | ordered_at | amount |
|---|---|---|
| 101 | 2024-05-03 14:30:00 | 8000 |
| 102 | 2024-05-15 09:15:00 | 12500 |
| 103 | 2024-06-02 18:45:00 | 3200 |
| 104 | 2024-06-20 11:00:00 | 6700 |
| month | month_label | order_count | total_amount |
|---|---|---|---|
| 2024-05-01 00:00:00 | 2024年05月 | 2 | 20500 |
| 2024-06-01 00:00:00 | 2024年06月 | 2 | 9900 |
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 → 月の昇順でソート */
LEGEND
① FROM — TIMESTAMP型の元データ
FROM ordersordered_atは '2024-05-03 14:30:00' のようなTIMESTAMP型です。月ごとに集計するにはDATE_TRUNCで月初日に切り捨ててからGROUP BYします。| order_id | ordered_at (TIMESTAMP) | amount |
|---|---|---|
| 101 | 2024-05-03 14:30:00 | 8000 |
| 102 | 2024-05-15 09:15:00 | 12500 |
| 103 | 2024-06-02 18:45:00 | 3200 |
| 104 | 2024-06-20 11:00:00 | 6700 |
LEGEND
① 仮想データ
年をまたぐデータ2024年5月と2025年5月のデータが混在しているとします。| ordered_at | amount |
|---|---|
| '2024-05-03' | 8000 |
| '2025-05-15' | 12500 |
DATE_TRUNC('week', ordered_at)、年次なら 'year' と変えるだけです。EXTRACT(MONTH FROM ts) は月を 1〜12 の数値で返します。これをGROUP BYすると2024年5月と2025年5月が同じグループになります。年をまたぐデータには DATE_TRUNC を使いましょう。TO_CHAR(ts, 'YYYY-MM-DD HH24:MI:SS') のように組み合わせます。DATE_TRUNC('month', ...) を使いましょう。WHERE ordered_at LIKE '2024-05%'(TEXT型)はインデックスが効かずフルスキャンになります。TIMESTAMP型で保存し WHERE ordered_at >= '2024-05-01' AND ordered_at < '2024-06-01' にしましょう。DATE_TRUNC('month', ordered_at AT TIME ZONE 'Asia/Tokyo') のようにタイムゾーンを明示することが重要です。UTCで保存されたTIMESTAMP(9時間差)を正しく日本時間の「月」で集計するには、タイムゾーン変換が必須です。タイムゾーンを無視すると、日本時間の月初日がUTCでは前月末になり、集計がズレるバグが発生します。ROUND / CEIL / 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 切り捨て(床)小数点以下を常に切り捨て
products テーブルの税抜 price と tax_rate から税込金額を計算してください。端数あり(tax_raw)、ROUND(tax_round)、CEIL(tax_ceil)、FLOOR(tax_floor)の4種類の列を付与して比較できるようにしてください。
| product_id | name | price | tax_rate |
|---|---|---|---|
| 1 | コーヒー | 486 | 0.08 |
| 2 | ノート | 82 | 0.10 |
| 3 | ペン | 95 | 0.10 |
| product_id | name | price | tax_rate | tax_raw | tax_round | tax_ceil | tax_floor |
|---|---|---|---|---|---|---|---|
| 1 | コーヒー | 486 | 0.08 | 524.88 | 525 | 525 | 524 |
| 2 | ノート | 82 | 0.10 | 90.20 | 90 | 91 | 90 |
| 3 | ペン | 95 | 0.10 | 104.50 | 105 | 105 | 104 |
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 昇順で出力 */
LEGEND
① FROM — 税抜価格と税率
FROM productspriceは税抜、tax_rateは0.08(8%)や0.10(10%)です。税込金額を計算すると小数点以下の端数が発生します。| product_id | name | price | tax_rate |
|---|---|---|---|
| 1 | コーヒー | 486 | 0.08 |
| 2 | ノート | 82 | 0.1 |
| 3 | ペン | 95 | 0.1 |
LEGEND
① FROM
FROM products元の価格データです。| name | price |
|---|---|
| コーヒー | 486 |
| ノート | 82 |
| ペン | 95 |
ROUND(9.875, 2) → 9.88 のように第2引数で小数点以下の桁数を指定できます。ROUND(1234.5, -2) → 1200 のように負数で100の位以上を丸めることもできます。0.1 + 0.2 がFLOAT型で0.30000000000000004になる問題が有名です。金額計算には必ず NUMERIC または DECIMAL 型を使いましょう。FLOAT型は内部的に2進数浮動小数点表現を使うため、0.08のような税率も厳密に表現できません。金額・税率には必ず NUMERIC(10,2) 等の固定小数点型を使いましょう。FLOOR(price * 1.10)(切り捨て)が多く採用されています。重要なのは「どの粒度で・どの方向で端数を処理するか」をシステム設計時に明示的に決定し、コードにコメントで残すことです。STRING_AGG — グループごとに文字列を1行に集約する
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 列 を指定すると、グループ内の連結順序を制御できます。ORDER BY を省略すると順序は非決定的(実行のたびに変わる可能性がある)になります。order_items テーブルから、注文ごとに商品名をカンマ区切りで集約した products(product_name の辞書順)と、合計数量 total_qty を取得してください。order_id 昇順で出力してください。
| order_id | product_name | quantity |
|---|---|---|
| 101 | コーヒー | 2 |
| 101 | サンドイッチ | 1 |
| 102 | コーヒー | 1 |
| 102 | ケーキ | 1 |
| 102 | 紅茶 | 2 |
| 103 | サンドイッチ | 3 |
| order_id | products | total_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 昇順で出力 */
LEGEND
① FROM — 複数商品行の元データ
FROM order_items1つのorder_idに対して複数の商品行があります。STRING_AGGで order_idごとに商品名を1行に集約します。| order_id | product_name | quantity |
|---|---|---|
| 101 | コーヒー | 2 |
| 101 | サンドイッチ | 1 |
| 102 | コーヒー | 1 |
| 102 | ケーキ | 1 |
| 102 | 紅茶 | 2 |
| 103 | サンドイッチ | 3 |
LEGEND
① FROM
FROM order_items同じ order_id に複数行あるデータです。| order_id | product_name |
|---|---|
| 101 | コーヒー |
| 101 | サンドイッチ |
| 102 | コーヒー |
| 102 | ケーキ |
| 102 | 紅茶 |
| 103 | サンドイッチ |
ARRAY_AGG(product_name ORDER BY product_name) は文字列配列 ARRAY['ケーキ','コーヒー','紅茶'] を返します。JSONに変換して API レスポンスとして返す用途などに便利です。GROUP_CONCAT(product_name ORDER BY product_name SEPARATOR ', ') を使います。書き方は異なりますが機能は同等です。LEFT(STRING_AGG(...), 500) などで上限を設けましょう。COALESCE(product_name, '(なし)') を使いましょう。json_agg(row_to_json(items)) のようなパターンと組み合わせると、複雑なネスト構造をSQL単体で生成することも可能です。REPLACE / REGEXP_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' 空白を '_' に置換
contacts テーブルには書式バラバラの電話番号があります。①REPLACE でハイフンのみ除去した replace_hyphen、②REGEXP_REPLACE で数字以外の文字を全て除去した regexp_digits_only を取得し、2つの結果の違いを確認してください。
| contact_id | phone |
|---|---|
| 1 | 03-1234-5678 |
| 2 | (090) 9876-5432 |
| 3 | 0120.000.111 |
| contact_id | original | replace_hyphen | regexp_digits_only |
|---|---|---|---|
| 1 | 03-1234-5678 | 0312345678 | 0312345678 |
| 2 | (090) 9876-5432 | (090) 98765432 | 09098765432 |
| 3 | 0120.000.111 | 0120.000.111 | 0120000111 |
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 */
LEGEND
① FROM — 書式バラバラの電話番号
FROM contacts電話番号が '03-1234-5678'、'(090) 9876-5432'、'0120.000.111' と異なる書式で混在しています。統一された数字列に正規化します。| contact_id | phone (元) |
|---|---|
| 1 | 03-1234-5678 |
| 2 | (090) 9876-5432 |
| 3 | 0120.000.111 |
LEGEND
① FROM
数字のみの電話番号ハイフンなしで保存された10桁の電話番号があるとします。| phone_raw |
|---|
| '0312345678' |
[^0-9] は「0-9以外の任意の文字」、\d は「数字」、\s+ は「1つ以上の空白」、^\d{11}$ は「11桁の数字列(先頭から末尾)」。REGEXP_REPLACE で置換、regexp_matches で抽出、~ 演算子でパターンマッチングができます。REGEXP_REPLACE('0312345678', '^(\d{2})(\d{4})(\d{4})$', '\1-\2-\3') → '03-1234-5678' のように、グループ化(括弧)を使った書式変換も可能です。REPLACE(REPLACE(REPLACE(phone,'-',''),'(',''),')','') ... のように REPLACE を何重にもネストするのはコードが煩雑になります。REGEXP_REPLACE で1回で処理しましょう。CHECK (phone ~ '^\d{10,11}$') でDBレベルで書式を強制するか、トリガーや GENERATED COLUMN で自動正規化する方法が実務で使われます。REGEXP_REPLACEはレガシーデータの一括クリーニングやバッチ処理での変換には非常に便利です。