深分页怎么导致索引失效了?提供6种优化的方案!

本文详细探讨了深分页场景中索引失效的问题,提供了覆盖索引、游标分页、子查询定位、in+子查询和联表查询等多种优化方案,帮助开发者理解和应对大规模分页查询中的性能挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深分页怎么导致索引失效了?提供6种优化的方案!

上篇文章说到索引失效的几种规则,其中就有包括 深分页回表太多导致索引失效 的场景

本篇文章来聊聊深分页场景中的问题并提供几种优化方案,以下是本篇文章的思维导图:

image-20240128150009803.png

深分页问题

那么什么是深分页问题呢?

在MySQL的limit中:limit 100,10MySQL会根据查询条件去存储引擎层找到前110条记录,然后在server层丢弃前100条记录取最后10条

这样先扫描完再丢弃的记录相当于白找,深分页问题指的就是这种场景(当limit的偏移量过大时会导致性能开销)

-- 0.04s
select SQL_NO_CACHE * from student where age = 18 limit 10;
-- 4.049s
select SQL_NO_CACHE * from student where age = 18 limit 5000,10;

该表中存在二级索引:idx_age_name 是学生年龄age和学生名称name的联合索引(该二级索引上只存在字段age,name,id)

这条SQL会基于MySQL最优成本选择使用 idx_age_name 或者 聚簇索引

假设它使用二级索引 idx_age_name,我们来分析一下使用二级索引的流程:

  1. 它会先根据二级索引定位到第一条满足age=18的记录

  2. 由于二级索引上的记录没有完整字段,因此会回表查询聚簇索引获取完整字段

  3. 将结果返回给server层,并根据这条记录的next找到下一条记录

循环1-3的过程,在二级索引上找到满足查询条件age=18的前5010条记录(或者直到不满足age=18),然后舍弃前5000条,取最后10条

image-20240128120002319.png

在这个过程中:先查二级索引接着回表获取完整记录然后返回给server层再查下一条记录

由于二级索引是联合索引,当age相等时,主键id不一定是有序的,这样回表就会产生随机IO

当深分页场景使用二级索引时会涉及回表(随机IO),如果偏移量太大回表的数据量也会很大,MySQL认为成本太大不偏向使用二级索引从而导致索引失效

那么该如何优化深分页这样的问题呢?从这里分析可以得到从两个方面进行优化:

  1. 让它不要回表,避免回表的开销
  2. 让它不要舍弃前XX条记录,避免白查询

接下来结合不同的方法进行讨论

覆盖索引避免回表

当业务上允许时可以使用覆盖索引避免回表,查完二级索引就交给server层再去查下一条记录

-- 4.049s
select SQL_NO_CACHE * 
from student 
where age = 18 limit 5000,10;

-- 0.034s
select SQL_NO_CACHE id,age,student_name 
from student 
where age = 18 limit 5000,10;

虽然说覆盖索引依旧会舍弃前XX条记录,但没有回表的开销已经快了不少

但如果业务上不允许还是要查较多在聚簇索引上的字段,又或者偏移量还是太大的情况,我们还是需要使用其他的方案

游标分页

为了避免limit中的偏移量,可以自己来存储该偏移量

我们可以使用上次查询的最大值来当作这次的查询条件(游标分页)

--  12.899s
select * from seat 
where seat_code = 'caicaiseat' 
limit 99990,10; -- 最后一条记录 seat_id = 988380

select * from seat 
where seat_code = 'caicaiseat' limit 100000,10;

--  0.189s  满足查询条件情况下主键有序 可以使用上一次记录
select * from seat 
where seat_code = 'caicaiseat' and seat_id > 上次查询最大记录 
limit 10;

select * from seat 
where seat_code = 'caicaiseat' and seat_id > 988380 
limit 10;

其中limit 100000,10seat_id > 988380 limit 10 查询结果相同,但是这种做法是有前提条件的

前提条件

  1. 需要一个列来记录上一次查询的最大值(通常是主键,后面就直接用主键表达,大家明白就好),并且满足查询条件时主键需要是有序的
  2. 因为本次查询需要依赖上一次查询的主键最大值,因此分页查询只能是连续的,不能进行跳页(比如查完第一页直接查第一百页)

