本文へ移動
マーケティングSQL 更新日: 2026年5月2日 約30分で読めます

紹介の伝播をSQLで追跡する方法「スーパースプレッダー」は誰か


友達紹介プログラムの「本当の価値」は見えているか

多くの通販・ECが友達紹介プログラムを運営している。「友達を紹介すると500円クーポン進呈」のような施策だ。

しかし「紹介プログラムの効果」を次のように測っている企業がほとんどだ。

効果 = 紹介経由の新規登録数 × 平均LTV

これは不完全だ。ある顧客Aが友人B・Cを紹介し、BがさらにD・Eを、CがFを紹介したとしよう。
Aを起点とした「紹介ツリー」の総価値は A の直接紹介(B・C)だけでなく、孫(D・E・F)の価値まで含むべきだ。

さらに「スーパースプレッダー」と呼ばれる、ごく少数の顧客が紹介ネットワーク全体の大半を支えているケースが実際に多い。
このスーパースプレッダーを特定し、集中的に優遇することが紹介プログラムの ROI を最大化する。

今回は再帰CTEを使ってこの紹介ネットワークを展開し、各顧客の「ネットワーク価値(自分と全子孫の合計LTV)」を計算する。これは SQL で実装できるグラフ分析の中でも実務的な価値が特に高いテーマだ。


使用するテーブル

-- referrals(紹介関係テーブル)
-- referral_id    : 紹介ID(主キー)
-- referrer_id    : 紹介した顧客のID
-- referred_id    : 紹介された顧客のID
-- referred_at    : 紹介が成立した日時
-- reward_amount  : 紹介報酬額(円)

-- orders(注文テーブル)
-- order_id, customer_id, order_date, total_amount, status

-- customers(顧客マスタ)
-- customer_id, registered_at, channel, rank

紹介関係のイメージ

A(創設者)
├── B(第1世代:Aが直接紹介)
│ ├── D(第2世代:Bが紹介)
│ └── E(第2世代:Bが紹介)
│ └── G(第3世代:Eが紹介)
└── C(第1世代:Aが直接紹介)
└── F(第2世代:Cが紹介)

STEP 1 ― 顧客ごとのLTVを計算する

紹介ツリーの価値計算の基礎となる顧客別LTVを準備する。

-- STEP1: 顧客別LTVの計算

WITH customer_ltv AS (
    SELECT
        customer_id,
        COUNT(order_id)         AS total_orders,
        SUM(total_amount)       AS lifetime_value,
        MIN(order_date)         AS first_order_date,
        MAX(order_date)         AS last_order_date
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
)
SELECT
    c.customer_id,
    c.registered_at,
    c.channel,
    COALESCE(ltv.total_orders, 0)    AS total_orders,
    COALESCE(ltv.lifetime_value, 0)  AS lifetime_value,
    ltv.first_order_date,
    ltv.last_order_date
FROM customers      c
LEFT JOIN customer_ltv  ltv  ON c.customer_id = ltv.customer_id;

STEP 2 ― 再帰CTEで紹介ネットワークを全世代展開する

再帰CTEで学んだパターンをここで応用する。
「親カテゴリから子孫を展開する」のと同じ構造で、「紹介者から全被紹介者を展開する」

-- STEP2: 再帰CTEで紹介ネットワークを全世代展開する

