LAG()和LEAD()判断波峰波谷的本质是识别局部极值:波峰需大于前后值,波谷需小于前后值;须按唯一排序列(如ts, id)开窗,显式处理NULL,并结合业务规则过滤无效极值。

用 LAG() 和 LEAD() 判断相邻趋势
波峰和波谷的本质是局部极值:波峰比前一个和后一个都大,波谷比前后都小。SQL 没有内置“找极值”函数,但 LAG() 和 LEAD() 能拿到相邻行的值,这是判断的基础。
关键点在于:必须明确排序依据(比如时间戳或序号),否则“前后”无意义。如果排序列有重复值,需加唯一键(如主键)到 ORDER BY 中避免窗口行为不确定。
- 写法示例:
LAG(value) OVER (ORDER BY ts, id)——ts是时间,id防重复 - 别直接用
LAG(value) OVER (ORDER BY ts),当多个记录ts相同时,LAG返回结果可能随机 - 注意 NULL:首行无前驱,
LAG返回 NULL;末行无后继,LEAD返回 NULL;比较时要显式处理,例如用value > LAG(value) AND value > LEAD(value)会因 NULL 失败,应改用value > COALESCE(LAG(value), -INF)
用布尔表达式标记波峰波谷
标记不是目的,但它是后续过滤或聚合的前提。用 CASE + 窗口值组合出清晰的逻辑分支最稳妥,避免把多个条件塞进 WHERE 里导致可读性崩坏。
- 波峰判断:
CASE WHEN value > LAG(value) AND value > LEAD(value) THEN 'peak' END - 波谷判断:
CASE WHEN value - 如果想合并标记,可统一用数值:1 表示波峰,-1 表示波谷,0 表示非极值,方便后续统计
- 不要忽略边界:第一行和最后一行天然无法成为完整波峰/波谷(缺一侧邻居),所以它们的标记字段一定是 NULL 或默认值,这点在业务语义上通常合理
处理平台兼容性差异
PostgreSQL、SQL Server、BigQuery 支持标准窗口函数,但 MySQL 8.0+ 才支持 LAG/LEAD,而旧版 MySQL 或 SQLite(除非 3.25+ 且启用窗口函数)根本不可用。别假设环境一致。
- MySQL 5.7 或更早:只能靠自连接或变量模拟,性能差且易出错,不推荐用于实时查询
- SQLite:确认版本 ≥ 3.25 且编译时启用了
ENABLE_WINDOW_FUNCTIONS - 某些云数仓(如 Redshift)对
LEAD的OFFSET参数支持有限,只认LEAD(value, 1),不能写LEAD(value, 2) - 如果目标平台不支持,优先考虑把数据拉到应用层用 Pandas(
diff()+sign())或 Python 列表遍历处理
性能与数据质量陷阱
窗口函数本身不慢,但错误的数据结构会让它变成性能黑洞。尤其当原始数据未按时间/序列排序、或存在大量重复时间戳时,ORDER BY 代价飙升,还可能让 LAG/LEAD 返回错误邻居。
- 务必在排序字段上建索引,特别是时间列(如
CREATE INDEX idx_ts ON sensor_data(ts)) - 检查是否存在异常平顶:连续多行值相同(如
[5,5,5,4]),此时中间的 5 不构成波峰——但LAG=5, LEAD=5,5 > 5为假,逻辑上正确;不过若业务定义“等于也算局部高点”,就得改用 >= 并小心处理平台对 NULL 的三值逻辑 - 高频采集数据(如毫秒级)容易产生密集微波动,建议先按时间窗口下采样(如取每秒最大值),再找波峰,否则可能返回成百上千个无效极值
真正的难点不在语法,而在定义清楚“什么算一个有效的波峰”:是否要过滤振幅低于阈值的?是否要求两侧下降持续至少 N 个点?这些业务规则没法靠窗口函数自动推导,得靠外层过滤或应用层补全。










