min-max归一化必须用窗口函数而非子查询,因后者引发三次全表扫描且where不同步导致错误;正确写法为(col - min(col) over(partition by category)) / nullif(max(col) over(partition by category) - min(col) over(partition by category), 0),nullif防除零。

Min-Max 归一化必须用 MIN() 和 MAX() 窗口函数,不能靠子查询
直接在主查询里嵌套三个子查询(SELECT MIN(col)、SELECT MAX(col)、主表扫描)看似能算出归一值,但实际会触发三次全表扫描,且 WHERE 条件不同步时结果完全错误。窗口函数才是唯一靠谱解法:MIN(col) OVER(PARTITION BY group_col) 一次扫描就完成分组极值广播。
-
OVER(PARTITION BY category)表示按category分组计算每组的MIN/MAX,不是整表统算 - 公式写成
(col - MIN(col) OVER(PARTITION BY category)) / NULLIF(MAX(col) OVER(PARTITION BY category) - MIN(col) OVER(PARTITION BY category), 0),NULLIF防除零必不可少 - 如果某组所有
col值相同,分母为 0,NULLIF返回NULL,整行归一化结果为NULL—— 这是预期行为,不是 bug
ROW_NUMBER() 不能直接用于归一化,但能辅助构造分组锚点
归一化本身不依赖排序,但有时业务要求“只对每组最新一条记录做归一化”,这时就得先用 ROW_NUMBER() 标记首行,再在外层过滤。注意它和归一化是两步操作,不能混进同一个 OVER() 里。
-
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY updated_at DESC)必须带ORDER BY,否则语法报错 - 必须用子查询或 CTE 包裹,再
WHERE rn = 1,不能在原查询里直接WHERE ROW_NUMBER() = 1 - 如果归一化字段本身参与排序(比如用
score排序取最高分),要确认该字段是否含NULL:默认NULLS LAST(多数引擎),但 PostgreSQL 可显式写NULLS FIRST
多列同时归一化时,OVER() 要分别写,不能复用
一个 OVER() 子句只能定义一套窗口逻辑。想对 price 按全局归一、quantity 按 category 分组归一,就得各自写独立的窗口表达式,不能共用同一个别名或子查询。
-
(price - MIN(price) OVER()) / NULLIF(MAX(price) OVER() - MIN(price) OVER(), 0)→ 全局归一 -
(quantity - MIN(quantity) OVER(PARTITION BY category)) / NULLIF(MAX(quantity) OVER(PARTITION BY category) - MIN(quantity) OVER(PARTITION BY category), 0)→ 分组归一 - 别试图用 CTE 预先算好极值再 JOIN:窗口函数天然支持并行计算,JOIN 反而引入额外连接开销和 NULL 匹配风险
MySQL 8.0+ 和 PostgreSQL 支持完整语法,旧版 MySQL 不行
如果你用的是 MySQL 5.7 或更早版本,OVER() 会直接报错 ERROR 1064。SQLite 3.25+ 支持基础窗口函数,但不支持 ROWS BETWEEN 等高级帧定义 —— 归一化倒够用,但滚动计算不行。
- 检查版本:MySQL 执行
SELECT VERSION();,确认输出以8.0.开头 - PostgreSQL 用户注意:
MIN()/MAX()窗口函数默认跳过NULL,若需把NULL当作最小值处理,得先用COALESCE(col, -999999)替换 - SQL Server 用户可省略
NULLIF,改用NULLIF(..., 0)或IIF(denom = 0, NULL, ...),语义等价但写法略有差异
归一化表达式里 NULLIF 的位置、PARTITION BY 的粒度、以及引擎对 NULL 的默认处理方式,这三处最容易被忽略。写完一定要用单组全同值(如全为 100)、单组全 NULL、跨组混合数据三类 case 验证输出。











