「売れている商品」を正確に定義できているか?
「うちのショップで売れている商品はどれですか?」
この質問に、あなたはすぐ答えられるだろうか。
「売上金額の高い商品TOP10」は出せるかもしれない。でも「売上全体の80%を支えている商品は何SKUあって、それは全商品の何%か」まで即答できる人は意外と少ない。
それを明らかにするのがABC分析だ。
ABC分析はシンプルだが強力な手法で、通販・EC現場では定番中の定番。在庫管理・仕入れ優先度・施策対象の絞り込みなど、あらゆる意思決定の土台になる。
この記事では、ABC分析の概念から始まり、SQLだけで完全に実装する方法を段階的に解説する。ウィンドウ関数(SUM() OVER())を使うので、前提知識として「GROUP BY と集計関数は分かる」レベルがあると読みやすい。
ABC分析とは何か
ABC分析とは、売上貢献度によってアイテムをA・B・Cの3ランクに分類する手法だ。
一般的な定義はこうだ。
| ランク | 累計売上構成比 | 意味 |
|---|---|---|
| A | 0〜80% | 売上の大半を支える主力商品 |
| B | 80〜95% | 中程度の貢献商品 |
| C | 95〜100% | 貢献が小さいロングテール商品 |
80-20の法則(パレートの法則)をご存知の方も多いだろう。「売上の80%は全商品の20%が生み出す」という経験則だ。ABC分析はこの法則を実際のデータで可視化し、どこにリソースを集中すべきかを教えてくれる。
A商品は在庫を切らさない、広告費を優先投下する、B商品はA昇格を狙う施策を打つ、C商品は縮小・廃番を検討する、という具合に意思決定が変わる。
サンプルデータの確認
今回使うテーブルはシンプルな注文明細テーブル order_items だ。
-- order_items テーブルのイメージ
-- order_id : 注文ID
-- product_id : 商品ID
-- product_name : 商品名
-- quantity : 数量
-- unit_price : 単価
-- order_date : 注文日
| order_id | product_id | product_name | quantity | unit_price | order_date |
|---|---|---|---|---|---|
| 1001 | P001 | オーガニックシャンプー | 2 | 1800 | 2024-01-15 |
| 1001 | P005 | コンディショナー | 1 | 1600 | 2024-01-15 |
| 1002 | P001 | オーガニックシャンプー | 1 | 1800 | 2024-01-15 |
| 1003 | P012 | 洗顔フォーム | 3 | 900 | 2024-01-16 |
| … | … | … | … | … | … |
STEP 1 ― 商品別の売上金額を集計する
まずはシンプルに、商品ごとの総売上金額を出す。
-- STEP1: 商品別売上集計
SELECT
product_id,
product_name,
SUM(quantity * unit_price) AS total_sales
FROM order_items
GROUP BY
product_id,
product_name
ORDER BY total_sales DESC;
結果はこんなイメージになる。
| product_id | product_name | total_sales |
|---|---|---|
| P001 | オーガニックシャンプー | 542,000 |
| P003 | ボディソープ | 398,000 |
| P012 | 洗顔フォーム | 287,000 |
| P005 | コンディショナー | 214,000 |
| P008 | 日焼け止め | 156,000 |
| … | … | … |
売上の高い順に並べられた。ここまでは「よく見るランキング表」だ。しかしこれだけでは合計に対して何%なのか、どこまでがAランクなのかが分からない。
STEP 2 ― 全体に対する構成比を計算する
次に、各商品の売上が全体の何%を占めるかを計算する。
全体の売上合計は SUM(total_sales) で出せるが、GROUP BY後のテーブルに対してさらに集計をかける方法として、ウィンドウ関数を使うのが最もスマートだ。
-- STEP2: 構成比を追加
WITH product_sales AS (
SELECT
product_id,
product_name,
SUM(quantity * unit_price) AS total_sales
FROM order_items
GROUP BY
product_id,
product_name
)
SELECT
product_id,
product_name,
total_sales,
SUM(total_sales) OVER () AS grand_total,
ROUND(total_sales * 100.0 / SUM(total_sales) OVER (), 2) AS sales_ratio
FROM product_sales
ORDER BY total_sales DESC;
ポイントは SUM(total_sales) OVER () という書き方だ。
OVER () の括弧の中が空であることに注目してほしい。これは「全レコードを1つのウィンドウとして扱う」という意味で、各行に全体合計を付与してくれる。GROUP BYのように行を畳まないので、商品ごとの行を保ちながら合計値を参照できる。
| product_id | product_name | total_sales | grand_total | sales_ratio |
|---|---|---|---|---|
| P001 | オーガニックシャンプー | 542,000 | 3,210,000 | 16.89 |
| P003 | ボディソープ | 398,000 | 3,210,000 | 12.40 |
| P012 | 洗顔フォーム | 287,000 | 3,210,000 | 8.94 |
| … | … | … | … | … |
STEP 3 ― 累計構成比を計算する(ここが核心)
ABC分析の肝は「上位から順番に足していった累計が80%・95%をいつ超えるか」だ。
これも SUM() OVER() で実現できる。今度は ORDER BY と組み合わせる。
-- STEP3: 累計構成比を追加
WITH product_sales AS (
SELECT
product_id,
product_name,
SUM(quantity * unit_price) AS total_sales
FROM order_items
GROUP BY
product_id,
product_name
)
SELECT
product_id,
product_name,
total_sales,
ROUND(total_sales * 100.0 / SUM(total_sales) OVER (), 2) AS sales_ratio,
ROUND(SUM(total_sales) OVER (ORDER BY total_sales DESC) * 100.0
/ SUM(total_sales) OVER (), 2) AS cumulative_ratio
FROM product_sales
ORDER BY total_sales DESC;
SUM(total_sales) OVER (ORDER BY total_sales DESC) が累計売上だ。
OVER() の中に ORDER BY total_sales DESC を入れることで、「売上の高い行から順番に値を足し込んでいく」累積和になる。デフォルトのフレーム指定が ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(最初の行から現在行まで)となるため、自動的に累計値になる。
| product_id | product_name | total_sales | sales_ratio | cumulative_ratio |
|---|---|---|---|---|
| P001 | オーガニックシャンプー | 542,000 | 16.89 | 16.89 |
| P003 | ボディソープ | 398,000 | 12.40 | 29.29 |
| P012 | 洗顔フォーム | 287,000 | 8.94 | 38.23 |
| P005 | コンディショナー | 214,000 | 6.67 | 44.90 |
| … | … | … | … | … |
累計構成比がどんどん積み上がっていく様子が見える。
STEP 4 ― ABCランクを自動でつける
最後に CASE WHEN を使って、累計構成比が80%以下ならA、95%以下ならB、それ以外はCと自動分類する。
-- STEP4: ABCランク付け(完成版)
WITH product_sales AS (
SELECT
product_id,
product_name,
SUM(quantity * unit_price) AS total_sales
FROM order_items
GROUP BY
product_id,
product_name
),
product_with_ratio AS (
SELECT
product_id,
product_name,
total_sales,
ROUND(total_sales * 100.0 / SUM(total_sales) OVER (), 2) AS sales_ratio,
ROUND(
SUM(total_sales) OVER (ORDER BY total_sales DESC) * 100.0
/ SUM(total_sales) OVER ()
, 2) AS cumulative_ratio
FROM product_sales
)
SELECT
product_id,
product_name,
total_sales,
sales_ratio,
cumulative_ratio,
CASE
WHEN cumulative_ratio <= 80 THEN 'A'
WHEN cumulative_ratio <= 95 THEN 'B'
ELSE 'C'
END AS abc_rank
FROM product_with_ratio
ORDER BY total_sales DESC;
完成した出力イメージ
| product_id | product_name | total_sales | sales_ratio | cumulative_ratio | abc_rank |
|---|---|---|---|---|---|
| P001 | オーガニックシャンプー | 542,000 | 16.89 | 16.89 | A |
| P003 | ボディソープ | 398,000 | 12.40 | 29.29 | A |
| P012 | 洗顔フォーム | 287,000 | 8.94 | 38.23 | A |
| P005 | コンディショナー | 214,000 | 6.67 | 44.90 | A |
| P008 | 日焼け止め | 156,000 | 4.86 | 49.76 | A |
| … | … | … | … | 79.83 | A |
| P021 | リップクリーム | 48,000 | 1.49 | 81.32 | B |
| … | … | … | … | 94.70 | B |
| P047 | 試供品セット | 8,200 | 0.26 | 95.11 | C |
| … | … | … | … | 100.00 | C |
これ1本のクエリで、全商品のABCランクが一発で出る。
さらに一歩進む ― ランク別サマリも同時に出す
現場では「Aランクは何SKUあって、売上合計はいくらか」というサマリも欲しくなる。上記クエリをCTEとして再利用すればいい。
-- ランク別サマリ
WITH product_sales AS (
SELECT
product_id,
product_name,
SUM(quantity * unit_price) AS total_sales
FROM order_items
GROUP BY product_id, product_name
),
product_with_ratio AS (
SELECT
product_id,
product_name,
total_sales,
ROUND(total_sales * 100.0 / SUM(total_sales) OVER (), 2) AS sales_ratio,
ROUND(
SUM(total_sales) OVER (ORDER BY total_sales DESC) * 100.0
/ SUM(total_sales) OVER ()
, 2) AS cumulative_ratio
FROM product_sales
),
abc_classified AS (
SELECT
*,
CASE
WHEN cumulative_ratio <= 80 THEN 'A'
WHEN cumulative_ratio <= 95 THEN 'B'
ELSE 'C'
END AS abc_rank
FROM product_with_ratio
)
SELECT
abc_rank,
COUNT(*) AS sku_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS sku_ratio,
SUM(total_sales) AS rank_total_sales,
ROUND(SUM(sales_ratio), 1) AS rank_sales_ratio
FROM abc_classified
GROUP BY abc_rank
ORDER BY abc_rank;
出力イメージ
| abc_rank | sku_count | sku_ratio | rank_total_sales | rank_sales_ratio |
|---|---|---|---|---|
| A | 12 | 18.8% | 2,568,000 | 80.0% |
| B | 18 | 28.1% | 482,000 | 15.0% |
| C | 34 | 53.1% | 160,000 | 5.0% |
全64SKU中、わずか12SKU(18.8%)が売上の80%を担っているという事実が浮かび上がる。経営会議でこの数字を出せれば、商品戦略の議論が一気に具体的になる。
実務での使い方ヒント
① 集計期間を柔軟に変える
直近3ヶ月のABC分析と、直近1年のABC分析では結果が変わることがある。WHERE order_date >= '2024-01-01' のような期間フィルタをSTEP1のCTEに追加するだけで対応できる。
② カテゴリ別にABC分析を行う
「全商品」ではなく「シャンプーカテゴリの中のABC」を見たい場合は、PARTITION BY category をウィンドウ関数に追加することで、カテゴリ内での順位付けができる。
SUM(total_sales) OVER (
PARTITION BY category
ORDER BY total_sales DESC
)
③ Treasure Dataでの注意点
Treasure DataのPrestoエンジンでは、上記クエリはほぼそのまま動く。ただし ROUND() の挙動が微妙に異なる場合があるので、小数点以下の扱いは CAST(x AS DOUBLE) を使って明示的に型変換しておくと安心だ。
まとめ ― ABC分析を「資産」にする
今回作ったクエリをまとめると、流れはこうだ。
GROUP BYで商品別の売上を集計SUM() OVER ()で全体合計を各行に付与し、構成比を計算SUM() OVER (ORDER BY ...)で累計構成比を計算CASE WHENでABCランクを自動付与
このクエリはそのままBIツール(Looker、Tableauなど)のデータソースとして使えるし、定期バッチとして自動実行すれば「毎月更新されるABCランク表」として運用できる。