登录日志接入:窗口函数是什么

增长团队接入 30 万行登录日志,第一个需求就让你愣住:「每条登录记录旁边,带上这个用户总共登录过几次。」--行不能被压掉,还得聚合。

学 45 min
练 60 min
盘 15 min
共 120 分钟

学 · 45 min

  1. 01OVER (PARTITION BY ... ORDER BY ...) 三个部分各自的作用

    场景「每条登录记录旁边带上该用户的登录总次数」--既不能丢行,又要有聚合值。

    公式:聚合/排名函数 + over (...) = 窗口函数。partition by user_id 把行按用户分堆(像 group by,但只是「围出计算范围」);order by 在堆内排序(排名、位移类函数必需);括号空着 = 全表一大堆。

    select user_id, login_at,
           count(*) over (partition by user_id) as 该用户登录总次数
    from user_logins
    limit 10;

    易错over() 不能省:聚合函数带 over 才是窗口函数,不带就是普通聚合(受 group by 管辖)。多写一个括号,语义两个世界。

  2. 02与 GROUP BY 的本质区别:不压缩行数

    场景用 group by 只能拿到「每个用户登录几次」4 行;需求要的是 30 万行明细、每行带个数。

    group by 把 N 行压成每组织一行;窗口函数 N 行进去 N 行出来,只是每行旁边多了几列计算结果。判断口诀:需求要「只看汇总」用 group by;要「明细和统计值同框」用窗口。以前用「group by 再 join 回去」实现的报表,全部可以一句话换成窗口。

    -- group by:1 万行(每用户一行)
    select user_id, count(*) from user_logins group by user_id;
    -- 窗口:30 万行(每条记录一行,旁边带总数)
    select user_id, login_at, count(*) over (partition by user_id) from user_logins;

    易错看到「group by 完又 join 回原表拿明细」的 SQL,一律是窗口函数的存量债。

  3. 03窗口函数在逻辑执行顺序中的位置(HAVING 之后、ORDER BY 之前)

    场景D6 背的九步顺序,今天要插进一个新步骤。

    窗口函数在 SELECT 阶段计算,位置在 HAVING 之后、DISTINCT / ORDER BY 之前。这意味着两件事:① 它看到的是 WHERE / HAVING 过滤之后的行(过滤条件影响了它的计算范围);② WHERE 执行时它还不存在(下一条的坑)。

    -- 先过滤(WHERE)再算窗口:count 数的是"过滤后"的行
    select user_id, login_at,
           count(*) over (partition by user_id) as n
    from user_logins
    where login_at >= current_date - 30;   -- 窗口只看到最近 30 天的行

    易错想要「全量统计 + 只显示部分行」时,别在 WHERE 里过滤--先窗口再在外层过滤(子查询包一层)。

  4. 04因此不能写在 WHERE / GROUP BY / HAVING 里

    场景你顺手写了 where count(*) over (...) > 5,报错 window functions are not allowed in WHERE。

    原因就是执行顺序:WHERE 是第 3 步,窗口值第 6 步才算出来,WHERE 执行时它根本不存在。标准修法:子查询里先算窗口,外层再过滤。这个「包一层」的动作本周要形成肌肉记忆,天天用。

    select *
    from (
      select user_id, login_at,
             count(*) over (partition by user_id) as n
      from user_logins
    ) t
    where t.n > 5;      -- 过滤窗口结果,必须在子查询外面

    易错同理 HAVING 里也不能用窗口函数;排序键里可以用(ORDER BY 在 SELECT 之后),但几乎没这个需求。

  5. 05WINDOW w AS (...) 复用窗口定义

    场景同一条 SQL 里三个函数要用同一个窗口,写三遍 partition by user_id order by login_at 太啰嗦。

    window w as (partition by user_id order by login_at) 定义一次,函数处写 over w 复用。改窗口定义只改一处,长的分析 SQL 里非常值得。

    select user_id, login_at,
           row_number()     over w as rn,
           lag(login_at)    over w as 上一条,
           sum(1)           over w as 累计条数
    from user_logins
    window w as (partition by user_id order by login_at)
    limit 10;

    易错WINDOW 子句只在同一个 SELECT 里生效,CTE / 子查询里得各自定义。

练 · 60 min

  1. 跑 seed.sql「W4」段导入 user_logins(30 万行)
    参考答案

    前提是 §B 的 users 已经灌过(§D 从 users 取全部 1 万个用户)。每个用户在自己的活跃窗口内散布 10~50 次登录,这个结构是后面「连续登录」题能查出答案的关键。跑完核对行数,别重复灌。

    -- 跑 seed.sql 的 §D 段(页面文案有时叫「W4」段):一条 INSERT,从 users 生成登录日志
    select count(*) from user_logins;   -- ≈30 万行
  2. 一条 SQL 同时输出每条登录记录和该用户的登录总次数
    参考答案

    窗口函数 N 行进 N 行出:30 万行明细一行不少,每行旁边多了该用户的总次数。partition by 只「围出计算范围」,不压缩行。

    select user_id, login_at,
           count(*) over (partition by user_id) as 该用户登录总次数
    from user_logins
    limit 10;
  3. 用 GROUP BY 实现「每用户登录次数」,对比两者返回行数
    参考答案

    行数差 30 倍就是两者的本质区别:GROUP BY 把行压成每组一行,窗口函数保留全部明细。要「只看汇总」用前者,要「明细和统计值同框」用后者。

    -- GROUP BY:每用户一行
    select user_id, count(*) as 次数
    from user_logins
    group by user_id;                                    -- ≈1 万行
    
    -- 窗口:每条登录记录一行,旁边带总次数
    select user_id, login_at,
           count(*) over (partition by user_id) as 次数
    from user_logins;                                    -- ≈30 万行
  4. 试着在 WHERE 里用窗口函数,记下报错;再用子查询包一层解决
    参考答案

    WHERE 是九步顺序的第 3 步,窗口值第 6 步(SELECT 阶段)才算出来。「子查询包一层」是本周天天要做的肌肉记忆动作。

    select user_id, login_at
    from user_logins
    where count(*) over (partition by user_id) > 5;
    -- ERROR: window functions are not allowed in WHERE
    
    -- 正确姿势:子查询里先算窗口,外层再过滤
    select *
    from (
      select user_id, login_at,
             count(*) over (partition by user_id) as n
      from user_logins
    ) t
    where t.n > 5;
  5. WINDOW 子句定义一次窗口,被三个函数复用
    参考答案

    三个函数共享同一个窗口定义,改一处全部生效。注意:带了 order by 之后 sum(1) 变成「到当前行为止的累计条数」--默认框架在起作用,D25 专门拆它。

    select user_id, login_at,
           row_number()  over w as rn,
           lag(login_at) over w as 上一条,
           sum(1)        over w as 累计条数
    from user_logins
    window w as (partition by user_id order by login_at)
    limit 10;
过关标准 用一句话讲清窗口函数和 GROUP BY 的区别,并解释为什么 WHERE 里用不了。