Featured image of post DuckDB 解 Gap 和 Island 问题:SQL 差值法搞定连续区间检测

DuckDB 解 Gap 和 Island 问题:SQL 差值法搞定连续区间检测

用 DuckDB 的窗口函数和差值法,一行 SQL 解决登录连续天数、价格涨跌区间、设备在线状态等 Gap 和 Island 经典问题。附完整代码和变现建议。

一、什么是 Gap 和 Island 问题?

在数据分析中,我们经常遇到一类被称为 Gap 和 Island 的问题——识别数据中的连续区间。这类问题在业务场景中非常普遍:

  • 用户留存分析:计算用户的连续登录天数
  • 金融行情:找出股价连续上涨/下跌的区间
  • 设备监控:检测服务器在线/离线的连续时段
  • 订单分析:识别用户连续下单的活跃周期

如果你尝试用普通的 GROUP BY 来解决,会发现根本做不到——因为连续日期在数据库中并不是连续存储的,它们散落在每一行记录中。

经典的解法是利用窗口函数 + 差值法,这个方法优雅、高效,而且在任何支持窗口函数的数据库中都能用。DuckDB 作为分析型数据库,天然擅长这类操作。

二、核心原理:差值法

差值法的核心思想非常简单:

连续日期 - 连续行号 = 相同值

假设你有一组连续日期 2024-01-01, 2024-01-02, 2024-01-03,对应的行号是 1, 2, 3。那么 日期 - 行号 的结果始终是同一个值(2023-12-31)。一旦遇到断档(比如 2024-01-05 缺失了 01-04),行号继续递增但日期跳过了,差值就会改变,从而形成一个新的「岛」。

这就是为什么 连续日期减去连续行号,结果恒定——它是识别连续区间的数学基础。

三、实战案例:用户连续登录天数

3.1 数据准备

假设我们有一张用户登录日志表 login_log

CREATE TABLE login_log (
    user_id INTEGER,
    login_date DATE
);

INSERT INTO login_log VALUES
(1, '2024-01-01'),
(1, '2024-01-02'),
(1, '2024-01-03'),
(1, '2024-01-05'),
(1, '2024-01-06'),
(2, '2024-01-01'),
(2, '2024-01-03'),
(2, '2024-01-04'),
(2, '2024-01-05');

用户 1 在 1 月 1-3 日连续登录,然后断了 1 天,又在 5-6 日连续登录。用户 2 在 1 日单独登录,然后在 3-5 日连续登录。

3.2 差值法实现

WITH numbered AS (
    -- 第一步:给每个用户的登录日期排序并编号
    SELECT user_id, login_date,
           ROW_NUMBER() OVER (
               PARTITION BY user_id 
               ORDER BY login_date
           ) AS rn
    FROM login_log
),
grouped AS (
    -- 第二步:计算日期 - 行号,得到分组标识
    SELECT user_id, login_date,
           login_date - INTERVAL (rn) DAY AS grp
    FROM numbered
)
-- 第三步:按 (user_id, grp) 聚合,得到连续区间
SELECT user_id,
       MIN(login_date) AS start_date,
       MAX(login_date) AS end_date,
       COUNT(*) AS consecutive_days
FROM grouped
GROUP BY user_id, grp
ORDER BY user_id, start_date;

3.3 执行结果

 user_id | start_date |  end_date  | consecutive_days
---------+------------+------------+------------------
       1 | 2024-01-01 | 2024-01-03 |                3
       1 | 2024-01-05 | 2024-01-06 |                2
       2 | 2024-01-01 | 2024-01-01 |                1
       2 | 2024-01-03 | 2024-01-05 |                3

结果完美展示了每个用户的连续登录区间。

3.4 逐步拆解

CTE作用关键操作
numbered编号ROW_NUMBER() 按用户分区排序
grouped分组login_date - rn DAYS 生成组标识
最终查询聚合GROUP BY user_id, grp 汇总区间

理解这一步是关键:当日期连续时,login_date - rn 的值保持不变;一旦断档,这个值就会跳变,自然形成新的分组。

四、进阶场景

4.1 价格涨跌区间检测

在金融分析中,识别连续上涨/下跌区间非常有价值:

WITH price_changes AS (
    SELECT date, price,
           LAG(price) OVER (ORDER BY date) AS prev_price,
           CASE 
               WHEN price > LAG(price) OVER (ORDER BY date) THEN 'UP'
               WHEN price < LAG(price) OVER (ORDER BY date) THEN 'DOWN'
               ELSE 'SAME'
           END AS trend
    FROM stock_prices
),
numbered AS (
    SELECT date, price, trend,
           ROW_NUMBER() OVER (PARTITION BY trend ORDER BY date) AS rn
    FROM price_changes
    WHERE trend != 'SAME'
)
SELECT trend,
       MIN(date) AS start_date,
       MAX(date) AS end_date,
       COUNT(*) AS days,
       FIRST_VALUE(price) OVER (PARTITION BY trend, date - INTERVAL rn DAY ORDER BY date) AS start_price,
       LAST_VALUE(price) OVER (PARTITION BY trend, date - INTERVAL rn DAY ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS end_price
FROM numbered
GROUP BY trend, date - INTERVAL rn DAY
ORDER BY start_date;

4.2 设备在线状态分析

WITH numbered AS (
    SELECT device_id, status_time,
           ROW_NUMBER() OVER (PARTITION BY device_id, status ORDER BY status_time) AS rn
    FROM device_events
)
SELECT device_id,
       status,
       MIN(status_time) AS start_time,
       MAX(status_time) AS end_time,
       COUNT(*) AS duration_minutes
FROM numbered
GROUP BY device_id, status, status_time - INTERVAL rn MINUTE
ORDER BY start_time;

4.3 使用 DuckDB 的 QUALIFY 简化

DuckDB 支持 QUALIFY 子句,可以进一步简化查询:

WITH numbered AS (
    SELECT user_id, login_date,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS rn
    FROM login_log
)
SELECT user_id,
       MIN(login_date) AS start_date,
       MAX(login_date) AS end_date,
       COUNT(*) AS consecutive_days
FROM numbered
GROUP BY user_id, login_date - INTERVAL rn DAY
QUALIFY ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY MIN(login_date)) <= 5;

五、DuckDB vs 传统工具对比

特性DuckDBPostgreSQLMySQL 8.0pandasSpark
差值法支持✅ 原生✅ 原生✅ 8.0+✅ Python✅ Scala/Py
安装复杂度零配置需部署需部署pip install集群部署
内存效率列式+向量化行式行式内存全部加载分布式
查询性能极快中等中等慢(大集群)
SQL 表达力完整窗口函数完整部分支持无 SQL有限
适合场景单机分析事务+分析事务为主数据科学大数据

结论:对于 Gap/Island 这类分析型查询,DuckDB 在开发效率和运行性能上都是最佳选择。

六、性能优化建议

6.1 索引策略

-- 为登录日志表创建复合索引
CREATE INDEX idx_login_user_date ON login_log (user_id, login_date);

6.2 分区表

对于超大数据量,可以按日期分区:

-- DuckDB 支持外部表分区
SELECT * FROM read_csv_auto('login_logs/*.csv');

6.3 并行处理

DuckDB 自动利用多核并行执行窗口函数,对于百万级数据通常只需秒级响应。

七、变现建议

掌握 Gap 和 Island 问题的解法,可以衍生出多个商业应用方向:

7.1 用户留存 SaaS 工具

开发一个面向中小电商的用户行为分析 SaaS,核心功能就是连续登录天数、连续购买周期分析。按月订阅定价 ¥99-499/月。

7.2 金融数据产品

搭建股票连续涨跌区间监控系统,为量化投资者提供信号触发服务。可以做成 API 服务,按调用量收费。

7.3 设备监控服务

为 IoT 设备厂商提供在线状态分析服务,帮助识别设备故障模式和用户活跃度。按设备数量收费。

7.4 技术顾问服务

将这套技术包装成数据分析培训课程,面向数据分析师和后端工程师,单节课收费 ¥199-999。

7.5 DuckDB 咨询服务

作为 DuckDB 专家,为企业提供实时数据分析架构设计和性能优化服务,按项目收费 ¥5000-50000。


这篇文章基于 DuckDB 频道「DuckDB 掘金实战」的每日推送整理。更多实战技巧请访问 duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计

⚠️ 本站为独立社区项目,与 DuckDB 基金会及 DuckDB 官方项目无任何从属、背书或赞助关系。

"DuckDB" 是 DuckDB 基金会的注册商标,本站仅以事实描述方式使用该名称。

本站内容仅供教育与社区推广用途,不构成任何商业服务。