データアナリスト面接:SQLの結果から事業判断まで説明する
Quick Overview
注文取消率をSQLで検算し、観測期間・明細JOIN・NULL理由・金額の占有率を分けて、事業側へ説明する練習です。
データアナリストの面接でSQLの結果を説明するときは、「33.33%でした」に加えて、何を数えた結果かを説明する必要があります。何を一件と数え、どこまで観測し、その結果から何を変えるのか。数字が計算できても、この三点が曖昧だと、事業側は次に何をするか判断できません。
この記事では、架空の通販の注文取消率を使い、分母の定義、商品明細のJOIN、理由の欠損、件数と金額の違いを追います。14件の注文と17行の明細をPostgreSQL 18.6で集計したオリジナル練習で、企業の実際の出題、候補者報告、実サービスの分析結果ではありません。公式のSQL仕様と実行結果、そこから提案する調査を分けます。PracHubの取消率SQL問題と組み合わせ、結果を短い業務メモへ直す練習に使ってください。

「取消率が下がった」の定義を確認する
担当者から「前期33.33%、今期25%なので改善しましたね」と言われたとします。最初の仕事は、すぐ賛成することでも、SQLを書き直すことでもなく、二つの数字が何を数えたかを確認することです。注文を作成した期間でまとめたのか、取消が記録された期間でまとめたのかで、分母と分子の対象が変わります。
この練習では、注文作成時刻で二つの期間へ分けます。前期は10月1日00:00以上・10月8日00:00未満、今期は10月8日00:00以上・10月15日00:00未満です。すべてUTC。観測基準は10月15日00:00に固定し、作成から168時間の観測が完了した注文だけを比較します。
取消は、作成時刻以上・作成から168時間未満に記録されたものとします。168時間ちょうどの取消は含めず、観測が完了したかの判定では168時間ちょうどを含めます。これはこの課題の合意した定義で、通販全般に共通する正解ではありません。部分取消、返品、テスト注文、支払失敗は扱わず、一注文に取消時刻は最大一つとします。
注文金額も、単一通貨の円建てで与えられた正の整数です。税込・送料込みか、値引きや返金をどの時点で反映するかは実務で別途確認します。本ケースでは、与えた注文金額の占有率だけを計算し、それを会計上の売上や利益と呼びません。
注文一行と明細一行の違いを表にする
orders は order_id を主キーに、一注文一行を持ちます。列は作成時刻 created_at、取消時刻 cancelled_at、取消理由 cancel_reason、注文金額 amount_yen です。order_items は注文IDと明細番号の組み合わせを主キーに、一商品明細一行を持ちます。
データの全体を、同じ条件の注文はまとめて示すと次のようになります。まとめた行でも、金額は一注文当たりの値です。時刻はすべて00:00 UTCで、前期の取消P1は10月2日、P2は10月3日、今期のC1は10月9日、C2は10月10日です。
| 注文ID | 作成日 | 注文数 | 一注文の金額 | 取消 | 明細数 |
|---|---|---|---|---|---|
| P1・P2 | 10月1日 | 2 | 各1,000円 | あり | 各1行 |
| P3〜P6 | 10月1日 | 4 | 各1,000円 | なし | 各1行 |
| C1 | 10月8日 | 1 | 8,000円 | あり | 4行、各2,000円 |
| C2 | 10月8日 | 1 | 2,000円 | あり | 1行 |
| C3〜C6 | 10月8日 | 4 | 各1,000円 | なし | 各1行 |
| C7・C8 | 10月12日・13日 | 2 | 各1,000円 | 現時点でなし | 各1行 |
C7・C8は取消がないように見えますが、基準時刻までに168時間たっていません。これから取消が起きる可能性のある注文を、観測が終わった注文と同じ扱いで分母へ入れると、今期を有利に見せることがあります。
「取消なし」は「配送完了」とも違います。このデータには配送完了時刻を持たせていないため、今期の取消記録がない六件がすべて配達済み、とは言えません。また、ここで数えるのは注文であり、購入者数や顧客の離脱率ではありません。
同じ観測期間に揃えると、25%は33.33%になる
単純に今期の全注文を数えると、取消2件を8件で割って25%になります。観測が完了した注文へ絞ると、対象はC1〜C6の6件で、取消は同じ2件。2/6 は約33.33%です。前期も観測が完了した6件中2件なので、この表での件数率は同じで、速報の25%を改善の根拠にはできません。
これは「実際の取消率が永久に同じ」という証拠ではありません。小さな架空データで、観測期間を揃えた場合の計算を示しただけです。実データなら、件数、対象の構成、集計期間、取り込み漏れ、変更前後の他の条件も調べます。
時刻の扱いにも注意します。公式仕様:PostgreSQLの日時型に沿って、この練習ではタイムゾーン付きの timestamptz とUTCオフセットを明示しています。168時間という経過時間を揃えるため、SQLでは INTERVAL '168 hours' を使います。業務上の「現地日付で七日」と同じ定義にするとは限りません。
全注文を分母にした速報が必要なら、それ自体を禁止する必要はありません。ただし「観測未完了2件を含む暫定値」と表示し、同じ条件の確定比較と分けます。速報の25%だけを、前期の確定値33.33%と並べて改善の根拠にしない、という整理です。
明細JOINの後に数えると、55.56%になる
商品別の情報を見たくなって、注文と明細を結合したとします。C1だけは四つの明細を持つため、観測が完了した今期の6注文が9行になります。取消されたC1の4行とC2の1行を数えると、取消行は5。5/9 は約55.56%です。
公式仕様:PostgreSQLの結合の説明では、結合条件に合う組み合わせが結果行になります。一対多の結合後も一注文一行だと思って数えると、明細の多い注文が強く重み付けされます。この練習で得た55.56%は、定義した注文取消率ではありません。
次は実際に実行した、比較用の誤集計です。flags は注文ごとの期間、観測完了 mature、168時間内取消 cancelled_7d を保持するビューで、一注文一行です。結合するとその単位が崩れます。
SELECT count(*) AS joined_rows,
count(*) FILTER (WHERE f.cancelled_7d) AS cancelled_rows,
round(100.0 * count(*) FILTER (WHERE f.cancelled_7d) / count(*), 2) AS pct
FROM flags f JOIN order_items i USING (order_id)
WHERE f.cohort = 'current' AND f.mature;
実行結果は joined_rows=9, cancelled_rows=5, pct=55.56 でした。件数だけなら注文IDのdistinctで戻せる場合がありますが、注文金額の合計も同時に直ったとは限りません。C1の8,000円を四行に繰り返して足すと、今期の注文総額は本来14,000円のところ38,000円になります。
sum(DISTINCT amount_yen) も解決になりません。同じ1,000円の別注文まで一つへまとめ、本ケースでは11,000円になります。注文単位の集計には注文表を使い、商品情報が必要なときは明細を適切な単位へ集約してから結合します。C6の明細を取り除く対照チェックでも、注文表から作る集計は変わらないことを確認しました。
取消の事実と理由の記録を分けて集計する
C1の取消理由は carrier_delay、C2はNULLです。C2には取消時刻があるため、理由がなくても取消2件のうち一件として数えます。NULLの理由を「取消されていない」と読み替えると、分子が変わってしまいます。
公式仕様:PostgreSQLの集約関数では、count(*) は入力行数、count(列) はその列がNULLでない行数を数えます。このケースで観測が完了した今期の取消注文に対して count(cancel_reason) を使うと1、count(*) なら2です。前者が答えるのは、理由が記録されている取消の件数です。
ここでは、取消2件、理由が分かるもの1件、理由が未記録のもの1件と分けて報告します。理由の記録率は 1/2=50% ですが、それは注文取消率33.33%とは別の指標です。C2の理由を、C1と同じ配送遅延だったと補ってはいけません。
取消理由は、顧客が選んだ選択肢なのか、担当者が後から付けた分類なのかでも意味が違います。集計のたびに分類が変わるなら、いつの値を使ったかを記録します。本ケースは固定データなので、履歴の修正や取り込み時点の再現まで実装したわけではありません。
一注文一行のまま件数と金額を計算する
次のSQLは明細表を使わず、観測が完了した注文を選んでから集計します。取消条件を満たさないNULLはfalseへ寄せ、理由の未記録は別に数えます。観測完了の条件があるため、取消の上限である作成後168時間も観測基準時刻以前になります。
WITH cohort_orders AS (
SELECT *,
CASE WHEN created_at < TIMESTAMPTZ '2026-10-08 00:00+00'
THEN 'previous' ELSE 'current' END AS cohort,
COALESCE(cancelled_at >= created_at
AND cancelled_at < created_at + INTERVAL '168 hours', false) AS cancelled_7d
FROM orders
WHERE created_at >= TIMESTAMPTZ '2026-10-01 00:00+00'
AND created_at < TIMESTAMPTZ '2026-10-15 00:00+00'
AND created_at + INTERVAL '168 hours' <= TIMESTAMPTZ '2026-10-15 00:00+00'
), totals AS (
SELECT cohort,
count(*) AS orders_n,
count(*) FILTER (WHERE cancelled_7d) AS cancelled_n,
sum(amount_yen) AS amount_yen,
COALESCE(sum(amount_yen) FILTER (WHERE cancelled_7d), 0) AS cancelled_yen,
count(*) FILTER (WHERE cancelled_7d AND cancel_reason IS NULL) AS unknown_reason_n
FROM cohort_orders
GROUP BY cohort
)
SELECT cohort, orders_n, cancelled_n,
round(100.0 * cancelled_n / NULLIF(orders_n, 0), 2) AS cancelled_pct,
amount_yen, cancelled_yen,
round(100.0 * cancelled_yen / NULLIF(amount_yen, 0), 2) AS cancelled_amount_pct,
unknown_reason_n
FROM totals
ORDER BY cohort;
公式仕様:集約のFILTERは、指定条件を満たす行をその集約へ渡す構文です。ここでは分母の注文行を残し、取消した件数や金額だけを条件付きで数えています。全体のWHEREで取消注文だけへ絞ると、取消されていない注文が分母から消えます。
NULLIFの公式説明に沿い、分母ゼロはNULLにして除算を避けます。対象がないときに率0%と表示すると「観測したが取消ゼロ」と混ざるためです。ただし、このSQLはGROUP BYなので、対象のない期間は行自体が出ません。空期間も表示したいダッシュボードなら期間一覧を別に作り、外部結合で件数0・率NULLを表示する設計が必要です。
100.0 を掛けるのは整数同士の除算を避けるためです。実行した 2/6 の整数除算は0でした。丸めは表示のために行い、元の件数や金額を残しておくと、率だけを見て誤解するのを防げます。
件数率が同じでも、金額の占有率は違う
上のSQLが返した件数と金額を、日本語の列で示すと次の通りです。SQLは文字列順で今期、前期の順に返しますが、ここでは比較しやすいよう前期を先に示します。
| 期間 | 対象注文 | 取消注文 | 件数率 | 全注文金額 | 取消注文金額 | 金額の占有率 | 理由未記録 |
|---|---|---|---|---|---|---|---|
| 前期 | 6 | 2 | 33.33% | 6,000円 | 2,000円 | 33.33% | 0 |
| 今期 | 6 | 2 | 33.33% | 14,000円 | 10,000円 | 71.43% | 1 |
今期は、取消されたC1が8,000円、C2が2,000円です。件数では六件中二件でも、注文金額では14,000円中10,000円を占めます。二つの率は同じ問いへ答えているわけではありません。前者は注文のうち何件が取消されたか、後者は注文金額のうち取消注文がどれだけを占めたかです。
ただし、10,000円を「失った利益」や「返金額」とは呼べません。決済や返金、原価、代替購入のデータを持たないためです。この二件の取消注文の金額は、確認の優先順位を考える材料にはなりますが、その対応でどれだけ利益を回復できるかは別に検証します。
本ケースでの提案は、まず金額が大きいC1の配送記録と理由の付け方を確認し、C2は理由を取得できていない原因を調べることです。二件から全利用者への配送施策や購入制限を決めるのではなく、どの仮説をどの追加データで確かめるかを示します。