WITH RECURSIVE referral_tree AS (
    -- ベースケース:紹介した側(referrer)を起点とする
    -- 誰かを紹介したことがある顧客を起点にすべての子孫を展開
    SELECT
        r.referrer_id      AS root_id,        -- ツリーの根(起点)
        r.referred_id      AS node_id,        -- 現在のノード
        r.referrer_id      AS parent_id,      -- 直接の親
        1                  AS generation,     -- 世代(直接紹介=1)
        CAST(r.referrer_id AS VARCHAR)
        || ' > '
        || CAST(r.referred_id AS VARCHAR)    AS referral_path,  -- 経路文字列
        r.referred_at,
        r.reward_amount
    FROM referrals  r

    UNION ALL

    -- 再帰ステップ:子ノードから孫ノードへ
    SELECT
        rt.root_id,
        r.referred_id          AS node_id,
        r.referrer_id          AS parent_id,
        rt.generation + 1      AS generation,
        rt.referral_path
        || ' > '
        || CAST(r.referred_id AS VARCHAR)  AS referral_path,
        r.referred_at,
        r.reward_amount
    FROM referral_tree  rt
    JOIN referrals       r  ON rt.node_id = r.referrer_id
    WHERE rt.generation < 10  -- 深さ上限(無限ループ防止)
    AND rt.referral_path NOT LIKE '%' || CAST(r.referred_id AS VARCHAR) || '%'
    -- 循環参照の防止(自分自身がすでに経路に含まれていないか確認)
)
SELECT
    rt.root_id,
    rt.node_id,
    rt.parent_id,
    rt.generation,
    rt.referral_path,
    rt.referred_at,
    -- 被紹介者のLTV
    COALESCE(ltv.lifetime_value, 0)  AS node_ltv,
    ltv.total_orders,
    rt.reward_amount
FROM referral_tree  rt
LEFT JOIN (
    SELECT customer_id, lifetime_value, total_orders
    FROM orders WHERE status = 'completed'
    GROUP BY customer_id
    -- 実際には STEP1の customer_ltv CTEを参照する
) ltv  ON rt.node_id = ltv.customer_id
ORDER BY rt.root_id, rt.generation, rt.node_id;

出力イメージ(顧客Aのツリー)

root_idnode_idgenerationreferral_pathnode_ltv
AB1A > B48,000
AC1A > C32,000
AD2A > B > D28,000
AE2A > B > E41,000
AF2A > C > F19,000
AG3A > B > E > G15,000

Aは直接2人(B・C)を紹介し、その子孫まで含めると6人のネットワークを持つ。


STEP 3 ― 各紹介者の「ネットワーク価値」を集計する

展開した全世代のLTVを紹介者(root_id)ごとに合計して「ネットワーク価値」を計算する。

-- STEP3: 紹介者ごとのネットワーク価値を集計する(完成版)

