MySQL 5.7实现高低值交替排序的SQL查询问题排查
需求说明
需要实现特殊排序规则:按最高值、最低值、次高值、次低值的顺序交替排列,高值侧从大到小取数,低值侧从小到大取数。
原有写法已经可以实现高低值穿插,但高值没有按从大到小的顺序正确排列,原有代码如下:
select users.* from users CROSS JOIN (select @even := 0, @odd := 0) param order by IF(score > 1, 2*(@odd := @odd + 1), 2*(@even := @even + 1) + 1), score DESC;
原有语句错误返回结果
Email Score ----- -------- foo1@gmail.com 42 foo5@gmail.com 1 foo2@gmail.com 49 foo6@gmail.com 0 foo3@gmail.com 37 foo4@gmail.com 7 foo@gmail.com 22
期望输出结果
Email Score ----- -------- foo2@gmail.com 49 foo6@gmail.com 0 foo1@gmail.com 42 foo5@gmail.com 1 foo3@gmail.com 37 foo4@gmail.com 7 foo@gmail.com 22
错误原因
原有写法存在两个核心问题:
- 高低值判断逻辑错误:用固定阈值
score>1区分高低值,实际上高低值是按全量数据排序后的相对位置划分的,和固定分数阈值无关。 - 变量赋值顺序不可控:MySQL 5.7中,直接在ORDER BY子句里给遍历行做用户变量赋值时,行扫描顺序不受同语句内
score DESC规则约束,优化器会按原始表存储顺序扫描行分配奇偶序号,导致序号分配错乱。
修正方案
先给所有数据按分数倒序生成连续行号,再按行号计算交替排序的键值:前半部分(高值)分配奇数排序位,后半部分(低值)倒序分配偶数排序位,最终按键值排序即可,完全兼容MySQL 5.7,代码如下:
SELECT email, score FROM ( SELECT u.email, u.score, @rn := @rn + 1 AS rn, @total AS total_cnt FROM users u, ( SELECT @rn := 0, @total := (SELECT COUNT(*) FROM users) ) init_param -- 先按分数倒序排列,保证行号分配顺序正确 ORDER BY u.score DESC LIMIT 18446744073709551615 ) ranked ORDER BY -- 前半段高值分配1、3、5...奇数位,后半段低值分配2、4、6...偶数位 IF( rn <= CEIL(total_cnt / 2), 2 * rn - 1, 2 * (total_cnt - rn + 1) );
说明:子查询中加超大值LIMIT是为了规避MySQL 5.7优化器默认忽略子查询内部ORDER BY规则的问题,保证行号严格按分数倒序生成。
执行后返回结果和预期完全一致:
Email Score ----- -------- foo2@gmail.com 49 foo6@gmail.com 0 foo1@gmail.com 42 foo5@gmail.com 1 foo3@gmail.com 37 foo4@gmail.com 7 foo@gmail.com 22
内容的提问来源于stack exchange,提问作者steve
相关产品推荐
相关产品推荐

