LAG()需在未聚合的明细数据上运行,通过CASE WHEN col != LAG(col)生成变化标记,再用SUM()窗口累计;GROUP BY会破坏时序致LAG失效,且须处理NULL、重复时间戳及跨库兼容性问题。

直接用 LAG() 比较相邻行是否不同,再对“变化标记”做窗口累加,就能得到每组内到当前行为止的变化次数;这不是统计极差或标准差,而是数「切换动作」本身。
为什么 COUNT(*) + GROUP BY 不行
分组聚合会打乱原始顺序,而“变化”依赖严格时序。一旦用 GROUP BY category 先聚合,就丢失了行与行之间的先后关系,LAG() 失去作用基础。
-
LAG()必须在未聚合的明细数据上运行,且ORDER BY字段必须有确定、唯一、非空的排序逻辑 - 如果原始表含重复时间戳(如多条
2026-04-01 00:00:00记录),LAG()可能跳过真实变化点——得先用ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY ts, id)补序号,或提前DISTINCT ON (group_col, DATE(ts))(PostgreSQL)去重 - MySQL 8.0 对
ORDER BY中 NULL 值更敏感:只要某行ts IS NULL,它之后所有LAG()结果都可能异常,建议先WHERE ts IS NOT NULL
怎么写“变化标记”列
核心是把“值变了没”转成 0/1,再用 SUM() 窗口函数累计。不能直接 COUNT(IF(...)),因为窗口 COUNT() 不支持条件表达式(除非用 COUNT(CASE WHEN ... THEN 1 END) OVER (...),但语义不如 SUM() 直观)。
- 正确写法:
CASE WHEN col != LAG(col) OVER (PARTITION BY group_col ORDER BY ts) THEN 1 ELSE 0 END AS changed_flag - 注意:字符串比较要小心大小写和空格,必要时加
TRIM(UPPER(col)) - 数值型字段若含
NULL,col != LAG(col)会返回NULL而非TRUE,应改用NOT (col LAG(col))(MySQL)或col IS DISTINCT FROM LAG(col)(PostgreSQL/SQL Server) - 外层再套
SUM(changed_flag) OVER (PARTITION BY group_col ORDER BY ts) AS change_count_so_far
如何查“变化最频繁的项”
变化频率不是看总数最大,而是单位时间内切换次数最多。所以得先算出每组的总变化次数,再除以该组的时间跨度(或记录数),最后取 TOP 1。
- 先用 CTE 或子查询生成带
change_count_so_far的中间结果 - 在外层按
group_col聚合:MAX(change_count_so_far) AS total_changes,同时算时间跨度:MAX(ts) - MIN(ts)(日期类型)或COUNT(*)(若只关心记录密度) - 计算频率:
total_changes * 1.0 / NULLIF(DATEDIFF('day', MIN(ts), MAX(ts)), 0)(MySQL)或total_changes::float / NULLIF(MAX(ts) - MIN(ts), 0)(PostgreSQL) - 避免除零:一律用
NULLIF(denominator, 0),别写CASE WHEN denominator = 0 THEN NULL ELSE ... END - 最终用
ORDER BY freq DESC LIMIT 1取最高频项——注意,如果多组并列第一,需额外处理去重逻辑
最容易被忽略的是:变化标记列必须保留所有原始行,包括变化起点(此时 LAG() 为 NULL,changed_flag 应为 1),删掉它们会导致整个计数偏移。另外,跨数据库时 IS DISTINCT FROM 和 的兼容性差异,常让同一语句在 PostgreSQL 和 MySQL 上行为不一致。

