WITH RECURSIVE referral_tree AS (
    SELECT
        r.referrer_id  AS root_id,
        r.referred_id  AS node_id,
        r.referrer_id  AS parent_id,
        1              AS generation,
        CAST(r.referrer_id AS VARCHAR) || ' > ' || CAST(r.referred_id AS VARCHAR)  AS path,
        r.reward_amount
    FROM referrals  r

    UNION ALL

    SELECT
        rt.root_id,
        r.referred_id,
        r.referrer_id,
        rt.generation + 1,
        rt.path || ' > ' || CAST(r.referred_id AS VARCHAR),
        r.reward_amount
    FROM referral_tree rt
    JOIN referrals     r   ON rt.node_id = r.referrer_id
    WHERE rt.generation < 10
    AND rt.path NOT LIKE '%' || CAST(r.referred_id AS VARCHAR) || '%'
),
-- 顧客別LTV
customer_ltv AS (
    SELECT
        customer_id,
        SUM(total_amount)  AS lifetime_value,
        COUNT(order_id)    AS total_orders
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id
),
-- 紹介者自身のLTVも加算
referrer_ltv AS (
    SELECT
        r.referrer_id                                AS customer_id,
        COALESCE(ltv.lifetime_value, 0)              AS own_ltv,
        COALESCE(ltv.total_orders, 0)                AS own_orders
    FROM (SELECT DISTINCT referrer_id FROM referrals) r
    LEFT JOIN customer_ltv ltv ON r.referrer_id = ltv.customer_id
),
-- 紹介ツリーのサマリ
tree_summary AS (
    SELECT
        rt.root_id,
        COUNT(DISTINCT rt.node_id)                    AS total_descendants,   -- 子孫の総数
        SUM(COALESCE(ltv.lifetime_value, 0))          AS descendants_ltv,     -- 子孫の合計LTV
        MAX(rt.generation)                            AS max_depth,            -- ツリーの深さ
        SUM(rt.reward_amount)                         AS total_rewards_paid,   -- 支払った報酬総額

        -- 世代別の子孫数
        COUNT(DISTINCT CASE WHEN rt.generation = 1 THEN rt.node_id END)  AS gen1_count,
        COUNT(DISTINCT CASE WHEN rt.generation = 2 THEN rt.node_id END)  AS gen2_count,
        COUNT(DISTINCT CASE WHEN rt.generation = 3 THEN rt.node_id END)  AS gen3_count,

        -- 世代別のLTV
        SUM(CASE WHEN rt.generation = 1 THEN COALESCE(ltv.lifetime_value, 0) ELSE 0 END)  AS gen1_ltv,
        SUM(CASE WHEN rt.generation = 2 THEN COALESCE(ltv.lifetime_value, 0) ELSE 0 END)  AS gen2_ltv,
        SUM(CASE WHEN rt.generation = 3 THEN COALESCE(ltv.lifetime_value, 0) ELSE 0 END)  AS gen3_ltv
    FROM referral_tree  rt
    LEFT JOIN customer_ltv  ltv  ON rt.node_id = ltv.customer_id
    GROUP BY rt.root_id
)
SELECT
    ts.root_id                                        AS customer_id,
    rl.own_ltv                                        AS own_lifetime_value,
    ts.total_descendants,
    ts.descendants_ltv,

    -- ネットワーク価値 = 自分のLTV + 全子孫のLTV
    rl.own_ltv + ts.descendants_ltv                  AS network_value,

    ts.max_depth,
    ts.gen1_count,
    ts.gen2_count,
    ts.gen3_count,
    ts.gen1_ltv,
    ts.gen2_ltv,
    ts.gen3_ltv,
    ts.total_rewards_paid,

    -- ネットワークROI(子孫LTV ÷ 支払った報酬)
    ROUND(ts.descendants_ltv * 1.0
        / NULLIF(ts.total_rewards_paid, 0), 2)        AS network_roi,

    -- スーパースプレッダースコア(ネットワーク価値の全体における相対的な位置)
    ROUND(
        (rl.own_ltv + ts.descendants_ltv) * 100.0
        / SUM(rl.own_ltv + ts.descendants_ltv) OVER ()
    , 2)  AS network_value_share_pct,

    -- ランク付け
    RANK() OVER (ORDER BY rl.own_ltv + ts.descendants_ltv DESC)  AS network_rank

FROM tree_summary   ts
JOIN referrer_ltv   rl  ON ts.root_id = rl.customer_id
ORDER BY network_rank;

完成した出力イメージ

customer_idown_ltvtotal_descendantsdescendants_ltvnetwork_valuemax_depthnetwork_roinetwork_rank
A84,0006183,000267,000318.3x1
X52,00012284,000336,000428.4x2
Y124,000248,000172,000124.0x3
Z18,000441,00059,000213.7x8

Xは自身のLTVは52,000円と中程度だが、12人の子孫を持ちネットワーク価値336,000円と最大のスーパースプレッダーだ。報酬に対して28.4倍のリターンを生んでいる。
Yは自身のLTV(124,000円)は最高だが紹介活動は少なく、ネットワーク価値172,000円にとどまる。


STEP 4 ― 紹介ネットワーク全体の可視化クエリ

ネットワーク全体の「深さ別分布」と「ネットワーク価値の集中度」を分析する。

-- STEP4: ネットワーク全体の構造分析