在上面案例SQL中会使用二级索引 idx_seat_code (seat_code,seat_id),当使用二级索引时,由于seat_code是等值查询,满足条件时id是有序的

如果是原来的SQL使用这种做法会导致查询出来的结果不一致,因为在二级索引上当age = 18时主键不一定有序

select SQL_NO_CACHE * from student 
where age = 18 and id > 6726705  
limit 10;

乱序该如何解决呢?当然是排序呀!

select SQL_NO_CACHE * from student 
where age = 18 and id > 上次查询最大记录 
order by id 
limit 10;

但是排序又会带来新的问题:可能更偏向使用聚簇索引(全表扫描),如果使用二级索引还需要对id排序(临时表),具体还要查看执行计划分析

游标分页排序下的SQL和原始limitSQL结果是不同的,因为原始的id无序,但它们都满足(业务)查询条件age=18,只是做分页时顺序不同

-- 原始limit SQL
select SQL_NO_CACHE * from student 
where age = 18 
limit 5000,10;

-- 游标分页
select SQL_NO_CACHE * from student 
where age = 18 and id > 上次查询最大记录 
order by id 
limit 10;

使用游标分页时需要使用主键记录每次查询的最大值,并且需要满足查询条件后主键值是有序的,只能在连续分页的场景使用,不能跳页,比如滑动分页(一边滑动一边分页)

子查询定位

另一种避免limit 偏移量太大的方式是通过子查询定位到第一条记录

子查询也是类似于游标分页,定位第一条记录就相当于先找到偏移量

select * from seat 
where seat_code = 'caicaiseat' 
limit 100000,10;

-- 0.068s 通过二级索引先定位到主键值
select * from seat 
where seat_code = 'caicaiseat'
and seat_id >= 
    (select seat_id from seat 
    where seat_code = 'caicaiseat' 
    limit 100000,1) 
limit 10;

使用子查询 select seat_id from seat where seat_code = 'caicaiseat' limit 100000,1 定位到第一条记录的主键值

然后再通过 seat_id >= 定位到的第一条记录 limit 10 查出需要的10条记录

