游戏玩法分析 I

活动表 Activity

+--------------+---------+
| Column Name  | Type    |
+--------------+---------+
| player_id    | int     |
| device_id    | int     |
| event_date   | date    |
| games_played | int     |
+--------------+---------+
在 SQL 中,表的主键是 (player_id, event_date)。
这张表展示了一些游戏玩家在游戏平台上的行为活动。
每行数据记录了一名玩家在退出平台之前,当天使用同一台设备登录平台后打开的游戏的数目(可能是 0 个)。

查询每位玩家 第一次登录平台的日期

查询结果的格式如下所示:

Activity 表:
+-----------+-----------+------------+--------------+
| player_id | device_id | event_date | games_played |
+-----------+-----------+------------+--------------+
| 1         | 2         | 2016-03-01 | 5            |
| 1         | 2         | 2016-05-02 | 6            |
| 2         | 3         | 2017-06-25 | 1            |
| 3         | 1         | 2016-03-02 | 0            |
| 3         | 4         | 2018-07-03 | 5            |
+-----------+-----------+------------+--------------+

Result 表:
+-----------+-------------+
| player_id | first_login |
+-----------+-------------+
| 1         | 2016-03-01  |
| 2         | 2017-06-25  |
| 3         | 2016-03-02  |
+-----------+-------------+

方法 1:分组并提取最小值
思路
通过 GROUP BY player_id 将行分组为每个玩家的子组。
通过 MIN() 找到每个子组内的 最早 event_date。
返回结果集,不需要特定的顺序(这里不需要 ORDER BY)。

MySQL

如果您第一次使用带有类似 MIN() 的 GROUP BY,那么考虑一下从以下查询中可以获取什么样的聚合信息:

SELECT
  A.player_id
FROM
  Activity A
GROUP BY
  A.player_id;


这个查询的结果可能一开始并不令人兴奋:

+-----------+
| player_id |
+-----------+
|         1 |
|         2 |
|         3 |
+-----------+
但是,使用问题描述中的示例,一旦将行按 player_id 分组,实际上我们对于每个组(即每个 player_id)有以下信息:

1:
device_id: 2, 2
event_date: 2016-03-01, 2016-05-02
games_played: 5, 6
2:
device_id: 3
event_date: 2017-06-25
games_played: 1
3:
device_id: 1, 4
event_date: 2016-03-02, 2018-07-03
games_played: 0, 5
上面的拆分使得很清楚我们总共有三个组(每个不同的 player_id 对应一个组)。对于 player_id 值为 1、2 和 3 的玩家,我们分别有 2、1 和 2 个子组。我们使用的聚合函数将应用于每个组的子组值。由于我们对每个玩家的第一个登录日期感兴趣,所以我们将使用 MIN() 聚合函数来扫描每个组的 event_date 子组值,以找到最小的日期。每个组的最小值将作为报告的 first_login 值:

SELECT
  A.player_id,
  MIN(A.event_date) AS first_login
FROM
  Activity A
GROUP BY
  A.player_id;


方法 2:窗口函数
思路
以下解决方案使用窗口函数,应该被视为一个简单问题的中级解决方案。应该指出,仅仅因为您 可以 使用窗口函数,并不意味着您 应该 使用窗口函数。

实现
MySQL
可以在内联视图中使用 RANK()、DENSE_RANK() 或 ROW_NUMBER() 与之结合使用。选择不会影响结果,因为 (player_id, event_date) 是 Activity 表的主键(即我们不必担心有多个行具有 rnk 值为 1 的可能性,因为分区是由 player_id 创建的,行是按 event_date 排序的,从而保证了唯一的 rnk 值):

SELECT
  X.player_id,
  X.event_date AS first_login
FROM
  (
    SELECT
      A.player_id,
      A.event_date,
      RANK() OVER (
        PARTITION BY
          A.player_id
        ORDER BY
          A.event_date
      ) AS rnk
    FROM
      Activity A
  ) X
WHERE
  X.rnk = 1;


MySQL
合适地说,可以使用 FIRST_VALUE()

窗口函数来构建此问题的解决方案:

SELECT DISTINCT
  A.player_id,
  FIRST_VALUE(A.event_date) OVER (
    PARTITION BY
      A.player_id
    ORDER BY
      A.event_date
  ) AS first_login
FROM
  Activity A;


注意: 使用 DISTINCT 是必要的,因为窗口函数的工作方式。如果我们不使用 DISTINCT,那么根据问题描述中的示例,我们将得到以下(不正确)的结果集:

+-----------+-------------+
| player_id | first_login |
+-----------+-------------+
|         1 | 2016-03-01  |
|         1 | 2016-03-01  |
|         2 | 2017-06-25  |
|         3 | 2016-03-02  |
|         3 | 2016-03-02  |
+-----------+-------------+

MySQL
对于那些好奇的人,还可以使用 LAST_VALUE() 窗口函数来构建此问题的解决方案,但必须注意有效地定义 窗口函数框架规范。如果我们没有提供框架规范,那么根据问题描述中的示例,我们将得到以下(不正确)的结果集:

+-----------+-------------+
| player_id | first_login |
+-----------+-------------+
|         1 | 2016-05-02  |
|         1 | 2016-03-01  |
|         2 | 2017-06-25  |
|         3 | 2018-07-03  |
|         3 | 2016-03-02  |
+-----------+-------------+
正如 MySQL文档 中所述,使用窗口函数内的 ORDER BY 将得到以下默认框架规范:

RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
如果未指定 ORDER BY,则默认框架规范如下:

RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
因此,当使用 LAST_VALUE() 时,适当的框架规范将如下所示:

SELECT DISTINCT
  A.player_id,
  LAST_VALUE(A.event_date) OVER (
    PARTITION BY
      A.player_id
    ORDER BY
      A.event_date DESC RANGE BETWEEN UNBOUNDED PRECEDING
      AND UNBOUNDED FOLLOWING
  ) AS first_login
FROM
  Activity A;


结论
我们推荐使用方法 1 ,因为它很简单。

方法 2 非常有用,因为它突出了许多替代解决方案,这些解决方案在面试中值得注意。如果您在面试中遇到了这个问题,那么方法1应该足够了。但是,使用方法 2 中的任何解决方案仍然会给面试官留下良好的印象,因为它暗示您可以用多种方式找到问题的解决方案,无论这种方式有多复杂。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值