事業側への報告は、結論と留保を一緒に伝える
業務メモは、SQLの構文説明から始める必要はありません。「同じ観測条件では件数率の改善を確認できない。金額の大きい取消と理由の欠損を先に調べたい」と述べ、その根拠を示します。たとえば、次のように説明できます。
「速報の今期25%には、作成から168時間たっていない二件が含まれていました。観測が完了した注文へ揃えると、前期・今期とも六件中二件、33.33%です。一方、今期は取消注文の金額が全体の71.43%を占めます。8,000円のC1は配送記録を確認し、2,000円のC2は理由が未記録なので取得状況を調べます。件数が少なく、この結果だけで施策の効果や将来の率は判断しません」。
このメモでの判断は、改善施策を断定することではなく、次の調査へ優先順位を付けることです。調査の担当、必要なデータ、確認できたら何を決めるかを追加すれば、相手が調査を始めるための情報が揃います。実務の担当者や期限は、本ケースから勝手に設定せず、関係者と合意します。
面接官に「では配送を早めれば解決しますか」と聞かれても、C1の理由欄だけで因果関係を断定しません。配送予定と実績、取消までの経緯、同じ条件の非取消注文、理由の入力方法を確認します。施策の効果を測りたい場合は、対象条件と評価期間を定め、可能な比較方法や実験の実施条件を検討すると説明できます。本記事ではその実験は行っていません。
チェックで確認したことと、未確認のことを示す
PostgreSQL 18.6で、24個のチェックを実行しました。注文14件・明細17行、観測完了の対象、誤った25%と55.56%、正しい集計表、NULL理由、金額の重複、ゼロ分母、168時間ちょうどと直前の取消、作成前の不正な取消時刻、明細のない注文などを確かめています。タイムゾーン設定をAsia/Tokyoへ変えても、UTCを明示した結果は同じでした。
チェックには、誤集計が予想通りの値を返すことを確認するものも含めます。24個通ったから誤ったJOINも正しい、という意味ではありません。すべて一時テーブル内の合成データで、終了時にトランザクションをロールバックしています。実注文や本番ダッシュボードを操作したわけではありません。
実務なら、取消ログの取り込み完了、注文IDの重複や更新履歴、部分取消、金額定義、通貨、集計結果と元システムの照合を追加します。固定データのSQLが正しいことと、入力データが揃っていることは別です。性能についても、本記事はインデックス設計や大規模データでの実行時間を測っていません。
自分の経験を話すときは、作ったSQL、定義を合意した指標、担当したダッシュボード、事業側が取った行動を分けてください。「取消率を改善した」と主張するなら、計算の修正だけなのか、施策による変化を確かめたのかを説明します。チーム全体の成果から、自分が確認・実装した部分を具体的に切り出しましょう。
次の五題で計算から提案まで練習する
以下は、見出しと遷移先を確認したPracHub問題です。企業名の表示を将来の出題保証とは扱わず、ここで練習した定義・計算・判断を広げるために使ってください。この記事には、候補者報告に基づく選考回数や出題頻度の主張はありません。
| PracHubの問題 | 練習する説明 |
|---|---|
| Write SQL for top drivers and cancellation rates | 取消率の分母・分子・期間をSQLの条件へ落とす。配車の文脈は通販と区別する |
| Analyze Cancellation Change with Statistics | 観測された差と、その差をどこまで解釈できるかを分ける |
| Derive Key Business Metrics Using SQL or Python | 計算単位と検算用の件数を残して業務指標を作る |
| Diagnose Business Decline Using Key Data Metrics | 一数値から原因を決めず、追加で確かめる仮説へ分ける |
| Design an End-to-End Customer Delivery Experience Dashboard | 取消だけでなく配送工程の記録や未確定値をどう表示するか考える |
まず取消率のSQL問題で、分母、観測期間、計算単位を一文ずつ定義してください。結果を出したら、事業側へ伝える結論と、その結論だけでは決められないことを各一文にします。SQLと提案の間に残る確認事項まで話せることが、このケースの練習目標です。
Sources and Further Reading
- PostgreSQL 18:Aggregate Functions — COUNTとNULL、空集合の集約。
- PostgreSQL 18:Table Expressions — JOINが作る行と外部結合。
- PostgreSQL 18:Aggregate Expressions — FILTERによる条件付き集約。
- PostgreSQL 18:Conditional Expressions — NULLIFとCOALESCE。
- PostgreSQL 18:Date/Time Types — タイムゾーンを含む日時型。
Comments (0)