基于湖库一体架构,统一管理结构化、半结构化与非结构化等多模态数据,一个系统承载事务处理、实时分析与 AI 工作负载。
SQL 查询中使用不等于谓词过多退化为全表扫描的原因和解决方法
更新时间:2026-05-14 07:41
问题现象
在 OceanBase 数据库 V4.2.1 BP7 版本中,当执行包含多个 <>/!= 条件的 SQL 查询时,如果 <>/!= 条件的数量超过 12 个,即使存在合适的索引,查询也会退化为索引全表扫描,导致执行效率显著下降。 示例如下。
explain extended_noaddr
select c1,c2,c3 from user_table
where c3 = 'a1dcdc796df77b19bf25a92c7ab00101' and (
c1 <>'0ac83825044e795455d3f01a353c71db' and
c1 <>'5bed7f5c49df9262feed4a8acfced477' and
c1 <>'daf9c12fa5f68b489b83905c1a96311b' and
c1 <>'0f5858b3b066c920483197d1bc586a10' and
c1 <>'b83bdf24874da0f9bba3dcde360f70b7' and
c1 <>'bfafdd1714093988575f93c9f52e3107' and
c1 <>'280cc2f28c2957042ad4aa2a7bd4f95e' and
c1 <>'f4da4437b7837df6abeb32c4931804da' and
c1 <>'70133539f5da84506227e4a6d478f8f4' and
c1 <>'9aaa73429257ebddcf760e6fc70a167e' and
c1 <>'de51330c941284c998dae0c259e5419d' and
c1 <>'79068315b1ce4458410a02114ff7ef3c'
);
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ================================================================================ |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| -------------------------------------------------------------------------------- |
| |0 |TABLE FULL SCAN|user_table(idx_uni_change)|9 |52030864 | |
| ================================================================================ |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([user_table.change_record], [user_table.company_name_digest], [user_table.use_flag]), filter([user_table.company_name_digest |
| = cast('a1dcdc796df77b19bf25a92c7ab00101', VARCHAR(1048576))], [user_table.change_record != cast('0ac83825044e795455d3f01a353c71db', VARCHAR(1048576))], |
| [user_table.change_record != cast('5bed7f5c49df9262feed4a8acfced477', VARCHAR(1048576))], [user_table.change_record != cast('daf9c12fa5f68b489b83905c1a96311b', |
| VARCHAR(1048576))], [user_table.change_record != cast('0f5858b3b066c920483197d1bc586a10', VARCHAR(1048576))], [user_table.change_record |
| != cast('b83bdf24874da0f9bba3dcde360f70b7', VARCHAR(1048576))], [user_table.change_record != cast('bfafdd1714093988575f93c9f52e3107', VARCHAR(1048576))], |
| [user_table.change_record != cast('280cc2f28c2957042ad4aa2a7bd4f95e', VARCHAR(1048576))], [user_table.change_record != cast('f4da4437b7837df6abeb32c4931804da', |
| VARCHAR(1048576))], [user_table.change_record != cast('385c0ba617ae847d9587d8c578c78504', VARCHAR(1048576))], [user_table.change_record |
| != cast('70133539f5da84506227e4a6d478f8f4', VARCHAR(1048576))], [user_table.change_record != cast('9aaa73429257ebddcf760e6fc70a167e', VARCHAR(1048576))], |
| [user_table.change_record != cast('de51330c941284c998dae0c259e5419d', VARCHAR(1048576))], [user_table.change_record != cast('79068315b1ce4458410a02114ff7ef3c', |
| VARCHAR(1048576))]), rowset=16 |
| access([user_table.id], [user_table.company_name_digest], [user_table.change_record], [user_table.use_flag]), partitions(p0) |
| is_index_back=true, is_global_index=false, filter_before_indexback[true,true,true,true,true,true,true,true,true,true,true,true,true,true], |
| range_key([user_table.company_name_digest], [user_table.change_record], [user_table.shadow_pk_0]), range(MIN,MIN, |
| MIN ; MAX,MAX,MAX)always true
关键信息
通过 SQL 语句判断,如下示例。
explain extended_noaddr select c1,c2,c3 from user_table where c3 = 'xxx' and (c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' and c1 <>'xxx' );当
!=条件个数小于 12 个时,能够正常利用索引进行范围查询。当
!=条件个数大于 12 个时,优化器无法有效利用索引,转为索引全表扫描。观察逻辑计划是否计划走索引但是抽取
Query Range为range(MIN,MAX)。
注意
!=条件个数不是固定值,主要看抽取Query Range时耗费的内存,如果超限则可能抽取 Range 失败尝试走主表。关键日志信息
is_reach_mem_limit_=true,query_range_ctx_->range_optimizer_max_mem_size。[2024-12-30 19:19:26.267494] WDIAG [SQL.REWRITE] deep_copy_key_part_and_items (ob_query_range.cpp:4771) [103267][T1004_L0_G0][T1004][xxxxx-xxxxx-xxxxx-xxxxx] [lt=14][errcode=0] use too much memory return always true keypart(mem_used_=696112, allocator_.used()=134914192) [2024-12-30 19:19:26.267524] WDIAG [SQL.REWRITE] and_range_graph (ob_query_range.cpp:5197) [103267][T1004_L0_G0][T1004][xxxxx-xxxxx-xxxxx-xxxxx] [lt=23][errcode=0] use too much memory(is_reach_mem_limit_=true, query_range_ctx_->range_optimizer_max_mem_size_=134217728) [2024-12-30 19:19:26.267531] WDIAG [SQL.REWRITE] and_range_graph (ob_query_range.cpp:5197) [103267][T1004_L0_G0][T1004][xxxxx-xxxxx-xxxxx-xxxxx] [lt=6][errcode=0] use too much memory(is_reach_mem_limit_=true, query_range_ctx_->range_optimizer_max_mem_size_=134217728)
问题原因
执行慢计划走索引但是抽不出来 Query Range 这个是内存的问题。具体来说例如 SQL 中存在 c1 != xx 会把 c1!= xx 展开为等效的 c1 < xx or c1 > xx,如果多个条件比如 13 个条件 != 会展开为 2^13 个 Query Range 很暴力的展开内存的使用是 2^13 次方 ,这会导致内存消耗迅速增加,超过 range_optimizer_mem_max_use 参数设定的内存限制,从而使得优化器无法有效利用索引,转而采用全表扫描的方式执行查询。改字符集序能规避是字符集是因为不需要加 cast,内存占用少一点没超参数的内存限制索引能正常走索引抽出来 Query Range。
有关配置项 range_optimizer_mem_max_use
默认值: 128M。
详细说明: 在复杂谓词场景(常见的是 IN 表达式以及 OR 表达式很多的场景),优化器抽取 Query Range 时会耗费较大内存,甚至影响集群的正常使用。针对这一问题,MySQL 可通过 range_optimizer_max_mem_size 参数来限制抽取 Query Range 阶段所占用的内存。限制 Query Range 模块使用的内存使用。当 Query Range 模块使用的内存达到上限时,则不做任何 Range 的抽取。例如,某个复杂谓词命中了索引,如果对这个复杂谓词抽取 Query Range 时使用的内存达到了上限,那就不会选择这个索引,而是尝试走主表。
问题的风险及影响
如果计划走索引但是抽取 Query Range 为 range(MIN,MAX),SQL 执行效率显著下降,可能导致查询响应时间延长。
适用版本
OceanBase 数据库 V4.2.x 版本。
解决方法
升级至问题已修复版本。目前已修复的版本包括 OceanBase 数据库 V4.2.5(oceanbase-4.2.5.0-100000082024102022)版本。
调整
range_optimizer_mem_max_use参数: 如果不立即升级,可以通过增大range_optimizer_mem_max_use参数来临时缓解问题,但这仅能部分解决问题,因为内存使用量随!=条件数量的增加呈指数增长。
规避方式
修改SQL语句: 将多个
!=条件改为NOT IN,这样可以避免优化器生成过多的范围查询,减少内存消耗(NOT IN如果过多有报错的可能性-4013,No memory or reach tenant memory limit)。字段的字符集序: 通过保证字段的字符集序一致,可以避免 cast 转换减少内存占用,但这种方法只能轻微改善问题。