9. 在线音乐平台综合分析

某在线音乐平台“乐享音乐”希望分析用户的付费订阅和退订行为,以优化内容推荐和定价策略。平台维护用户信息、歌曲信息、订阅支付记录和退订记录四张核心表。

数据规模:用户表约 3000 万行,订阅支付记录每日新增约 200 万行,歌曲表约 60 万行,退订记录每日新增约 5 万行。

9.1 表结构

用户表 dim_users

字段 类型 含义
user_id string 用户编号
user_name string 用户名
register_date string 注册日期
country string 国家
tags array<string> 标签,如 ['premium', 'family']
profile map<string,string> 扩展属性,如 {'age':'22','gender':'F'}
vip_level int VIP 等级,范围 0—5

歌曲表 dim_songs

字段 类型 含义
song_id string 歌曲编号
song_name string 歌曲名称
genre_l1 string 一级流派
genre_l2 string 二级流派
price double 单曲订阅价格
singer string 歌手

订阅支付记录表 fact_subscriptions

字段 类型 含义
sub_id string 订阅记录号
user_id string 用户编号
song_id string 歌曲编号
sub_date string 订阅支付日期
duration int 订阅时长,单位为天
pay_amount double 实际支付金额
status string 状态:completedexpired

退订表 fact_unsubscribes

字段 类型 含义
unsub_id string 退订单号
sub_id string 原订阅记录号
unsub_date string 退订申请日期
reason string 退订原因

9.2 查询高等级 VIP 用户

查询 VIP 等级大于等于 4 的用户编号、用户名、国家,以及 profile 中的年龄。年龄转换为整数,无法转换时显示 NULL

SELECT
  user_id,
  user_name,
  country,
  CAST(profile['age'] AS INT) AS age
FROM dim_users
WHERE vip_level >= 4;

9.3 统计 Pop 歌手订阅金额 Top 5

查询二级流派为 pop 的歌曲,统计每位歌手的订阅支付总金额和订阅记录数,按总金额降序取前 5 名。

SELECT
  ds.singer,
  SUM(fs.pay_amount) AS total_pay,
  COUNT(*) AS subscription_count
FROM fact_subscriptions fs
JOIN dim_songs ds
  ON fs.song_id = ds.song_id
WHERE ds.genre_l2 = 'pop'
GROUP BY ds.singer
ORDER BY total_pay DESC
LIMIT 5;

9.4 统计发生过退订的歌曲

通过订阅记录关联退订表,找出曾被退订过至少一次的歌曲,输出歌曲编号、歌曲名称和退订次数。

SELECT
  ds.song_id,
  ds.song_name,
  COUNT(fu.unsub_id) AS unsubscribe_count
FROM fact_subscriptions fs
JOIN fact_unsubscribes fu
  ON fs.sub_id = fu.sub_id
JOIN dim_songs ds
  ON fs.song_id = ds.song_id
GROUP BY ds.song_id, ds.song_name
HAVING COUNT(fu.unsub_id) >= 1;

9.5 找出首单支付超过 50 元的用户

首单定义为每位用户按 sub_date 升序排列后的第一条订阅支付记录。假设同一天只有一条记录。

WITH ranked_subscriptions AS (
  SELECT
    user_id,
    pay_amount,
    sub_date,
    ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY sub_date ASC
    ) AS rn
  FROM fact_subscriptions
)
SELECT
  u.user_id,
  u.user_name,
  r.pay_amount AS first_pay_amount,
  r.sub_date AS first_sub_date
FROM ranked_subscriptions r
JOIN dim_users u
  ON r.user_id = u.user_id
WHERE r.rn = 1
  AND r.pay_amount > 50;