WITH RECURSIVE referral_tree AS (
    SELECT referrer_id AS root_id, referred_id AS node_id, 1 AS generation,
           CAST(referrer_id AS VARCHAR) || ' > ' || CAST(referred_id AS VARCHAR) AS path
    FROM referrals
    UNION ALL
    SELECT rt.root_id, r.referred_id, rt.generation + 1,
           rt.path || ' > ' || CAST(r.referred_id AS VARCHAR)
    FROM referral_tree rt JOIN referrals r ON rt.node_id = r.referrer_id
    WHERE rt.generation < 10 AND rt.path NOT LIKE '%' || CAST(r.referred_id AS VARCHAR) || '%'
),
customer_ltv AS (
    SELECT customer_id, SUM(total_amount) AS ltv
    FROM orders WHERE status = 'completed' GROUP BY customer_id
),
network_summary AS (
    SELECT
        root_id,
        COUNT(DISTINCT node_id)                    AS total_descendants,
        SUM(COALESCE(ltv.ltv, 0))                 AS descendants_ltv
    FROM referral_tree rt
    LEFT JOIN customer_ltv ltv ON rt.node_id = ltv.customer_id
    GROUP BY root_id
),
-- ネットワーク価値の分布
value_distribution AS (
    SELECT
        ns.root_id,
        COALESCE(own_ltv.ltv, 0) + ns.descendants_ltv  AS network_value,
        ns.total_descendants
    FROM network_summary  ns
    LEFT JOIN customer_ltv  own_ltv  ON ns.root_id = own_ltv.customer_id
)
SELECT
    -- パレート分析:上位N%がネットワーク価値全体の何%を占めるか
    CASE
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.01 THEN '上位1%'
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.05 THEN '上位5%'
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.10 THEN '上位10%'
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.20 THEN '上位20%'
        ELSE                                                                 '下位80%'
    END  AS percentile_group,

    COUNT(root_id)                                                 AS referrer_count,
    SUM(network_value)                                             AS group_network_value,
    ROUND(SUM(network_value) * 100.0 / SUM(SUM(network_value)) OVER (), 1)
        AS value_share_pct,

    ROUND(AVG(total_descendants), 1)                               AS avg_descendants,
    ROUND(AVG(network_value), 0)                                   AS avg_network_value,
    MAX(network_value)                                             AS max_network_value
FROM value_distribution
GROUP BY
    CASE
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.01 THEN '上位1%'
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.05 THEN '上位5%'
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.10 THEN '上位10%'
        WHEN PERCENT_RANK() OVER (ORDER BY network_value DESC) <= 0.20 THEN '上位20%'
        ELSE                                                                 '下位80%'
    END
ORDER BY MAX(PERCENT_RANK() OVER (ORDER BY network_value DESC));

出力イメージ

percentile_groupreferrer_countvalue_share_pctavg_descendantsavg_network_value
上位1%824.8%18.4412,000
上位5%4048.2%11.2248,000
上位10%8063.4%7.8168,000
上位20%16078.1%4.298,000
下位80%64221.9%0.88,400

上位1%(8人)が紹介ネットワーク価値全体の24.8%を支えている。上位5%で48.2%だ。
これがスーパースプレッダーの実態だ。
この8人を特定して「プレミアムアンバサダープログラム」として特別な優遇(より高い報酬・限定特典・専用サポート)を提供することが、紹介プログラムの ROI を最大化する最短ルートだ。


STEP 5 ― スーパースプレッダーの早期識別クエリ

「この顧客はスーパースプレッダーになる可能性が高いか」を登録後の早期段階で識別する。

-- STEP5: スーパースプレッダーの早期識別

