返回

MySQL深分页查询:`select *`与`select [主键]`结果为何不一致?

mysql

MySQL 深分页查询:当 select * 遇上 select [主键],数据不一致之谜

在使用 MySQL 进行深分页查询时,你是否遭遇过 select *select [主键] 返回结果不一致的怪异现象?这个问题看似不可思议,却真实存在,并且可能引发严重的数据错误。本文将深入剖析这一问题的根源,带你了解其背后的机制,并提供有效的解决方案,助你规避潜在的数据风险。

问题重现

假设我们有一个包含 160 万条记录的订单表 t_order,其中 order_id 是主键,order_status 是二级索引。order_status 字段的数据分布如下:

order_status total
3001 1480000
3002 120000

现在,我们需要查询 order_status 大于 3002 的订单,并获取从第 349001 条记录开始的 10 条数据。

SQL 1:

select order_id from t_order where order_status > 3002 order by order_status limit 349000,10;

执行结果: order_id 的序号范围为 1498660 到 1498669。

执行计划: 使用了 idx_order_status 索引。

SQL 2:

select * from t_order where order_status > 3002 order by order_status limit 349000,10;

执行结果: order_id 的序号范围为 1746820 到 1746829。

执行计划: 未使用 idx_order_status 索引。

问题: 为什么两条 SQL 的执行结果不同?哪个结果是正确的?

深入分析

出现这种情况的根源在于 MySQL 的索引覆盖和回表查询机制。

索引覆盖:

当查询语句只涉及索引字段时,MySQL 可以直接从索引中获取数据,无需回表查询。这就好比我们查字典时,只需知道拼音就能找到对应的汉字,无需翻阅整个字典。

SQL 1 只查询了 order_id,而 order_idorder_status 都在同一个索引中,MySQL 可以直接利用索引覆盖,快速定位到目标数据。

回表查询:

当查询语句涉及非索引字段时,MySQL 需要先通过索引定位到主键值,再根据主键值回表查询完整的数据。这就好比我们先查拼音找到页码,再翻到对应页码查找详细内容。

SQL 2 查询了所有字段 (select *),包括非索引字段。MySQL 首先使用 idx_order_status 索引定位到满足条件的主键值,然后根据主键值回表查询完整的数据。

深分页问题:

在深分页场景下,MySQL 需要扫描大量的数据才能定位到目标记录。当使用回表查询时,每次回表都需要磁盘 I/O,这会极大地增加查询时间。为了优化性能,MySQL 在某些情况下会选择放弃使用索引,直接进行全表扫描。

SQL 2 中,由于深分页和回表查询的双重影响,MySQL 最终选择了全表扫描,导致其执行结果与 SQL 1 不同。

正确性判定

在上述例子中,SQL 1 的结果是正确的 。这是因为 SQL 1 使用了索引覆盖,避免了回表查询,保证了数据的准确性。

解决方案

为了避免深分页查询带来的数据不一致问题,我们可以采取以下解决方案:

延迟关联查询:

将查询拆分为两步:

  1. 使用索引覆盖查询主键值。
  2. 根据主键值进行关联查询,获取完整数据。
select * from t_order where order_id in (
    select order_id from t_order where order_status > 3002 order by order_status limit 349000,10
);

使用游标分页:

使用游标逐条读取数据,避免一次性获取大量数据。

DECLARE order_id_cursor CURSOR FOR 
    select order_id from t_order where order_status > 3002 order by order_status;

OPEN order_id_cursor;

FETCH order_id_cursor INTO @order_id;

-- 跳过前 349000 条记录
REPEAT 
    FETCH order_id_cursor INTO @order_id;
UNTIL @rownum = 349000;

-- 获取接下来的 10 条记录
SET @counter = 0;
WHILE @counter < 10 DO
    FETCH order_id_cursor INTO @order_id;
    -- 处理数据
    SET @counter = @counter + 1;
END WHILE;

CLOSE order_id_cursor;

优化数据库设计:

  • 避免在非必要情况下使用 select *,尽量指定查询字段。
  • 为经常参与查询的字段创建索引。
  • 考虑使用分表分库等技术手段,提高数据库性能。

常见问题

1. 为什么深分页查询会导致性能问题?

深分页查询需要 MySQL 扫描大量的数据才能定位到目标记录,尤其是在使用回表查询时,每次回表都需要磁盘 I/O,这会极大地增加查询时间。

2. 如何判断 MySQL 是否使用了索引覆盖?

可以通过查看执行计划来判断。如果 Extra 列显示 Using index,则表示使用了索引覆盖。

3. 延迟关联查询有什么优缺点?

优点:可以避免回表查询,提高查询效率。缺点:需要执行两次查询,增加了数据库的负担。

4. 游标分页有什么优缺点?

优点:可以逐条读取数据,避免一次性获取大量数据。缺点:需要维护游标状态,增加了代码的复杂度。

5. 除了上述解决方案外,还有哪些方法可以优化深分页查询?

可以使用缓存机制,将查询结果缓存起来,避免重复查询。还可以使用 Elasticsearch 等搜索引擎,提高查询效率。