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
无关了,主要是业务理解不当导致的。
也即,因为 并发
导致遍历的不是一个 静态数据集合
。这里,我们需要通过别的手段进行解决。
最简单粗暴的解决方案为,加一个 时间戳限制
:
|
|
也即,在已经过去的时间里,业务终态的数据集合必然是静态的
。