はじめに
SQLはSELECT * FROM usersは書けても、「先月比を出す」「顧客ごとの最新注文だけ取る」といった実務の要求になると急に難しくなります。
この記事では、実務で頻出するSQLパターンを10個まとめました。売上・注文データを例に、コピペして応用できる形で紹介します。
サンプルテーブル
以下の3テーブルを前提にします。
-- 顧客
customers (customer_id, name, email, created_at)
-- 注文
orders (order_id, customer_id, order_date, total_amount, status)
-- 注文明細
order_items (item_id, order_id, product_name, category, quantity, price)
① 複数テーブルを結合する(JOIN)
最も基本かつ重要な操作です。
SELECT
o.order_id,
c.name AS customer_name,
o.order_date,
o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2026-04-01'
ORDER BY o.order_date DESC;
JOINの種類
| 種類 | 動作 |
|---|---|
INNER JOIN(= JOIN) |
両方に存在するデータのみ |
LEFT JOIN |
左テーブルは全件、右がなければNULL |
RIGHT JOIN |
右テーブルは全件(あまり使わない) |
使い分けの目安: 「注文がない顧客も含めたい」なら LEFT JOIN、「注文がある顧客だけでいい」なら JOIN。
② 注文がない顧客を抽出する
LEFT JOIN + IS NULL の組み合わせで「片方にだけ存在するデータ」を取れます。
SELECT
c.customer_id,
c.name,
c.email
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
休眠顧客の抽出やデータ不整合のチェックで頻繁に使います。
③ グループごとに集計する(GROUP BY)
SELECT
c.name,
COUNT(o.order_id) AS 注文回数,
SUM(o.total_amount) AS 累計購入額,
AVG(o.total_amount) AS 平均単価,
MAX(o.order_date) AS 最終注文日
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name
ORDER BY 累計購入額 DESC;
よくあるエラー:
SELECTに書いた非集計カラムは、すべてGROUP BYに含める必要があります(MySQLは設定によっては通りますが、明示するのが安全です)。
④ 集計結果で絞り込む(HAVING)
WHEREは集計前、HAVINGは集計後の絞り込みです。
-- 3回以上注文している顧客だけ
SELECT
c.name,
COUNT(o.order_id) AS 注文回数
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name
HAVING COUNT(o.order_id) >= 3
ORDER BY 注文回数 DESC;
-- WHERE と HAVING の併用
SELECT
c.name,
SUM(o.total_amount) AS 売上合計
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'completed' -- 集計前:完了した注文のみ
GROUP BY c.customer_id, c.name
HAVING SUM(o.total_amount) >= 100000 -- 集計後:合計10万以上
⑤ 月別に集計する
日付を月単位にまとめるのは、レポート作成の定番です。
-- MySQL
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS 年月,
COUNT(*) AS 注文件数,
SUM(total_amount) AS 売上合計
FROM orders
WHERE status = 'completed'
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY 年月;
-- PostgreSQL
SELECT
TO_CHAR(order_date, 'YYYY-MM') AS 年月,
COUNT(*) AS 注文件数,
SUM(total_amount) AS 売上合計
FROM orders
WHERE status = 'completed'
GROUP BY TO_CHAR(order_date, 'YYYY-MM')
ORDER BY 年月;
⑥ 条件によって値を振り分ける(CASE)
SELECT
name,
total_purchase,
CASE
WHEN total_purchase >= 500000 THEN 'プラチナ'
WHEN total_purchase >= 100000 THEN 'ゴールド'
WHEN total_purchase >= 10000 THEN 'シルバー'
ELSE 'ブロンズ'
END AS 会員ランク
FROM (
SELECT
c.name,
SUM(o.total_amount) AS total_purchase
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name
) AS summary;
CASEで横持ち集計(クロス集計)
-- カテゴリを列にして集計する
SELECT
DATE_FORMAT(o.order_date, '%Y-%m') AS 年月,
SUM(CASE WHEN oi.category = '食品' THEN oi.price * oi.quantity ELSE 0 END) AS 食品,
SUM(CASE WHEN oi.category = '飲料' THEN oi.price * oi.quantity ELSE 0 END) AS 飲料,
SUM(CASE WHEN oi.category = '雑貨' THEN oi.price * oi.quantity ELSE 0 END) AS 雑貨
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY DATE_FORMAT(o.order_date, '%Y-%m')
ORDER BY 年月;
Excelのピボットテーブルのような表がSQLだけで作れます。
⑦ 顧客ごとの最新注文だけを取得する
「グループごとの最新1件」は頻出ですが、意外と書き方に迷うパターンです。
方法A:ウィンドウ関数(MySQL 8.0+ / PostgreSQL)
SELECT *
FROM (
SELECT
o.*,
ROW_NUMBER() OVER (
PARTITION BY o.customer_id
ORDER BY o.order_date DESC
) AS rn
FROM orders o
) AS ranked
WHERE rn = 1;
PARTITION BY で顧客ごとに区切り、その中で日付順に番号を振って1番目だけ取る、という考え方です。
方法B:サブクエリ(古いMySQLでも動く)
SELECT o.*
FROM orders o
JOIN (
SELECT customer_id, MAX(order_date) AS latest
FROM orders
GROUP BY customer_id
) AS m
ON o.customer_id = m.customer_id
AND o.order_date = m.latest;
⑧ ランキングを出す(ウィンドウ関数)
SELECT
product_name,
SUM(quantity * price) AS 売上,
RANK() OVER (ORDER BY SUM(quantity * price) DESC) AS 順位
FROM order_items
GROUP BY product_name;
RANK / DENSE_RANK / ROW_NUMBER の違い
同じ売上が並んだときの挙動が異なります。
| 関数 | 同順位のとき |
|---|---|
ROW_NUMBER() |
1, 2, 3, 4(必ず連番) |
RANK() |
1, 2, 2, 4(次を飛ばす) |
DENSE_RANK() |
1, 2, 2, 3(次を飛ばさない) |
⑨ 前月比・前日比を出す(LAG)
LAG() は「1つ前の行の値」を取得する関数です。これで期間比較が簡単に書けます。
SELECT
年月,
売上,
LAG(売上) OVER (ORDER BY 年月) AS 前月売上,
売上 - LAG(売上) OVER (ORDER BY 年月) AS 増減,
ROUND(
(売上 - LAG(売上) OVER (ORDER BY 年月))
/ LAG(売上) OVER (ORDER BY 年月) * 100,
1
) AS 増減率
FROM (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS 年月,
SUM(total_amount) AS 売上
FROM orders
WHERE status = 'completed'
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) AS monthly
ORDER BY 年月;
これまでExcelでやっていた前月比の計算が、SQL1本で完結します。
⑩ 累計を出す(SUM OVER)
SELECT
年月,
売上,
SUM(売上) OVER (ORDER BY 年月) AS 累計売上
FROM (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS 年月,
SUM(total_amount) AS 売上
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) AS monthly
ORDER BY 年月;
ORDER BY を付けた SUM() OVER は「そこまでの累計」になります。年度累計や進捗管理に便利です。
書くときのコツ
① 複雑なクエリはCTE(WITH句)で分解する
サブクエリのネストが深くなったら、WITH で名前を付けて分けると格段に読みやすくなります。
WITH monthly_sales AS (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS 年月,
SUM(total_amount) AS 売上
FROM orders
WHERE status = 'completed'
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
)
SELECT
年月,
売上,
LAG(売上) OVER (ORDER BY 年月) AS 前月売上
FROM monthly_sales
ORDER BY 年月;
処理の流れが上から下に読めるようになります。
② まず小さく作って確かめる
いきなり完成形を書かず、SELECT で対象データを確認 → WHERE で絞る → GROUP BY を足す、と段階的に組み立てるとミスが減ります。
③ 遅いと感じたらEXPLAINを見る
パフォーマンスが気になったらMySQLが遅い時に最初に確認する5つのことも参考にしてください。
まとめ
実務でよく使うSQLパターン10選でした。
- JOINで結合
- LEFT JOIN + IS NULLで「ない方」を抽出
- GROUP BYで集計
- HAVINGで集計後に絞り込み
- 日付を月単位にまとめる
- CASEで条件分岐・クロス集計
- グループごとの最新1件を取る
- RANKでランキング
- LAGで前月比
- SUM OVERで累計
特に**ウィンドウ関数(⑦〜⑩)**を使えるようになると、これまでExcelに出力してから計算していた処理がSQLだけで完結するようになります。
SQL・データ集計のご相談
「複雑な集計クエリを組みたい」「レポート作成を自動化したい」「DBのパフォーマンスを改善したい」などのご相談を受け付けています。