友達紹介プログラムの「本当の価値」は見えているか
多くの通販・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_id | node_id | generation | referral_path | node_ltv |
|---|---|---|---|---|
| A | B | 1 | A > B | 48,000 |
| A | C | 1 | A > C | 32,000 |
| A | D | 2 | A > B > D | 28,000 |
| A | E | 2 | A > B > E | 41,000 |
| A | F | 2 | A > C > F | 19,000 |
| A | G | 3 | A > B > E > G | 15,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_id | own_ltv | total_descendants | descendants_ltv | network_value | max_depth | network_roi | network_rank |
|---|---|---|---|---|---|---|---|
| A | 84,000 | 6 | 183,000 | 267,000 | 3 | 18.3x | 1 |
| X | 52,000 | 12 | 284,000 | 336,000 | 4 | 28.4x | 2 |
| Y | 124,000 | 2 | 48,000 | 172,000 | 1 | 24.0x | 3 |
| Z | 18,000 | 4 | 41,000 | 59,000 | 2 | 13.7x | 8 |
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_group | referrer_count | value_share_pct | avg_descendants | avg_network_value |
|---|---|---|---|---|
| 上位1% | 8 | 24.8% | 18.4 | 412,000 |
| 上位5% | 40 | 48.2% | 11.2 | 248,000 |
| 上位10% | 80 | 63.4% | 7.8 | 168,000 |
| 上位20% | 160 | 78.1% | 4.2 | 98,000 |
| 下位80% | 642 | 21.9% | 0.8 | 8,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_id | referral_count | days_to_first_referral | monthly_referral_rate | superspreader_score | early_ambassador_flag |
|---|---|---|---|---|---|
| X | 12 | 3 | 2.8 | 9.44 | 1 |
| A | 6 | 8 | 1.4 | 5.42 | 1 |
| Y | 2 | 45 | 0.5 | 1.48 | 0 |
登録後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実装をまとめる。
WITH RECURSIVEで紹介ネットワークを全世代展開する- 各ノードのLTVを JOIN して合計し、root_id ごとに「子孫の合計LTV」を計算する
- 自分のLTVと子孫LTVの合計を「ネットワーク価値」とする
PERCENT_RANK()で上位1%・5%のスーパースプレッダーを特定する- 「紹介速度」「早期行動」の複合スコアでスーパースプレッダーを早期識別する