日付から年月を取り出して月次集計する方法と、EXISTS で「条件に合う行が存在するか」を判定する書き方を練習します。
2026-01-15 のような日付をそのまま GROUP BY すると、 日ごとにバラバラの
グループになってしまいます。 月次で見たいときは 年月の文字列に切り落として
から集計します。
SELECT strftime('%Y-%m', order_date) AS ym, COUNT(*) AS cnt
FROM orders
GROUP BY ym
ORDER BY ym;
strftime は日付を指定した書式の文字列に変える関数です。
| 書式 | 意味 | 例 |
|---|---|---|
| %Y | 年 4 桁 | 2026 |
| %m | 月 2 桁 | 01 |
| %d | 日 2 桁 | 15 |
| %Y-%m | 年月 | 2026-01 |
%Y-%m は 並べ替えるとそのまま時系列順 になるのが利点です。
1月 2月 … のような文字列にすると、 10 月が 1 月の次に来てしまいます。
> 日付関数の名前は データベースごとに違います。 SQLite は strftime、
> MySQL は DATE_FORMAT、 PostgreSQL は TO_CHAR / DATE_TRUNC です。
> 考え方は同じで、 関数名だけ読み替えれば通用します。
金額は orders には無く products にあるので、 つないでから集計します。
SELECT strftime('%Y-%m', o.order_date) AS ym,
SUM(p.price * o.quantity) AS sales
FROM orders o
JOIN products p ON p.id = o.product_id
GROUP BY ym
ORDER BY ym;
SELECT name FROM products p
WHERE EXISTS (
SELECT 1 FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.product_id = p.id AND c.city = '東京'
);
EXISTS は 中のクエリが 1 行でも返せば真 になります。 中で
SELECT 1 と書いているのは、 値そのものは使わず 「あるかどうか」 しか
見ないからです。 SELECT * でも動きますが、 意図が伝わる 1 が慣用です。
外側の p.id を 内側から参照している点に注目してください。 このように
外の行ごとに内側を評価する サブクエリを 相関サブクエリと呼びます。
同じことは IN でも JOIN でも書けますが、 向き不向きがあります。
- EXISTS … 「存在するか」 だけ知りたい。 1 件見つかった時点で打ち切れる。
NULL があっても正しく動く。
- IN … 内側が返す値の一覧と突き合わせたい。 リストが小さいときは読みやすい。
ただし 内側に NULL が混ざると NOT IN が期待どおりに動かない。
- JOIN … 相手側の列も 結果に並べたい とき。 相手が複数行あると
行が増える (重複する) 点に注意。
「条件として使うだけで、 結果には出さない」 なら EXISTS が素直です。
SELECT name FROM products p
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.product_id = p.id);
売れていない商品、 一度も来店していない会員、 提出のない課題 —
「無いもの」 を探す のは EXISTS の得意分野です。
(このサンプルデータでは 全商品が注文済みなので 結果は 0 件になります。
0 件が返るのも 正しい答えです。)
- 日付を文字列として比較して失敗する — YYYY-MM-DD 形式なら
文字列比較でも時系列順になりますが、 2026/1/5 のような形式だと崩れます。
- GROUP BY に別名が使えない DB がある — SQLite は使えますが、
厳密な DB では GROUP BY strftime('%Y-%m', order_date) と書き直します。
- NOT IN と NULL — 内側に 1 つでも NULL があると NOT IN は
常に偽になります。 迷ったら NOT EXISTS を使うのが安全です。
月次・年次のレポートは ほぼ全部これです。 EXISTS のほうは
「まだ登録していないユーザー」 「在庫がない商品」 のような 抜け漏れの洗い出しで、
データの品質チェックにも そのまま使えます。