Mysql Order by 问题
环境说明
-
MySQL 版本:
5.1.73-log -
存储引擎:
ENGINE=InnoDB
问题起因
某个需求实现中,采用的是通过使用「业务表」的某个时间字段 作为 游标,通过每次获取的数量作为 偏移量,遍历一个 静态数据的集合,从而达到迭代遍历的目的。
也即,会构造 <last_id|time> 的游标,time 按照月划分(业务需要),从 0 开始计数。
-
time: 表示起始时间,截止时间为+1 月。 -
last_id:表示当前该时间取到了哪里。
举例说明:
-
0|2018-01-01:表示在[1月, 2月)从0开始获取n条数据 -
128|2018-01-01:表示在[1月, 2月)从128开始获取n条数据
当数据集合为空时,break 至下一个月。
一切看起来正常,但在最终 check 数据的时候,却发现有 重复的推送。
下面,我们举个例子,尝试解释该问题。
场景还原
注:以下数据均为举例说明,不代表生产环境真实数据情况。
推送订单(orders),使用 pay_time 作为游标。
sql:
|
|
我们设想这里在相同 pay_time 的上,会根据主键 id 进行 asc。
- 索引:
|
|
explain:
|
|
按照预期,该 sql 理应能够不重复的遍历整个数据集合。但不幸的是,事实并没按预期发生,在推送过程中,出现了重复推送的问题。
举例说明
-
pay_time为2019-01-01 18:09:34。 -
该时间点有
2条数据,id为100,98。 -
某次
limit 100, 500恰好取到了这个时间的1 条数据,取出来的是id=98,也即发生了跨页问题。 -
下次偏移依然从这个时间开始,但
MySQL仍然返回了id=98,而不是另一条数据:id=100。 -
这里,并没有按照我们预想的一样,有个隐含的排序规则:
id asc。
分析与解决
通过上面的描述,我们猜测 MySQL 针对 order by ... limit 的查询,并没有对结果集进行排序。具体到这个例子,并没有按照 id asc 或 id desc 排序。而且很可能是 随机 返回。
去找 mysql 手册,5.1 版本 没找到,直接看 5.7 版本1。
这篇文章很明确给出了答案:
|
|
通俗翻译过来就是:
|
|
而我们的查询场景,正好符合这个解释。
通过这篇文章,我们还可以了解到 MySQL 是到底是怎么解析 order by ... limit 语句的,比如如下一段:
|
|
简而言之,MySQL 在按照 ORDER BY 排序后的匹配结果集,会且只会选取 LIMIT row_count 行返回,而不会再针对该结果集进行排序。
既然知道了原因,那么就很好解决了。既然没有按照设想的 id asc,那么就在 sql 指定顺序:
|
|
显式指明 id asc,明确告诉 MySQL,我们需要针对主键 id 进行一次排序。
但需要注意的是,这里只能选取具有 unqiue 属性 的字段,才能保证稳定排序。(思考下是为什么)。
延伸
在实际的业务场景中,即便我们使用了 order by pay_time asc, id asc,但还是可能存在问题。
一个典型的场景如下:
-
假设脚本执行到了
当前时间(设为etime)停止,取到了 n 条数据,假设id=100。 -
恰好这时有
id=98的订单在变更状态,但并没有被这次轮询获取到。 -
下次脚本启动时,从上次的时间
etime开始,如果是使用了id asc,会导致id=98的数据被忽略,重复取到了id=100这条。如果是使用了id > last_id的条件,会导致漏掉了id=98的数据。
当然,这个问题就和 MySQL 无关了,主要是业务理解不当导致的。
也即,因为 并发 导致遍历的不是一个 静态数据集合。这里,我们需要通过别的手段进行解决。
最简单粗暴的解决方案为,加一个 时间戳限制:
|
|
也即,在已经过去的时间里,业务终态的数据集合必然是静态的。