导读:本期聚焦于小伙伴创作的《SQL连续登录问题有哪些解法?连续登录天数统计的多种方案对比》,敬请观看详情。统计用户连续登录天数是业务分析中常见需求,但很多人在写SQL时容易把断签和续签混在一起算错。最直观的做法是用自关联把前后日期连起来逐天比对,逻辑清楚但数据量大时关联成本高。后来出现用日期减去行号的方式,把连续日期映射到同一分组值,再用聚合求出每段连续区间长度,这种思路大幅减少了扫描次数。窗口函数中的row_number与date_sub组合现在几乎成了标准解法,配合group by能快速拿到最长连续登录。此外还可以借助临时表先去重再打标。不同方案在可读性与执行效率上差异明显,小表用自关联足够,大表应优先选窗口函数。

在用户行为分析里,连续登录天数是衡量活跃度与留存的核心指标。所谓连续登录,是指用户在自然日维度上登录日期紧挨着、中间没有断开的日子序列。比如用户在一月一号、二号、三号都登录了,这算连续三天;如果四号没登、五号登了,那五号就只能算新的一段连续起点。我们要做的,就是针对每张用户登录记录表,算出每个人最长连续登录了多少天,或者有哪些连续区间。

SQL连续登录问题有哪些解法?连续登录天数统计的多种方案对比

一、自关联比对解法

最容易被想到的办法,是把登录表和自己按用户与日期差一天的条件做连接。这样每条记录都能找到它的前一天是否也登录了,从而判断是否属于连续段。这种方法逻辑非常直白,刚接触SQL的人也能看懂。

下面用一张简单的表 user_login(uid, login_date) 来演示。我们先把同一用户登录日期去重,再左联前一天的数据:

select
  a.uid,
  a.login_date,
  case when b.login_date is not null then 1 else 0 end as is_continue
from (
  select distinct uid, login_date from user_login
) a
left join (
  select distinct uid, login_date from user_login
) b
on a.uid = b.uid
and a.login_date = date_add(b.login_date, interval 1 day);

上面这段只能标出每一天是不是接在上一天后面。要算出连续长度,还得再做变量或子查询累计。自关联的缺点是,如果数据有上亿条,distinct加join会带来很高的临时表和排序开销,执行计划里常常能看到 Using temporary; Using filesort。

不过在小数据量或者临时查数场景下,它不用记复杂函数,改起来也方便。比如只想看某个人有没有连续登录满七天,直接加 where 过滤即可,不需要引入新语法。

二、日期减行号分组解法

这是一种巧妙的数学思路。先对每个用户按登录日期排序打出行号,再用登录日期减去行号对应的天数,连续日期算出来的值会相等。因为每过一天日期加一、行号也加一,相减就抵消了。断掉之后,日期往前跳但行号连续,差值就变了,自然分成不同组。

我们用 date_sub 和 row_number 实现,注意里面特殊字符都做了转义:

select
  uid,
  min(login_date) as start_date,
  max(login_date) as end_date,
  count(*) as continue_days
from (
  select
    uid,
    login_date,
    date_sub(login_date, interval row_number() over (
      partition by uid order by login_date
    ) day) as grp
  from (
    select distinct uid, login_date from user_login
  ) t1
) t2
group by uid, grp;
</p>
<p>这个写法一次性给出了每段连续的起止日和天数。想拿最长连续,只需在外面再包一层求 max(continue_days)。它只需要两次扫描和一次窗口计算,比自关联少了很多比较。</p>
<p>要注意的是,login_date 必须是 date 类型而不是 datetime,否则相减会带时间片段导致分组错位。如果原表是时间戳,先用 date(ts) 处理。</p>

<h2>三、窗口函数直接求最长连续</h2>
<p>在支持窗口函数的数据库如 MySQL 8、PostgreSQL、Hive 中,上面第二种方案就是业界主流。我们可以再精简,直接求每个人最长连续天数:</p>
<pre class=brush:sql;toolbar:false>
select uid, max(continue_days) as max_continue
from (
  select
    uid,
    count(*) as continue_days
  from (
    select
      uid,
      login_date,
      date_sub(login_date, interval row_number() over (
        partition by uid order by login_date
      ) day) as grp
    from (select distinct uid, login_date from user_login) x
  ) y
  group by uid, grp
) z
group by uid;

这种结构清晰,而且数据库优化器对 window function 有专门的向量化执行,在千万级数据上通常比自关联快数倍。它也是面试里公认的标准答案。

如果还要输出连续区间明细,把中间层留着即可,外层不聚合 uid 而只按 uid、grp 聚就能拿到所有段。可见同一套核心逻辑既能算最长也能算全量,复用度高。

四、临时表与标记法

有些老版本 MySQL 不支持窗口函数,可以用临时表加用户变量模拟行号与分组。思路是先建去重排序表,再读的时候用 @prev 和 @grp 变量判断断层:

create temporary table tmp_login as
select uid, login_date from user_login group by uid, login_date order by uid, login_date;

set @uid := '', @prev_date := null, @grp := 0;
select uid, grp, min(login_date) start_date, max(login_date) end_date, count(*) days
from (
  select
    uid, login_date,
    @grp := if(@uid = uid and datediff(login_date, @prev_date) = 1, @grp, if(@uid := uid, @grp + 1, @grp + 1)) as grp,
    @prev_date := login_date
  from tmp_login
) t
group by uid, grp;

变量法在旧系统能跑,但可读性差,而且 MySQL 8 之后官方不推荐在查询里用用户变量做行级计算,容易和优化器冲突。所以新项目直接用窗口函数就好。

五、方案对比与选型

我们把四种解法从多个维度列出来:

方案可读性大数据性能数据库要求
自关联基础SQL
日期减行号需窗口函数
窗口函数最长中高需窗口函数
临时表变量一般旧版兼容

实际工作中,只要环境支持 row_number,就优先用日期减行号的分组思路。它既能应对日活报表里的连续登录,也能改造成近三十天最大连续这样的滑动需求。自关联只建议用在教学或极小的表上,避免给线上库添堵。

连续登录问题本质是把时间轴变成分组标签,抓住这一点,不管换什么 SQL 方言,都能很快写出对应实现。

SQL连续登录窗口函数修改时间:2026-08-07 13:54:34

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。