TO_CHAR / DATE_TRUNC — タイムスタンプを日本語表示・月次集計キーに変換する
TO_CHAR(値, フォーマット) は日付・タイムスタンプを任意の文字列に変換する関数です。DATE_TRUNC('単位', タイムスタンプ) はタイムスタンプを指定単位(月・日・時など)の先頭に切り捨てます。基礎編の CAST とは逆方向(日付型 → 文字列)の変換です。
TO_CHAR('2024-05-15 14:30:00'::TIMESTAMP, 'YYYY年MM月DD日') -- → '2024年05月15日' TO_CHAR('2024-05-15 14:30:00'::TIMESTAMP, 'HH24:MI') -- → '14:30' DATE_TRUNC('month', '2024-05-15 14:30:00'::TIMESTAMP) -- → 2024-05-01 00:00:00 DATE_TRUNC('day', '2024-05-15 14:30:00'::TIMESTAMP) -- → 2024-05-15 00:00:00
YYYY=4桁年、MM=2桁月、DD=2桁日、HH24=24時間制の時、MI=分、SS=秒。DATE_TRUNC の単位には 'year'/'month'/'week'/'day'/'hour' 等が使えます。TO_CHAR(DATE_TRUNC(...), 'YYYY-MM') で文字列化、または ::DATE でキャストするのが実務の定番パターンです。orders テーブルには TIMESTAMP 型の ordered_at 列があります。次の3列を追加して取得してください。①ordered_at_jp: 日本語日付形式 ('YYYY年MM月DD日')、②ordered_time: 時刻部分 ('HH24:MI')、③order_month: 月次集計キー ('YYYY-MM' 形式)。order_id 昇順で出力してください。
| order_id | amount | ordered_at (TIMESTAMP) |
|---|---|---|
| 1 | 8000 | 2024-05-01 09:15:00 |
| 2 | 12500 | 2024-05-15 14:30:00 |
| 3 | 3200 | 2024-06-02 10:00:00 |
| 4 | 6700 | 2024-06-18 16:45:00 |
| 5 | 450 | 2024-06-30 23:59:00 |
| order_id | amount | ordered_at_jp | ordered_time | order_month |
|---|---|---|---|---|
| 1 | 8000 | 2024年05月01日 | 09:15 | 2024-05 |
| 2 | 12500 | 2024年05月15日 | 14:30 | 2024-05 |
| 3 | 3200 | 2024年06月02日 | 10:00 | 2024-06 |
| 4 | 6700 | 2024年06月18日 | 16:45 | 2024-06 |
| 5 | 450 | 2024年06月30日 | 23:59 | 2024-06 |
SELECT order_id, amount, TO_CHAR(ordered_at, 'YYYY年MM月DD日') AS ordered_at_jp, -- TIMESTAMPを日本語日付文字列に変換 TO_CHAR(ordered_at, 'HH24:MI') AS ordered_time, -- 'HH24'=24時間制の時、'MI'=分 TO_CHAR( DATE_TRUNC('month', ordered_at), -- 月の先頭(1日 00:00:00)に切り捨て 'YYYY-MM' -- 月次キー文字列に変換(GROUP BYで使用可能) ) AS order_month FROM orders ORDER BY order_id; /* 実行順序(SQLの論理的な評価順): 1. FROM orders → 全5行のTIMESTAMPデータを読み込む 2. TO_CHAR(ordered_at, 'YYYY年MM月DD日') → 'YYYY' 3. TO_CHAR(ordered_at, 'HH24:MI') → 時刻部分を24時間制で抽出 4. DATE_TRUNC('month', ordered_at) 5. ORDER BY order_id → order_id 昇順で出力 */
LEGEND
① FROM — タイムスタンプを含む元データ
FROM ordersordered_at が TIMESTAMP 型です。表示・集計に使いやすい文字列形式に変換します。| order_id | amount | ordered_at (TIMESTAMP) |
|---|---|---|
| 1 | 8000 | 2024-05-01 09:15:00 |
| 2 | 12500 | 2024-05-15 14:30:00 |
| 3 | 3200 | 2024-06-02 10:00:00 |
| 4 | 6700 | 2024-06-18 16:45:00 |
| 5 | 450 | 2024-06-30 23:59:00 |
LEGEND
① FROM
FROM orders5月の注文だけを絞り込みたい場面です。TO_CHAR で変換した文字列に WHERE をかけると何が問題になるでしょうか?| order_id | ordered_at |
|---|---|
| 1 | 2024-05-01 09:15:00 |
| 2 | 2024-05-15 14:30:00 |
| 3 | 2024-06-02 10:00:00 |
| 4 | 2024-06-18 16:45:00 |
| 5 | 2024-06-30 23:59:00 |
'YYYY'=4桁年、'MM'=2桁月(0埋め)、'DD'=2桁日、'HH24'=24時間制の時、'MI'=分、'SS'=秒。組み合わせて 'YYYY/MM/DD HH24:MI:SS' のように使えます。GROUP BY DATE_TRUNC('month', ordered_at) で月ごと、DATE_TRUNC('week', ...) で週次集計が実現できます。ダッシュボードのグラフ用集計クエリで頻出のパターンです。EXTRACT(DOW FROM ordered_at) は日=0、月=1…土=6 の整数を返します。曜日を日本語化するには CASE WHEN EXTRACT(DOW FROM ts) = 0 THEN '日' WHEN 1 THEN '月' ... が必要です。WHERE TO_CHAR(ordered_at,'YYYY-MM')='2024-05' は結果は正しいですがインデックスが使えません。日付フィルタは必ず >= / < による範囲比較で書きましょう。DATE_TRUNC('month', ts)::DATE と明示的にキャストしないと、DATE 型の列との JOIN やフィルタで型ミスマッチエラーが起きることがあります。GROUP BY DATE_TRUNC($granularity, ordered_at) のように粒度をパラメータ化すれば、1クエリで全粒度に対応できます。フロントエンドへの表示ラベルは TO_CHAR で生成し、グラフ横軸のキーとして使うのが一般的なパターンです。タイムゾーンが必要な場合は ordered_at AT TIME ZONE 'Asia/Tokyo' を事前に適用してから DATE_TRUNC します。SPLIT_PART / REGEXP_REPLACE — メールアドレス分解と電話番号正規化
SPLIT_PART(文字列, 区切り文字, フィールド番号) は文字列を区切り文字で分割し、n番目のフィールドを返します。フィールド番号は1始まりです。REGEXP_REPLACE(文字列, パターン, 置換後, フラグ) は正規表現にマッチした箇所を別の文字に置き換えます。
SPLIT_PART('alice@example.com', '@', 1) -- → 'alice' (1番目) SPLIT_PART('alice@example.com', '@', 2) -- → 'example.com' (2番目) SPLIT_PART('a.b.c', '.', 2) -- → 'b' REGEXP_REPLACE('090-1234-5678', '-', '', 'g') -- → '09012345678' ('g'=全件置換) REGEXP_REPLACE('090-1234-5678', '-', '') -- → '0901234-5678' (フラグ省略=初回のみ)
'g'=global(全件置換)、'i'=case-insensitive(大文字小文字無視)、'gi'=両方。フラグを省略すると最初の1件しか置換されません。電話番号のハイフン除去など全件置換が必要なときは 'g' が必須です。contacts テーブルの email を SPLIT_PART で分解し email_user(@左側)と email_domain(@右側)を、phone のハイフンを REGEXP_REPLACE で全て除去した phone_normalized を取得してください。id 昇順で出力してください。
| id | phone | |
|---|---|---|
| 1 | alice@example.com | 090-1234-5678 |
| 2 | bob.smith@gmail.com | 03-5678-9012 |
| 3 | carol@company.co.jp | 06-1234-5678 |
| id | email_user | email_domain | phone_normalized |
|---|---|---|---|
| 1 | alice | example.com | 09012345678 |
| 2 | bob.smith | gmail.com | 0356789012 |
| 3 | carol | company.co.jp | 0612345678 |
SELECT id, SPLIT_PART(email, '@', 1) AS email_user, -- '@'で分割し1番目(ユーザー名)を取得 SPLIT_PART(email, '@', 2) AS email_domain, -- '@'で分割し2番目(ドメイン)を取得 REGEXP_REPLACE(phone, '-', '', 'g') AS phone_normalized -- '-'を全て空文字に置換('g'=全件) FROM contacts ORDER BY id; /* 実行順序(SQLの論理的な評価順): 1. FROM contacts → 全3行を読み込む 2. SPLIT_PART(email, '@', 1) → '@' で分割し1番目フィールド(左側)を返す 3. REGEXP_REPLACE(phone, '-', '', 'g') → '-' にマッチした全箇所を '' に置換 4. ORDER BY id → id 昇順で出力 */
LEGEND
① FROM — メール・電話番号を含む元データ
FROM contactsemail は '@' の左右に構造があり、phone はハイフン区切りです。それぞれ SPLIT_PART と REGEXP_REPLACE で処理します。| id | phone | |
|---|---|---|
| 1 | alice@example.com | 090-1234-5678 |
| 2 | bob.smith@gmail.com | 03-5678-9012 |
| 3 | carol@company.co.jp | 06-1234-5678 |
LEGEND
① FROM
FROM contacts@ より前のユーザー名を取得する際、POSITION + SUBSTRING で書こうとするとどうなるでしょうか?| id | |
|---|---|
| 1 | alice@example.com |
| 2 | bob.smith@gmail.com |
| 3 | no-at-sign-here |
'a/b/c/d') から特定フィールドを取得するのに最適です。SPLIT_PART('a/b/c', '/', 3) → 'c'。存在しないフィールドは空文字で安全に返します。REGEXP_REPLACE(phone, '[^0-9]', '', 'g') で「数字以外を全て除去」できます。ハイフン・カッコ・空白が混在した電話番号でも1回の REGEXP_REPLACE で正規化可能です。~=パターンマッチ、!~=非マッチ、~*=大文字小文字無視マッチ。WHERE email ~ '@gmail\.com$' でGmailユーザーをフィルタできます。'090-1234-5678' は '0901234-5678' になり後半のハイフンが残ります。全件置換が必要なときは必ず 'g' を指定しましょう。POSITION が 0 を返し SUBSTRING(s, 1, -1) になりエラーが発生します。SPLIT_PART のほうがシンプルかつ堅牢です。REGEXP_REPLACE / SPLIT_PART は特に威力を発揮します。新規設計ではアプリ側バリデーション → 正規化済みをDB保存 → SELECTで変換不要、という流れが保守コストを最小化します。ROUND / CEIL + CASE WHEN — 軽減税率に対応した税込価格計算と価格帯分類
ROUND(数値, 小数点以下桁数) は指定桁数で四捨五入します。桁数を省略すると整数に丸めます。CEIL(数値) は切り上げ、FLOOR(数値) は切り捨てです。
ROUND(734.4) -- → 734 (四捨五入、桁数省略=整数) ROUND(734.5) -- → 735 (0.5は切り上げ) CEIL(734.1) -- → 735 (小数があれば必ず+1) FLOOR(734.9) -- → 734 (小数を常に切り捨て) ROUND(9.999, 2) -- → 10.00 (小数第2位で四捨五入)
1 / 3 は 0 になります(整数÷整数=整数)。小数点以下を得るには 1::NUMERIC / 3 または 1.0 / 3 のように一方を NUMERIC に変換する必要があります。ROUND を使う前に型を確認しましょう。ROUND(price * (1 + CASE ... END)) のように計算式の中に CASE 式をインライン展開できます。サブクエリなしで1行で税込計算が完結します。products テーブルから、category が 'food' の商品には軽減税率8%、それ以外には10%を適用した税込価格 (price_with_tax、ROUND で整数化)と、price の価格帯ランク (price_rank: 3000円以上='premium' / 1000円以上='standard' / それ未満='budget') を取得してください。product_id 昇順で出力してください。
| product_id | name | price | category |
|---|---|---|---|
| 1 | コーヒー豆 | 1800 | food |
| 2 | マグカップ | 2200 | goods |
| 3 | ケーキ | 680 | food |
| 4 | ギフトセット | 5400 | gift |
| 5 | エコバッグ | 980 | goods |
| product_id | name | price | tax_rate | price_with_tax | price_rank |
|---|---|---|---|---|---|
| 1 | コーヒー豆 | 1800 | 0.08 | 1944 | standard |
| 2 | マグカップ | 2200 | 0.10 | 2420 | standard |
| 3 | ケーキ | 680 | 0.08 | 734 | budget |
| 4 | ギフトセット | 5400 | 0.10 | 5940 | premium |
| 5 | エコバッグ | 980 | 0.10 | 1078 | budget |
SELECT product_id, name, price, CASE category -- 単純CASE:カテゴリで税率を決定 WHEN 'food' THEN 0.08 -- 食品は軽減税率8% ELSE 0.10 -- それ以外は標準税率10% END AS tax_rate, ROUND( -- 小数点以下を四捨五入して整数に price * (1 + CASE category -- CASE WHENを計算式の中にインライン展開 WHEN 'food' THEN 0.08 ELSE 0.10 END) ) AS price_with_tax, CASE -- 検索CASE:税抜き価格で帯域を判定 WHEN price >= 3000 THEN 'premium' -- 上位条件を先に書く(検索CASEの鉄則) WHEN price >= 1000 THEN 'standard' ELSE 'budget' END AS price_rank FROM products ORDER BY product_id; /* 実行順序(SQLの論理的な評価順): 1. FROM products → 全5行を読み込む 2. CASE category → カテゴリで税率を決定 3. price * (1 + 税率) / ROUND → 税込金額を計算し四捨五入 4. CASE WHEN price … → 価格でランク付け 5. ORDER BY product_id → 昇順で出力 */
LEGEND
① FROM — 元データ読み込み
FROM productscategory によって適用する税率が異なります。CASE WHEN で税率を動的に決定し、ROUND で税込価格を整数化します。| product_id | name | price | category |
|---|---|---|---|
| 1 | コーヒー豆 | 1800 | food |
| 2 | マグカップ | 2200 | goods |
| 3 | ケーキ | 680 | food |
| 4 | ギフトセット | 5400 | gift |
| 5 | エコバッグ | 980 | goods |
LEGEND
① 端数が発生する食品の例
food カテゴリ (tax=8%)食品に8%の税率を掛けると端数が出やすくなります。ROUND・CEIL・FLOORのどれを選ぶかで金額が変わります。仕様書で端数処理ルールを必ず確認しましょう。| product_id | name | price | 税込(生の値) |
|---|---|---|---|
| 1 | コーヒー豆 | 1800 | 1800×1.08=1944.0 |
| 3 | ケーキ | 680 | 680×1.08=734.4 |
| 6 | チョコレート | 150 | 150×1.08=162.0 |
ROUND(price * (1 + CASE category WHEN 'food' THEN 0.08 ELSE 0.10 END)) のように計算式の中に埋め込めるため、サブクエリなしで1行で税込計算が完結します。INTEGER * NUMERIC の結果は NUMERIC になります。0.08 は NUMERIC リテラルなので 1800 * 0.08 = 144.00 (NUMERIC) となり精度は保たれます。一方 1800 * 8 / 100 は INTEGER 演算で 144 (INTEGER) になり小数が消えます。8 / 100 * price は PostgreSQL では 0 * price = 0 になります(整数÷整数=整数)。必ず 0.08(NUMERIC リテラル)か 8::NUMERIC / 100 で書きましょう。WHEN price >= 1000 THEN 'standard' を先に書くと、5400も 'standard' に分類されます。検索CASEは必ず上位の条件(より厳しい条件)を先に書きましょう。CASE category WHEN 'food' THEN 0.08 ELSE 0.10 END のハードコードは設計初期段階では有効ですが、税率マスタ管理は後続フェーズで検討する価値があります。COALESCE + NULLIF + GROUP BY — NULL が混在するデータを安全に集計する
基礎編の COALESCE / NULLIF を GROUP BY 集計と組み合わせる応用パターンです。SUM / AVG はグループ内の NULL を自動で無視しますが、グループ全体が NULL の場合は SUM 自体が NULL を返します。また NULLIF はゼロ除算防止パターンとして集計クエリで特に頻出します。
SUM(points) -- NULL行を無視して合計(全行NULLならNULLを返す) COALESCE(SUM(points), 0) -- SUM がNULL(全行NULL)なら 0 に置換 SUM(CASE WHEN completed THEN 1 ELSE 0 END) -- 条件付きカウント numerator / NULLIF(denominator, 0) -- 分母が0のときNULLを返しゼロ除算を防ぐ
AVG に (0, NULL, 10) を渡すと (0+10)/2 = 5 になります。NULL を 0 として平均に含めたい場合は AVG(COALESCE(col, 0)) と書きます。どちらが意図どおりかを常に確認しましょう。2 / 3 は 0 になります。完了率など小数が必要な計算では、分子または分母を ::NUMERIC に変換してから割り算してください。tasks テーブルを project_id でグループ化し、①task_count (全タスク数)、②completed_count (完了タスク数)、③total_points (NULLは0として合算)、④completion_rate (完了率%、ROUNDで整数化・ゼロ除算防止) を求めてください。project_id 昇順で出力してください。
| task_id | project_id | points | completed |
|---|---|---|---|
| 1 | A | 10 | true |
| 2 | A | NULL | false |
| 3 | A | 5 | true |
| 4 | B | 8 | false |
| 5 | B | NULL | false |
| 6 | C | 3 | true |
| project_id | task_count | completed_count | total_points | completion_rate |
|---|---|---|---|---|
| A | 3 | 2 | 15 | 67 |
| B | 2 | 0 | 8 | 0 |
| C | 1 | 1 | 3 | 100 |
SELECT project_id, COUNT(*) AS task_count, SUM(CASE WHEN completed THEN 1 ELSE 0 END) AS completed_count, -- 完了タスクだけ1を加算 COALESCE(SUM(points), 0) AS total_points, -- NULL行は無視、全行NULLなら0 ROUND( SUM(CASE WHEN completed THEN 1 ELSE 0 END)::NUMERIC -- INTEGER→NUMERIC変換で小数演算を確保 / NULLIF(COUNT(*), 0) * 100 -- COUNT=0のときNULLを返しゼロ除算防止 ) AS completion_rate FROM tasks GROUP BY project_id ORDER BY project_id; /* 実行順序(SQLの論理的な評価順): 1. FROM tasks → 全6行を読み込む 2. GROUP BY project_id 3. COUNT(*) → グループ内の全行数 (NULLも含む) 4. SUM(CASE WHEN completed THEN 1 ELSE 0 END) → true行のみ1を加算 5. COALESCE(SUM(points), 0) → NULL行は SUM が自動で無視 6. SUM(...)::NUMERIC / NULLIF(COUNT(*), 0) * 100 → 完了率を小数精度で計算 7. ROUND(...) → 小数点以下を四捨五入して整数 8. ORDER BY project_id → project_id 昇順で出力 */
LEGEND
① FROM — NULL と BOOLEAN を含む元データ
FROM taskspoints に NULL が含まれます。completed は BOOLEAN です。GROUP BY で集計する前に、NULL の扱いを把握しておくことが重要です。| task_id | project_id | points | completed |
|---|---|---|---|
| 1 | A | 10 | true |
| 2 | A | NULL | false |
| 3 | A | 5 | true |
| 4 | B | 8 | false |
| 5 | B | NULL | false |
| 6 | C | 3 | true |
LEGEND
① 集計後 — 完了が0件のグループが存在
GROUP BY 後の仮想集計結果プロジェクト B の completed_count が 0 です。この値を分母にした計算を試みるとどうなるでしょうか?| project_id | total_points | completed_count |
|---|---|---|
| A | 15 | 2 |
| B | 8 | 0 |
| C | 3 | 1 |
SUM(CASE WHEN completed THEN 1 ELSE 0 END) はSQL標準で移植性が高い書き方です。PostgreSQL では COUNT(*) FILTER (WHERE completed) というより簡潔な書き方も使えます(PostgreSQL 9.4以降)。COUNT(*) は NULL を含む全行を数えます。COUNT(points) は NULL でない行だけを数えます。タスク総数には COUNT(*)、有効ポイント数には COUNT(points) と使い分けましょう。/ NULLIF(denominator, 0) を必ず使いましょう。WHERE で除外していても新しいデータで発生する可能性があります。防衛的に書く癖が重要です。SUM(...) / COUNT(*) は整数同士の演算で小数が消えます。2 / 3 は 0 です。::NUMERIC または * 1.0 で一方を NUMERIC に変換してから割り算しましょう。複合変換 — TO_CHAR / LPAD / ROUND / CASE WHEN を組み合わせて請求書ラベルを一括生成する
実務では複数の変換関数をネストして使うことが多いです。この問題ではこれまで学んだ関数を組み合わせて、請求書に必要な情報を1クエリで整形します。
-- 関数のネスト: 内側から外側へ順に適用される 'INV-' || LPAD(invoice_id::TEXT, 6, '0') -- → 'INV-000001' TO_CHAR(ROUND(amount * 1.1), 'FM999,999,999') -- → '8,800' (FM=先頭スペース除去) COALESCE(TO_CHAR(paid_at, 'YYYY-MM-DD'), '未払い') -- → 日付 or '未払い'
TO_CHAR(1234, '999,999') は ' 1,234' のように先頭にスペースが入ります。'FM999,999' と FM を付けることで先頭スペースを除去できます(FM = Fill Mode)。数値を文字列として表示する際に重要です。invoice_id::TEXT と明示的に変換しましょう。invoices テーブルから以下の列を生成してください。①invoice_no: 'INV-' + 6桁ゼロ埋めのID(例: 'INV-000001')、②amount_with_tax: 税込金額(10%・ROUND・カンマ区切り書式)、③issued_at_jp: 発行日の日本語表示、④paid_status: paid_at が非NULLなら支払日、NULLなら '未払い'、⑤payment_label: CASE WHENで支払区分ラベル('paid'→'支払済'、'pending'→'支払待ち'、'overdue'→'延滞')。invoice_id 昇順で出力してください。
| invoice_id | amount | issued_at | paid_at | status |
|---|---|---|---|---|
| 1 | 8000 | 2024-05-01 | 2024-05-10 | paid |
| 2 | 125000 | 2024-05-15 | NULL | pending |
| 3 | 3200 | 2024-06-02 | NULL | overdue |
| 42 | 6700 | 2024-06-18 | 2024-06-25 | paid |
| invoice_no | amount_with_tax | issued_at_jp | paid_status | payment_label |
|---|---|---|---|---|
| INV-000001 | 8,800 | 2024年05月01日 | 2024-05-10 | 支払済 |
| INV-000002 | 137,500 | 2024年05月15日 | 未払い | 支払待ち |
| INV-000003 | 3,520 | 2024年06月02日 | 未払い | 延滞 |
| INV-000042 | 7,370 | 2024年06月18日 | 2024-06-25 | 支払済 |
SELECT 'INV-' || LPAD(invoice_id::TEXT, 6, '0') AS invoice_no, -- ①整数をTEXTにキャスト ②6桁0埋め ③プレフィックス結合 TO_CHAR( ROUND(amount * 1.1), -- 税込金額を四捨五入して整数化 'FM999,999,999' -- FM=先頭スペース除去、カンマ区切り書式 ) AS amount_with_tax, TO_CHAR(issued_at, 'YYYY年MM月DD日') AS issued_at_jp, -- 日付を日本語形式に変換 COALESCE( TO_CHAR(paid_at, 'YYYY-MM-DD'), -- paid_at が非NULLなら日付文字列に変換 '未払い' -- paid_at が NULL なら '未払い' ) AS paid_status, CASE status -- 単純CASE:英語ステータスを日本語ラベルに変換 WHEN 'paid' THEN '支払済' WHEN 'pending' THEN '支払待ち' WHEN 'overdue' THEN '延滞' ELSE '不明' -- 想定外の値に備えてELSEを必ず書く END AS payment_label FROM invoices ORDER BY invoice_id; /* 実行順序(SQLの論理的な評価順): 1. FROM invoices → 全4行を読み込む 2. invoice_id::TEXT → 整数をTEXTにキャスト(LPAD引数の型合わせ) 3. amount * 1.1 → 税込金額を計算 (INTEGER×NUMERIC 4. TO_CHAR(issued_at, 'YYYY年MM月DD日') → DATE型を日本語文字列に変換 5. TO_CHAR(paid_at, 'YYYY-MM-DD') → paid_atが非NULLなら日付文字列に変換 6. CASE status WHEN 'paid' THEN '支払済' ... → ステータスコードを日本語に変換 7. ORDER BY invoice_id → invoice_id 昇順で出力 */
LEGEND
① FROM — 請求書の元データ
FROM invoicesinvoice_id は整数、issued_at は DATE 型、paid_at は NULL を含む DATE 型、status は英語コードです。これらを複数の変換関数で帳票表示用に整形します。| invoice_id | amount | issued_at | paid_at | status |
|---|---|---|---|---|
| 1 | 8000 | 2024-05-01 | 2024-05-10 | paid |
| 2 | 125000 | 2024-05-15 | NULL | pending |
| 3 | 3200 | 2024-06-02 | NULL | overdue |
| 42 | 6700 | 2024-06-18 | 2024-06-25 | paid |
LEGEND
① 数値フォーマットの確認
FROM invoices (amount × 1.1 の整数値)ROUND(amount * 1.1) で整数化した税込金額に TO_CHAR でカンマ区切り書式を適用します。FM フラグの有無で出力が変わります。| invoice_id | ROUND(amount*1.1) |
|---|---|
| 1 | 8800 |
| 2 | 137500 |
| 3 | 3520 |
| 42 | 7370 |
FM999,999,999 と FM を付けると先頭スペースを除去できます。また FM は末尾の余分なゼロも除去します。TO_CHAR(1.50, 'FM0.99') → '1.5'。帳票やCSV出力では FM を付けることを習慣にしましょう。LPAD(id::TEXT, 10, '0') のように余裕を持った桁数にするか、LPAD を使わずアプリ側でフォーマットする設計も検討しましょう。LPAD(invoice_id, 6, '0') は PostgreSQL でエラーになります。必ず invoice_id::TEXT で TEXT にキャストしてから渡しましょう。TO_CHAR(8800, '999,999,999') は ' 8,800' と先頭にスペースが入ります。CSVエクスポートで文字列として出力される場合、このスペースが文字列比較や表示ズレを引き起こします。カンマ区切り書式では FM を付けましょう。