子查询定位的方案也有使用前提:

  1. 子查询可以使用二级索引快速定位(不用回表)

  2. 满足查询条件后主键需要有序(因为使用 seat_id >=

子查询定位支持跳页,但需要使用二级索引定位且满足条件后主键值有序

in + 子查询

在游标分页与子查询使用二级索引定位的场景中总是需要记录偏移量的列(主键)有序,遇到无序的场景还需要排序,增加性能开销

有没有更好的办法避免排序呢?

id >= X 需要主键有序,但是 id in (x,x...) 似乎就不需要主键有序了呀

使用子查询常用的搭配in,因为分页时子查询数据量也不大,可以使用in来进行查询

select SQL_NO_CACHE * from student 
where age = 18 
and id in 
    (select id from student 
    where age = 18 
    limit 5000,10);

但是MySQL好像不支持in与limit同时使用,这样使用会报错

1235 - This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'

于是只能把子查询的结果封装成临时表

select SQL_NO_CACHE * 
from student 
where age = 18 
and id in 
    (select id 
    from (select id 
          from student 
          where age = 18 
          limit 5000,10) 
    tmp);

in + 子查询的方案即支持跳页又不用排序,虽然会生成临时表但数据量较少

联表查询 + 子查询

熟悉MySQL中in优化(半连接)的同学,一定能够知道in与内连接的奇妙关系

在某些符合条件的场景下,in会被优化为内连接

(感兴趣或者不熟悉的同学可以看这篇文章MySQL半连接优化

当然我们也可以手动编写内连接的联表查询来让其进行关联

-- 4.049s 原始
select SQL_NO_CACHE * 
from student 
where age = 18 
limit 5000,10;

-- 0.033s 联表 + 子查询
select SQL_NO_CACHE * 
from student s 
inner join (select id tmp_id 
            from student 
            where age = 18 
            limit 5000,10) tmp
on s.id = tmp_id

联表查询的思路与in相同,都能够支持跳页和主键无序

需求沟通

其实这几种方案要么实现不回表,要么实现不用偏移量,在解决这类问题时其实要先与需求进行沟通:

1、是否有必要查这么多记录,记录太多时能否提醒用户减少查询范围(比如起止时间)

2、如果不能修改查询条件的情况下且数据量确实大(比如查18岁学生还有很多分页),考虑是否能让用户翻页翻到这么后呢?

...

总结

本文描述发生深分页问题的原因以及各种解决方案,总结如下:

方法描述使用前提优点缺点
覆盖索引通过覆盖索引避免回表,limit还是会放弃前XX条记录查询的列都在二级索引上不用回表,避免随机IO还是会舍弃前XX条记录
游标分页通过主键记录偏移量,避免limit放弃前XX条记录记录主键,满足条件时主键需要有序避免limit放弃前XX条记录不能跳页,如果满足条件时主键无序还需要排序
子查询定位通过使用二级索引子查询快速定位第一条偏移量的记录,避免limit放弃前XX条记录使用二级索引定位,满足条件时主键需要有序与游标分页相比,能够跳页子查询时还是会舍弃前XX条记录,如果满足条件时主键无序还需要排序
in + 子查询使用in关联子查询定位的主键使用二级索引定位,使用临时表支持跳页、主键无序生成临时表,子查询数据量大会影响性能
联表 + 子查询使用内连接关联子查询定位的主键使用二级索引定位,使用临时表支持跳页、主键无序生成临时表,子查询数据量大会影响性能
需求沟通根据具体场景进行沟通防止深分页问题发生产品经理答应省事产品经理没那么容易答应

深分页问题是因为MySQL limit时,会先把记录查询出来,再舍弃前XX条记录所导致的

不同的方案适合不同的业务场景,在收到数据量较大的分页需求时先进行沟通,无法避免时再做优化

如果需要查询的列在二级索引上都存在,可以使用二级索引(覆盖索引)避免回表

如果满足查询条件后主键有序并且业务上不用跳页那么可以选择游标分页

如果满足查询条件后主键有序并且业务上需要支持跳页,可以选择子查询

如果满足查询条件后主键(记录偏移量的列)无序,那么可以选择in或联表的方案

本文由博客一文多发平台 OpenWrite 发布!

<think>嗯,用户想知道在Oracle中使用IN子句时索引为什么会失效,以及如何解决这个问题。首先,我需要回忆一下Oracle中索引的工作原理。索引失效通常和查询的写法有关,比如使用函数、类型转换或者统计信息不准确等。IN子句可能导致索引失效的情况可能涉及多个方面。 首先,IN子句中的值数量太多,可能会让优化器认为全表扫描更高效。比如当IN列表里有成百上千个值时,优化器可能选择全表扫描而不是索引扫描,因为后者需要多次访问索引和数据块,反而更慢。这时候可能需要考虑分页查询或者临时表的方法。 然后,统计信息不准确也是一个可能的原因。如果表的统计信息过时,优化器可能错误估计使用索引的成本,导致选择全表扫描。这时候需要重新收集统计信息,使用DBMS_STATS包来更新。 还有一个情况是绑定变量和隐式类型转换。如果IN子句中的变量类型与列类型不一致,Oracle会进行隐式转换,这会导致索引失效。比如列是VARCHAR2类型,但传入的是数字,这时候需要显式转换类型或者使用绑定变量正确的类型。 另外,索引的选择性不高也可能导致失效。如果列的值重复很多,优化器可能认为全表扫描更高效。这时候可以考虑复合索引或者位图索引,但位图索引在OLTP系统中要谨慎使用。 优化器模式也可能影响执行计划,比如设置为ALL_ROWS时倾向于全表扫描,而FIRST_ROWS_n可能更倾向于索引。可以通过HINT强制使用索引,但需要测试效果。 最后,如果IN子句中的值是子查询结果,子查询效率低的话也会影响。这时候需要优化子查询,比如添加索引或物化视图。 解决方案方面,分页处理IN列表、更新统计信息、检查类型一致性、使用HINT、复合索引、绑定变量分组、优化子查询等都是可行的方法。需要根据具体情况选择合适的策略,并通过执行计划验证是否生效。 还要注意用户提供的引用内容,比如引用[1]提到监控索引和分析,引用[2]提到全文索引,但这里可能不适用。引用[3]的错误代码可能和语法有关,但当前问题不涉及。需要确保解决方案与引用内容的相关部分结合,比如监控索引状态和使用正确的索引类型。 总结起来,需要逐步分析可能的原因,并逐一测试对应的解决方法,同时参考Oracle的官方文档和最佳实践。</think>### Oracle中IN子句导致索引失效的原因及解决方案 #### 一、索引失效的核心原因 1. **IN列表值过多** 当IN子句包含大量值时(例如超过1000个),优化器可能认为全表扫描成本低于索引扫描,导致索引失效[^1]。例如: ```sql SELECT * FROM employees WHERE department_id IN (1,2,3,...,1001); ``` 2. **统计信息不准确** 表的统计信息(如数据分布、行数)未及时更新,优化器可能误判索引效率,选择全表扫描。 3. **隐式类型转换** 如果IN子句中的值与列类型不匹配(如列是VARCHAR2,但传入数值),Oracle会触发隐式转换,导致索引失效。例如: ```sql SELECT * FROM orders WHERE order_code IN (1001, '1002A'); -- order_code为VARCHAR2类型 ``` 4. **索引选择性低** 当列重复值较多时,优化器可能认为索引效率低于全表扫描。例如状态字段(如`status IN (0,1)`)。 5. **优化器模式影响** 若优化器模式设置为`ALL_ROWS`(偏向全表扫描),而非`FIRST_ROWS_n`(偏向索引),可能主动放弃索引。 --- #### 二、解决方案 1. **分页处理IN列表** 将大量值拆分到多个查询中,每批次处理100-500个值,强制使用索引: ```sql SELECT * FROM employees WHERE department_id IN (1,2,...,100); SELECT * FROM employees WHERE department_id IN (101,102,...,200); ``` 2. **更新统计信息** 使用`DBMS_STATS`重新收集表和索引的统计信息: ```sql EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA_NAME', 'TABLE_NAME', CASCADE=>TRUE); ``` 3. **显式类型匹配** 确保IN子句值与列类型完全一致,避免隐式转换: ```sql SELECT * FROM orders WHERE order_code IN ('1001', '1002A'); ``` 4. **强制使用索引(HINT)** 通过`/*+ INDEX(table_name index_name) */`提示强制索引: ```sql SELECT /*+ INDEX(employees idx_dept) */ * FROM employees WHERE department_id IN (1,2,3); ``` 5. **复合索引优化** 若查询涉及多列,可创建复合索引: ```sql CREATE INDEX idx_dept_status ON employees(department_id, status); ``` 6. **绑定变量分组** 对大量IN值使用临时表存储,通过JOIN关联查询: ```sql CREATE GLOBAL TEMPORARY TABLE temp_ids (id NUMBER); INSERT INTO temp_ids VALUES (1),(2),...; SELECT e.* FROM employees e JOIN temp_ids t ON e.department_id = t.id; ``` --- #### 三、验证索引是否生效 1. **检查执行计划** 使用`EXPLAIN PLAN`查看是否走索引: ```sql EXPLAIN PLAN FOR SELECT * FROM employees WHERE department_id IN (1,2,3); SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); ``` 2. **监控索引使用状态** 参考引用[1],通过`ALTER INDEX`监控索引: ```sql ALTER INDEX idx_dept MONITORING USAGE; -- 查询使用记录 SELECT * FROM V$OBJECT_USAGE WHERE INDEX_NAME = 'IDX_DEPT'; ``` --- #### 四、扩展场景 1. **子查询导致失效** 如果IN子句是子查询结果,需优化子查询效率: ```sql SELECT * FROM employees WHERE department_id IN (SELECT department_id FROM departments WHERE active = 1); -- 为departments.active添加索引 ``` 2. **位图索引适用性** 对低基数列(如性别),考虑位图索引(仅适用于OLAP场景): ```sql CREATE BITMAP INDEX idx_gender ON employees(gender); ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值