WITH referral_velocity AS (
    SELECT
        referrer_id                                   AS customer_id,
        COUNT(referred_id)                            AS referral_count,
        DATE_DIFF('day',
            MIN(referred_at),
            MAX(referred_at)
        )  AS referral_span_days,

        -- 最初の紹介を何日以内に行ったか(早期行動指標)
        MIN(DATE_DIFF('day',
            c.registered_at,
            CAST(r.referred_at AS DATE)
        ))  AS days_to_first_referral,

        -- 月あたりの紹介数(紹介速度)
        ROUND(
            COUNT(referred_id) * 30.0
            / NULLIF(DATE_DIFF('day', MIN(CAST(r.referred_at AS DATE)),
                                      CURRENT_DATE), 0)
        , 2)  AS monthly_referral_rate
    FROM referrals        r
    JOIN customers        c   ON r.referrer_id = c.customer_id
    GROUP BY r.referrer_id, c.registered_at
),
with_network AS (
    -- STEP3のnetwork_summaryを参照(実際はCTEとして連結する)
    SELECT
        rv.customer_id,
        rv.referral_count,
        rv.days_to_first_referral,
        rv.monthly_referral_rate,
        COALESCE(ltv.lifetime_value, 0)  AS own_ltv
    FROM referral_velocity  rv
    LEFT JOIN (
        SELECT customer_id, SUM(total_amount) AS lifetime_value
        FROM orders WHERE status = 'completed' GROUP BY customer_id
    ) ltv  ON rv.customer_id = ltv.customer_id
)
SELECT
    customer_id,
    referral_count,
    days_to_first_referral,
    monthly_referral_rate,
    own_ltv,

    -- スーパースプレッダースコア(複合指標)
    ROUND(
        -- 紹介数(重み:50%)
        referral_count * 0.5
        -- 紹介速度(重み:30%)
        + monthly_referral_rate * 0.3
        -- 早期行動(重み:20%。早いほど高スコア)
        + CASE WHEN days_to_first_referral <= 7  THEN 10
               WHEN days_to_first_referral <= 30 THEN 7
               WHEN days_to_first_referral <= 90 THEN 4
               ELSE                                   1 END * 0.2
    , 2)  AS superspreader_score,

    -- アーリーアンバサダーフラグ(登録30日以内に3人以上紹介)
    CASE
        WHEN days_to_first_referral <= 30
        AND referral_count >= 3
        THEN 1 ELSE 0
    END  AS early_ambassador_flag

FROM with_network
ORDER BY superspreader_score DESC;

出力イメージ

customer_idreferral_countdays_to_first_referralmonthly_referral_ratesuperspreader_scoreearly_ambassador_flag
X1232.89.441
A681.45.421
Y2450.51.480

登録後3日以内に紹介を開始し、月に2.8件のペースで紹介しているXは、スーパースプレッダースコア9.44で最高位だ。
このような顧客を登録後30日以内に識別して「プレミアムアンバサダーへの招待」を送ることが、紹介プログラムを加速させる。


実務での運用ヒント

① マルチレベルマーケティング(MLM)との法的リスクに注意

紹介連鎖を複数世代にわたって報酬設計する場合、特定商取引法における連鎖販売取引(ねずみ講・マルチ商法)の規制に抵触する可能性がある。
第2世代以降への報酬設計には法務確認が必須だ。今回のクエリは「追跡・分析」のためであり、報酬設計は別途設計が必要だ。

② 大規模ネットワークでの再帰CTEのパフォーマンス

紹介者が数万人・被紹介者が数十万人になると、再帰CTEが重くなる。
対策として「毎月バッチで全ネットワークを展開したテーブルを事前生成し、分析クエリはそこを参照する」設計にすることでリアルタイムの再帰計算を避けられる。

③ 循環参照の検知

稀に「A が B を紹介し、B が A を紹介する」という循環がデータに混入することがある。
path NOT LIKE '%' || node_id || '%' の条件だけでは文字列の部分一致に依存するため、id が4桁以上の長い文字列なら問題ないが、1〜2桁の短いIDの場合に誤検知が起きる可能性がある。
本番では循環参照の有無を事前にチェックし、クレンジングしておく。


まとめ

紹介ネットワーク分析のSQL実装をまとめる。

  1. WITH RECURSIVE で紹介ネットワークを全世代展開する
  2. 各ノードのLTVを JOIN して合計し、root_id ごとに「子孫の合計LTV」を計算する
  3. 自分のLTVと子孫LTVの合計を「ネットワーク価値」とする
  4. PERCENT_RANK() で上位1%・5%のスーパースプレッダーを特定する
  5. 「紹介速度」「早期行動」の複合スコアでスーパースプレッダーを早期識別する

次のアクション

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

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

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

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

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

実績・支援内容を見る →

MarTech Farmをもっと見る

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

続きを読む