本文へ移動
売上・LTV分析SQL 更新日: 2026年5月2日 約16分で読めます

ABC分析をSQLで実装する方法|売上上位80%の商品を抽出するクエリ


「売れている商品」を正確に定義できているか?

「うちのショップで売れている商品はどれですか?」

この質問に、あなたはすぐ答えられるだろうか。

「売上金額の高い商品TOP10」は出せるかもしれない。でも「売上全体の80%を支えている商品は何SKUあって、それは全商品の何%か」まで即答できる人は意外と少ない。

それを明らかにするのがABC分析だ。

ABC分析はシンプルだが強力な手法で、通販・EC現場では定番中の定番。在庫管理・仕入れ優先度・施策対象の絞り込みなど、あらゆる意思決定の土台になる。

この記事では、ABC分析の概念から始まり、SQLだけで完全に実装する方法を段階的に解説する。ウィンドウ関数(SUM() OVER())を使うので、前提知識として「GROUP BY と集計関数は分かる」レベルがあると読みやすい。


ABC分析とは何か

ABC分析とは、売上貢献度によってアイテムをA・B・Cの3ランクに分類する手法だ。

一般的な定義はこうだ。

ランク累計売上構成比意味
A0〜80%売上の大半を支える主力商品
B80〜95%中程度の貢献商品
C95〜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_idproduct_idproduct_namequantityunit_priceorder_date
1001P001オーガニックシャンプー218002024-01-15
1001P005コンディショナー116002024-01-15
1002P001オーガニックシャンプー118002024-01-15
1003P012洗顔フォーム39002024-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_idproduct_nametotal_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_idproduct_nametotal_salesgrand_totalsales_ratio
P001オーガニックシャンプー542,0003,210,00016.89
P003ボディソープ398,0003,210,00012.40
P012洗顔フォーム287,0003,210,0008.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_idproduct_nametotal_salessales_ratiocumulative_ratio
P001オーガニックシャンプー542,00016.8916.89
P003ボディソープ398,00012.4029.29
P012洗顔フォーム287,0008.9438.23
P005コンディショナー214,0006.6744.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_idproduct_nametotal_salessales_ratiocumulative_ratioabc_rank
P001オーガニックシャンプー542,00016.8916.89A
P003ボディソープ398,00012.4029.29A
P012洗顔フォーム287,0008.9438.23A
P005コンディショナー214,0006.6744.90A
P008日焼け止め156,0004.8649.76A
79.83A
P021リップクリーム48,0001.4981.32B
94.70B
P047試供品セット8,2000.2695.11C
100.00C

これ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_ranksku_countsku_ratiorank_total_salesrank_sales_ratio
A1218.8%2,568,00080.0%
B1828.1%482,00015.0%
C3453.1%160,0005.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分析を「資産」にする

今回作ったクエリをまとめると、流れはこうだ。

  1. GROUP BY で商品別の売上を集計
  2. SUM() OVER () で全体合計を各行に付与し、構成比を計算
  3. SUM() OVER (ORDER BY ...) で累計構成比を計算
  4. CASE WHEN でABCランクを自動付与

このクエリはそのままBIツール(Looker、Tableauなど)のデータソースとして使えるし、定期バッチとして自動実行すれば「毎月更新されるABCランク表」として運用できる。

次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む