直接用 rand() 无法实现加权随机,因其生成均匀分布,而权重要求高权重项概率更高;-log(1-rand())/weight 利用指数分布特性,使小值更可能来自高权重行,取最小值即实现按权重抽样。

为什么直接用 RAND() 无法实现加权随机?
因为 RAND() 给每行分配的是均匀分布的 [0,1) 随机数,而权重要求高权重项被选中的概率更高。如果简单按 ORDER BY RAND() * weight 排序取 TOP 1,结果会严重偏向低权重(小 weight)——因为 RAND() * weight 的期望值随 weight 增大而增大,但排序后最大值反而最“靠后”,逻辑反了。
LOG(1-RAND()) / weight 是怎么让高权重更易被选中的?
这是基于指数分布采样的经典技巧:对每个候选行,计算 -LOG(1 - RAND()) / weight(LOG 默认自然对数,SQL Server 用 LOG(),MySQL 用 LN(),PostgreSQL 用 LN())。这个值服从参数为 weight 的指数分布,其概率密度函数正比于 weight * EXP(-weight * x),所以较小的生成值更可能来自高 weight 行。我们取最小值对应的行,就等价于按权重概率抽样。
实操要点:
- 必须用
1 - RAND()避免LOG(0)(RAND()可能返回 0,但几乎不返回 1;1-RAND()则避开 0,保证对数有定义) - SQL Server 中需写成
-LOG(1 - RAND()) / NULLIF(weight, 0),防止 weight 为 0 导致除零错误 - MySQL 8.0+ 支持窗口函数,可直接在子查询中计算并
ORDER BY computed_score LIMIT 1;SQL Server 需用TOP 1 WITH TIES或 CTE +ROW_NUMBER()
SQL Server 存储过程中完整写法示例
假设表 #candidates 有字段 id、name、weight,要抽 1 条:
DECLARE @selected_id INT; SELECT TOP 1 @selected_id = id FROM #candidates WHERE weight > 0 ORDER BY -LOG(1 - RAND(CHECKSUM(NEWID()))) / CAST(weight AS FLOAT);
注意点:
-
RAND(CHECKSUM(NEWID()))确保每行获得独立随机数(否则单条RAND()在整个 SELECT 中只计算一次) - 显式转
CAST(weight AS FLOAT)防止整数除法截断 - 必须过滤
weight > 0,否则LOG无效或除零 - 若需抽多条(如 3 条不重复),不能简单用
TOP 3—— 指数采样天然支持无放回,但需用递归 CTE 或临时表标记已选,否则概率失真
容易被忽略的边界问题
权重为浮点小数(如 0.001)、含负数、全为零、或仅一行权重非零时,公式行为会异常:
- 负 weight 会导致
-LOG(1-RAND())/weight为正数变小,逻辑崩溃;务必WHERE weight > 0 - weight 极小(如 1e-10)会使对应行的
-LOG(...)/weight极大,几乎不可能被选中——这不是 bug,是数学本质;若业务上“极小权重≈应忽略”,提前过滤更稳妥 - 所有 weight 都为 0 → 查询无结果,需在存储过程开头加校验:
IF NOT EXISTS (SELECT 1 FROM #candidates WHERE weight > 0) RAISERROR('No valid weight', 16, 1) - 某些旧版 SQL Server 对
LOG输入精度敏感,遇到RAND()返回极接近 1 的值(如 0.999999999)时,1-RAND()可能下溢为 0;加MAX(1e-15, 1 - RAND(...))更鲁棒
真正难的不是公式本身,而是把 weight 的业务语义和指数分布的数学行为对齐——比如“权重=点击率”和“权重=倒排排名分”,是否都适合用同一套归一化方式,得看下游是否真的需要严格按概率比例抽样。










