“连续状态”需先识别:SQL无原生连续语义,必须用LAG(status)找状态变化点,再用SUM() OVER构造分组标识;关键依赖严格有序字段(如id或event_time),并显式处理NULL和边界情况。

什么是“连续状态”需要先识别出来
SQL 本身不自带“连续性”语义,ROW_NUMBER()、LAG()、GROUPS 这些函数不会自动感知“连续”。必须靠时间戳或序号字段(如 id、created_at)+ 状态字段(如 status)联合判断:同一状态在物理顺序上是否相邻。
关键思路是:用 LAG(status) 找出状态变化点,再用 SUM() 窗口函数打组。不是所有数据库都支持 GROUPS 模式(PostgreSQL 14+、Oracle),所以更通用的做法是构造分组标识。
实操建议:
- 确保有严格递增/有序的排序字段(比如
id或event_time),否则“连续”无意义 - 先用
LAG(status) OVER (ORDER BY id)判断当前行与上一行状态是否不同 - 用
CASE WHEN status != LAG(status) THEN 1 ELSE 0 END标记变化点 - 再对这个标记做
SUM() OVER (ORDER BY id ROWS UNBOUNDED PRECEDING)得到每个连续段的唯一组号
PostgreSQL / MySQL 8.0+ 的典型写法
以订单状态流转表为例,字段为 id、order_id、status、updated_at:
SELECT
order_id,
status,
MIN(updated_at) AS start_time,
MAX(updated_at) AS end_time,
COUNT(*) AS duration_count
FROM (
SELECT *,
SUM(is_new_group) OVER (ORDER BY id ROWS UNBOUNDED PRECEDING) AS grp
FROM (
SELECT *,
CASE
WHEN status != LAG(status) OVER (ORDER BY id) THEN 1
ELSE 0
END AS is_new_group
FROM order_status_log
) t1
) t2
GROUP BY order_id, status, grp;
注意点:
-
LAG()默认返回NULL,第一行会误判为“状态变化”,所以实际中常加COALESCE(LAG(status), '___')避免歧义 - MySQL 5.7 不支持窗口函数,这种写法直接报错
ERROR 1064;必须升级或改用自连接模拟 - 如果
id不连续(比如有删行),但updated_at是严格递增的,就把ORDER BY id换成ORDER BY updated_at, id防止时序错乱
遇到 NULL 状态或中间插入记录怎么办
连续性判断对空值极其敏感:LAG(status) 返回 NULL,而 NULL != 'shipped' 为 UNKNOWN(不是 TRUE),导致 CASE 不触发。结果就是:含 NULL 的行永远被归进前一个组,或单独成组,取决于写法。
安全写法是显式处理 NULL:
- 把状态转成非空占位符:
COALESCE(status, '__MISSING__') - 比较时用
IS DISTINCT FROM(PostgreSQL)或(a IS NULL) != (b IS NULL) OR a != b(兼容写法) - 如果业务允许跳过 NULL 记录,可提前
WHERE status IS NOT NULL过滤,但要注意这会破坏原始时间线
性能和索引怎么配合
这类查询本质是全表扫描 + 多层嵌套窗口计算,没索引时大表(百万行以上)很容易慢。核心优化点不在 SQL 写法,而在支撑结构:
- 必须在
ORDER BY 字段上有索引,例如 CREATE INDEX idx_log_order_time ON order_status_log (order_id, updated_at)
- 如果只查单个
order_id,复合索引要把 order_id 放最左;如果全局合并,则优先 updated_at 或 id
- 避免在窗口函数里用
ORDER BY 表达式(如 DATE(updated_at)),会导致无法走索引
- 物化中间结果(如用 CTE 或临时表存
grp)有时比一层嵌套快,尤其当外层 GROUP BY 字段多时
连续段合并真正难的不是语法,而是状态定义是否稳定、时间字段是否可信、以及 NULL 是否被当作合法状态参与分组——这些不厘清,窗口函数写得再漂亮也会产出错误区间。
ORDER BY 字段上有索引,例如 CREATE INDEX idx_log_order_time ON order_status_log (order_id, updated_at)
order_id,复合索引要把 order_id 放最左;如果全局合并,则优先 updated_at 或 id
ORDER BY 表达式(如 DATE(updated_at)),会导致无法走索引grp)有时比一层嵌套快,尤其当外层 GROUP BY 字段多时










