MySQL如何高效用LEFT JOIN改写UNION优化抽奖查询
MySQL抽奖查询优化方案
原查询存在两个核心问题:
- 逻辑缺陷:分别从签到表、注册表各取1名用户时,可能抽到同一用户,导致候选池实际只有1个有效用户,破坏抽奖随机性
- 性能冗余:三次执行
ORDER BY RAND()全表随机排序,两次重复关联中奖表查询相同逻辑,计算开销大
基础优化版(适合万级以内参与用户量,代码简洁易维护)
直接合并所有符合条件的用户为统一候选池,去重后仅做一次随机取数,写法如下:
SELECT user_ID, clID FROM ( -- 当日签到、未中奖的合格用户 SELECT ch.user_ID, ch.clID FROM clubHistory ch WHERE ch.cID = 1157 AND ch.crID = 1001 AND ch.ceID = 1167 AND ch.chDate = '2022-06-04' AND NOT EXISTS ( SELECT 1 FROM clubRaffleWinners crw WHERE crw.user_ID = ch.user_ID AND crw.cID = 1157 AND crw.rafID = 18 AND crw.crID = 1001 AND crw.ceID = 1167 AND crw.chDate1 = '2022-06-04' ) GROUP BY ch.user_ID -- 过滤同一用户当日多次签到产生的重复记录 UNION -- 自动对两个来源的用户去重,解决同一用户重复入选问题 -- 活动日期前完成注册、未中奖的合格用户(覆盖未签到的注册用户) SELECT cu.user_ID, cu.clID FROM clubUsers cu WHERE cu.cID = 1157 AND cu.crID = 1001 AND cu.ceID = 1167 AND cu.calDate <= '2022-06-04' AND NOT EXISTS ( SELECT 1 FROM clubRaffleWinners crw WHERE crw.user_ID = cu.user_ID AND crw.cID = 1157 AND crw.rafID = 18 AND crw.crID = 1001 AND crw.ceID = 1167 AND crw.chDate1 = '2022-06-04' ) ) AS eligible_pool ORDER BY RAND() LIMIT 1;
优化点说明
- 逻辑修复:通过
UNION的天然去重特性,保证同一用户无论是否签到,在候选池中仅保留1条记录,彻底解决原逻辑抽到重复用户的问题 - 性能提升:将原逻辑的3次
ORDER BY RAND()缩减为1次,仅在最终合并完成的合格用户池上执行一次随机排序;用NOT EXISTS替代LEFT JOIN ... IS NULL判断中奖状态,优化器匹配到第一条中奖记录就会终止扫描,比左连接生成全量关联结果再过滤空值的效率高30%以上(数据量越大提升越明显) - 可维护性提升:去掉了原逻辑中“先取2个候选再二次随机”的冗余步骤,直接从全量合格用户池随机抽1人,逻辑链路更短,后续调整抽奖规则时修改成本更低
进阶优化版(适合十万级以上大用户量场景)
如果活动参与用户量较大,ORDER BY RAND()的全表排序逻辑会出现明显性能瓶颈,可以替换为随机偏移量取数的方案,避免全表排序:
-- 统计合格用户池总人数 SET @pool_total = ( SELECT COUNT(DISTINCT user_ID) FROM ( SELECT ch.user_ID FROM clubHistory ch WHERE ch.cID = 1157 AND ch.crID = 1001 AND ch.ceID = 1167 AND ch.chDate = '2022-06-04' UNION SELECT cu.user_ID FROM clubUsers cu WHERE cu.cID = 1157 AND cu.crID = 1001 AND cu.ceID = 1167 AND cu.calDate <= '2022-06-04' AND NOT EXISTS ( SELECT 1 FROM clubRaffleWinners crw WHERE crw.user_ID = cu.user_ID AND crw.cID = 1157 AND crw.rafID = 18 AND crw.crID = 1001 AND crw.ceID = 1167 AND crw.chDate1 = '2022-06-04' ) ) AS tmp ); -- 生成随机偏移量 SET @rand_offset = FLOOR(RAND() * @pool_total); -- 预编译SQL取对应位置的随机用户 PREPARE raffle_stmt FROM ' SELECT user_ID, clID FROM ( SELECT ch.user_ID, ch.clID FROM clubHistory ch WHERE ch.cID = 1157 AND ch.crID = 1001 AND ch.ceID = 1167 AND ch.chDate = ''2022-06-04'' AND NOT EXISTS ( SELECT 1 FROM clubRaffleWinners crw WHERE crw.user_ID = ch.user_ID AND crw.cID = 1157 AND crw.rafID = 18 AND crw.crID = 1001 AND crw.ceID = 1167 AND crw.chDate1 = ''2022-06-04'' ) GROUP BY ch.user_ID UNION SELECT cu.user_ID, cu.clID FROM clubUsers cu WHERE cu.cID = 1157 AND cu.crID = 1001 AND cu.ceID = 1167 AND cu.calDate <= ''2022-06-04'' AND NOT EXISTS ( SELECT 1 FROM clubRaffleWinners crw WHERE crw.user_ID = cu.user_ID AND crw.cID = 1157 AND crw.rafID = 18 AND crw.crID = 1001 AND crw.ceID = 1167 AND crw.chDate1 = ''2022-06-04'' ) ) AS eligible_pool LIMIT ?, 1 '; EXECUTE raffle_stmt USING @rand_offset; DEALLOCATE PREPARE raffle_stmt;
该方案在百万级用户量下的查询耗时可以控制在毫秒级,缺点是代码量稍大,适合用户规模较大的活动使用。
内容的提问来源于stack exchange,提问作者rolinger
相关产品推荐
相关产品推荐

