LPAD / RPAD / CONCAT_WS — ゼロ埋め・固定長整形で帳票コードを生成する
文字列を固定長にパディング(埋め込み)する関数と、NULL安全な文字列連結関数です。
LPAD('5', 6, '0') -- → '000005' 左に '0' を補填して6文字に LPAD('ABCDEFGH', 6, '0') -- → 'ABCDEF' 元が指定長を超えると右側を切り捨てる RPAD('FOOD', 8, '.') -- → 'FOOD....' 右に '.' を補填して8文字に UPPER('tokyo') -- → 'TOKYO' 全て大文字 CONCAT_WS('-', '2024', 'TKY', '000005') -- → '2024-TKY-000005' 区切り文字で連結 CONCAT_WS('-', '2024', NULL, '000005') -- → '2024-000005' NULL はスキップ ✓ CONCAT('2024', '-', NULL, '-', '000005') -- → '2024--000005' NULL は無視するが区切りが重複 ✗
CONCAT_WS(区切り文字, a, b, c) は NULL 引数を自動スキップし、残った値の間だけに区切り文字を挿入します。通常の CONCAT も NULL を無視しますが、区切り文字を個別の引数として渡すと '2024--000005' のように区切りが重複します。実データには NULL が混在することが多いため、区切り付きの文字列連結では CONCAT_WS を優先しましょう。invoices テーブルから以下の列を生成してください。
① padded_seq: seq_no(整数)を6桁ゼロ埋めしたシーケンスコード
② invoice_no: {fiscal_year}-{UPPER(branch_code)}-{padded_seq} 形式の帳票番号
③ display_label: customer_name を右側ドット埋めで16文字に整形したラベル
| invoice_id | fiscal_year | branch_code | seq_no | customer_name |
|---|---|---|---|---|
| 1 | 2024 | tky | 5 | Alpine Cafe |
| 2 | 2024 | osk | 123 | Tech Corp |
| 3 | 2024 | tky | 42 | Garden Shop |
| 4 | 2025 | ngy | 1 | Book World |
| invoice_id | padded_seq | invoice_no | display_label |
|---|---|---|---|
| 1 | 000005 | 2024-TKY-000005 | Alpine Cafe..... |
| 2 | 000123 | 2024-OSK-000123 | Tech Corp....... |
| 3 | 000042 | 2024-TKY-000042 | Garden Shop..... |
| 4 | 000001 | 2025-NGY-000001 | Book World...... |
AGE / INTERVAL / NOW() — 有効期限・残日数・更新日をリアルタイムで算出する
現在日時を基準にした期間計算に使う関数群です。
DATE '2024-12-15' -- 再現可能な固定基準日(DATE型) NOW() -- → '2024-12-15 09:30+09' 現在日時(TIMESTAMP WITH TIME ZONE) -- DATE 同士の引き算 → INTEGER (日数) '2025-01-14'::DATE - '2024-12-15'::DATE -- → 30 (整数・合計日数) -- AGE → INTERVAL (人間が読みやすい相対表示) AGE('2025-11-30'::DATE, '2024-12-15'::DATE) -- → '11 mons 15 days'(合計日数≠350!) EXTRACT(DAY FROM AGE(...)) -- → 15 ← 日成分のみ!合計日数ではない -- INTERVAL 加算・減算で未来/過去の日付を計算 '2025-01-14'::DATE + INTERVAL '1 year' -- → '2026-01-14 00:00:00' NOW() - INTERVAL '30 days' -- → 30日前の日時
AGE('2025-11-30', '2024-12-15') は '11 mons 15 days' という INTERVAL を返します。EXTRACT(DAY FROM ...) を適用すると 15(日の成分のみ)が返り、合計日数の 350 にはなりません。合計日数の整数が必要なときは expires_at::DATE - DATE '2024-12-15' を使いましょう。subscriptions テーブルから、DATE '2024-12-15' を基準にして以下を算出してください(出力例は 2024-12-15 基準)。
① days_remaining: 有効期限までの残日数(期限切れは負の値)
② status: '期限切れ' / '期限間近'(30日以内)/ '有効' の3段階ラベル
③ renewal_at: 次回更新予定日(現在の有効期限 + 1年)を 'YYYY-MM-DD' 形式で
days_remaining の昇順で出力してください。
| user_id | plan | started_at | expires_at |
|---|---|---|---|
| 1 | basic | 2024-01-15 | 2025-01-14 |
| 2 | pro | 2023-06-01 | 2024-07-31 |
| 3 | premium | 2024-11-01 | 2025-11-30 |
| 4 | basic | 2024-03-01 | 2024-12-31 |
| user_id | plan | days_remaining | status | renewal_at |
|---|---|---|---|---|
| 2 | pro | -137 | 期限切れ | 2025-07-31 |
| 4 | basic | 16 | 期限間近 | 2025-12-31 |
| 1 | basic | 30 | 期限間近 | 2026-01-14 |
| 3 | premium | 350 | 有効 | 2026-11-30 |
CASE WHEN + ROUND + GREATEST — 段階割引と最低価格保証を1クエリで実装する
複数条件の分岐・上下限値クランプ・CTE(共通テーブル式)の組み合わせです。
-- CASE 値 WHEN 比較値 THEN 結果 の等値比較形式(簡潔版) CASE customer_rank WHEN 'gold' THEN 0.10 -- customer_rank = 'gold' の場合 WHEN 'silver' THEN 0.05 ELSE 0.00 -- どれにも一致しない場合(ELSE は必ず書く習慣を) END GREATEST(405, 500) -- → 500 複数引数の最大値(下限クランプに使用) GREATEST(NULL, 500) -- → 500 NULL は無視される(COALESCE と同様の挙動) LEAST(12000, 10000) -- → 10000 複数引数の最小値(上限クランプに使用)
orders テーブルから、顧客ランクに応じた段階割引を適用した価格を計算してください。
割引率: platinum=20% / gold=10% / silver=5% / bronze=0%
① discount_rate: ランクに対応する割引率
② discounted: 割引後の金額(ROUND で整数に四捨五入)
③ final_price: 最低価格500円を下回らないようクランプした最終価格
CTE(WITH句)を使って CASE WHEN の重複を避けてください。
| order_id | customer_id | customer_rank | subtotal |
|---|---|---|---|
| 1 | 101 | platinum | 15000 |
| 2 | 102 | gold | 8500 |
| 3 | 103 | silver | 3200 |
| 4 | 104 | bronze | 980 |
| 5 | 105 | gold | 450 |
| order_id | customer_rank | subtotal | discount_rate | discounted | final_price |
|---|---|---|---|---|---|
| 1 | platinum | 15000 | 0.20 | 12000 | 12000 |
| 2 | gold | 8500 | 0.10 | 7650 | 7650 |
| 3 | silver | 3200 | 0.05 | 3040 | 3040 |
| 4 | bronze | 980 | 0.00 | 980 | 980 |
| 5 | gold | 450 | 0.10 | 405 | 500 |
FILTER句 + STRING_AGG(DISTINCT) — 条件付き集計と重複排除を同時に行う
集計関数に条件フィルタリングを追加する FILTER 句と、重複を除いた文字列集約の組み合わせです。
-- FILTER句: 集計関数の中で WHERE のように絞り込む(GROUP BY の行数は変えない) COUNT(*) FILTER (WHERE status = 'completed') -- → 完了した行のみカウント SUM(amount) FILTER (WHERE status = 'completed') -- → 完了した行の金額のみ合計 -- STRING_AGG(DISTINCT ...): 重複なしで文字列を連結 STRING_AGG(DISTINCT product_name, ', ' ORDER BY product_name) -- → 同じ商品名を1回だけ含めて辞書順に連結 -- 注: DISTINCT 使用時、ORDER BY 式は集約関数の引数に含める必要がある -- 整数同士の除算でゼロにならないよう 100.0 を掛ける 100.0 * COUNT(*) FILTER (...) / COUNT(*) -- → 66.7 (NUMERIC除算) 100 * COUNT(*) FILTER (...) / COUNT(*) -- → 66 (整数除算で小数点以下が切り捨て!)
sales テーブルから、カテゴリごとに以下を集計してください。
① total_sales: 全件数(ステータス問わず)
② completed_count: status = 'completed' の件数(FILTER 使用)
③ completed_amount: 完了した取引の合計金額(FILTER 使用)
④ completion_rate: 完了率(%、小数1桁)
⑤ products: 重複排除・辞書順の商品名リスト(STRING_AGG(DISTINCT) 使用)
category の昇順で出力してください。
| sale_id | category | product_name | amount | status |
|---|---|---|---|---|
| 1 | food | Coffee | 500 | completed |
| 2 | food | Sandwich | 800 | completed |
| 3 | food | Coffee | 500 | cancelled |
| 4 | tech | Mouse | 3000 | completed |
| 5 | tech | Keyboard | 8500 | completed |
| 6 | tech | Mouse | 3000 | refunded |
| 7 | books | SQL Guide | 1500 | completed |
| 8 | books | Design Book | 2200 | cancelled |
| category | total_sales | completed_count | completed_amount | completion_rate | products |
|---|---|---|---|---|---|
| books | 2 | 1 | 1500 | 50.0 | Design Book, SQL Guide |
| food | 3 | 2 | 1300 | 66.7 | Coffee, Sandwich |
| tech | 3 | 2 | 11500 | 66.7 | Keyboard, Mouse |
REGEXP_MATCHES / REGEXP_REPLACE — 構造化ログからデータを抽出・変換する
正規表現の「キャプチャグループ ()」を使ったデータ抽出と書式変換の応用です。
-- REGEXP_MATCHES: マッチしたグループを配列で返す (setof text[]) REGEXP_MATCHES('user=42; action=login', 'user=(\d+); action=(\w+)') -- → {'42','login'} 各グループが配列要素に -- 配列インデックスで各グループを取り出す(1始まり) (REGEXP_MATCHES(text_value, 'user=(\d+); action=(\w+)'))[1] -- → '42' 1番目のグループ (REGEXP_MATCHES(text_value, 'user=(\d+); action=(\w+)'))[2] -- → 'login' 2番目のグループ -- REGEXP_REPLACE: キャプチャグループを参照して書式変換 REGEXP_REPLACE('2024/03/15', '^(\d{4})/(\d{2})/(\d{2})$', '\1-\2-\3') -- → '2024-03-15' \1=年 \2=月 \3=日 として参照 -- ~ 演算子でパターンマッチング(BOOLEAN を返す) 'NOTICE job complete' ~ '^(NOTICE|DEBUG)' -- → true (大文字小文字を区別) 'notice job complete' ~* '^(notice|debug)' -- → true (~* は大文字小文字を区別しない)
() が使えます。REGEXP_MATCHES はマッチしない行では行を返さないため、LEFT JOIN や CASE で扱う必要があります。app_logs テーブルには構造化されていないログ文字列が保存されています。正規表現で解析し、以下の列を生成してください。
① log_level: ログレベル(ERROR, WARN, INFO)
② status_code: HTTPステータスコード(数値)
③ endpoint: APIエンドポイントパス
④ formatted_date: log_date('YYYYMMDD' 形式)を 'YYYY-MM-DD' 形式に変換
⑤ is_error: ログレベルが ERROR か WARN なら true(~ 演算子使用)
log_id の昇順で出力してください。
| log_id | log_date | log_message |
|---|---|---|
| 1 | 20240315 | ERROR 500 /api/users |
| 2 | 20240315 | INFO 200 /api/health |
| 3 | 20240316 | WARN 429 /api/orders |
| 4 | 20240316 | ERROR 404 /api/products |
| 5 | 20240317 | INFO 200 /api/users |
| log_id | log_level | status_code | endpoint | formatted_date | is_error |
|---|---|---|---|---|---|
| 1 | ERROR | 500 | /api/users | 2024-03-15 | true |
| 2 | INFO | 200 | /api/health | 2024-03-15 | false |
| 3 | WARN | 429 | /api/orders | 2024-03-16 | true |
| 4 | ERROR | 404 | /api/products | 2024-03-16 | true |
| 5 | INFO | 200 | /api/users | 2024-03-17 | false |