资讯动态

Oracle SQL中OR运算符的索引失效与优化改写实战指南

发布时间:2026/10/17 8:19:50 来源:尧图企业网站定制
想把 Oracle 里一个最简单的OR写出正经的门道真不是多写一个关键字的事。这期就来掰扯一下OR的底层逻辑、索引失效场景、改写优化思路以及我在生产环境里踩过的几个典型坑。无论你是刚学 SQL 的新手还是在优化慢查询的老手这篇都值得花几分钟过一遍。1. 先搞清楚 OR 在 Oracle 里到底怎么算的1.1 OR 的语法和语义OR是 SQL 里的逻辑或运算符它的语义很直观连接两个或多个条件只要其中一个条件为 TRUE整个表达式的结果就是 TRUE。最基础的用法长这样SELECT * FROM employee WHERE dept_id 10 OR dept_id 20;这段查询的意思就是把部门编号为 10 或者 20 的员工都捞出来。表面上这跟WHERE dept_id IN (10, 20)完全等价实际也确实是等价。但注意我这句话里有一个“表面上”和一个“实际也”这俩词放在一起容易给人一种“OR 很简单”的错觉。真正的坑在后面的语义细节尤其是NULL参与运算的时候。有一点必须提Oracle 里的逻辑判断是三值逻辑不只有 TRUE 和 FALSE还有 NULL。任何一个条件在遇到 NULL 时结果都可能是 UNKNOWN未知而OR两边的表达式只要有一边是 TRUE结果才是 TRUE如果两边都是 FALSE 或 NULL结果就是 FALSE 或 UNKNOWN。这直接影响你写OR时会不会查漏数据。1.2 优化器不保证短路求值跟编程语言里的 || 是两码事很多人写过 Java、Python里面a || b是短路求值只要 a 是 true就不会再去算 bb 里面哪怕写了注定报错的表达式也能安全跳过。于是到了 SQL 里很多人天然以为WHERE cond1 OR cond2也是先算 cond1成立就不算 cond2。这个想法是错的。SQL 是一门声明式语言你告诉 Oracle“我要什么结果”而不是“你要怎么一步一步算”。优化器有权决定先评估哪个谓词、用什么顺序扫描数据完全不等同于编程语言里的从左到右执行。我举一个我自己验证过的例子。假设有这样一个查询SELECT * FROM employee WHERE 1 0 OR UPPER(last_name) SMITH;按照编程直觉1 0永远是 FALSE那整个条件不就应该等同后半句吗在该不该调用UPPER(last_name)这个问题上有的数据库实现会优化掉前半句但 Oracle 的优化器并不会保证一定按文本顺序短路。在一些复杂场景下它依然可能先访问UPPER(last_name)的计算结果尽管这个扫描对最终结果没有任何影响。所以请你记住一句话在 Oracle SQL 里不要指望 OR 的短路行为不要用 OR 去“保护”另一个可能报错的表达式。如果确实需要条件保护用 CASE WHEN 或者把计算放进内联视图里会更可控。1.3 NULL 与三值逻辑OR 最容易踩的第一个坑这是OR相关面试题里最经典的考点也是实际生产环境里最容易漏数据的元凶。先看一段 SQLSELECT * FROM employee WHERE emp_name 张三 OR emp_name 张三;这句的意图是反正等于不等于都查出来了那应该能取出全部员工对吧但不对。因为emp_name如果是NULL那么NULL 张三结果是 UNKNOWNNULL 张三结果也是 UNKNOWNUNKNOWN OR 任何值只要另一边不是 TRUE最终都是 UNKNOWN。查询条件是 UNKNOWN 的行不会被返回。所以上面这条 SQL 会漏掉所有 emp_name 为 NULL 的员工。这是OR和NULL结合时最常见的隐患。解决办法也很直白如果你明确需要把空值也查出来就显式加上OR emp_name IS NULLSELECT * FROM employee WHERE emp_name 张三 OR emp_name 张三 OR emp_name IS NULL;或者用更简洁的NVL(emp_name, ###)之类的写法。但注意给字段套函数往往又会影响索引这个我放到下一章细说。2. OR 造成的性能灾难从执行计划看本质2.1 一个经典案例明明有索引却给你全表扫描我接手过很多条被人从慢查询日志里捞出来的“罪魁祸首”其中OR出现频率极高。最常见的场景是这个SELECT * FROM orders WHERE customer_id 10001 OR customer_id 20002;我一看customer_id上有索引但执行计划里赫然出现了TABLE ACCESS FULL——全表扫描。很多人这时候就懵了单列条件值也是普通数字为什么索引没用上核心原因在于优化器需要估算“返回多少行”。这个场景里OR连接的是同一个字段的两个离散值理论上优化器可以把它改写成 IN。但问题是优化器对 OR 的**选择率selectivity**估算往往比较悲观尤其是两个值的过滤性都一般的时候它可能认为返回行数太多干脆全表扫更快。如果你的表不大全表扫描确实不一定慢但如果订单表有几千万行全表扫就彻底爆了。优化器最终选择的执行计划由很多因素决定统计信息、直方图、数据库版本、索引类型、绑定变量等。但你改不了它的内部决策逻辑你能改的是手里的 SQL。2.2 哪几类 OR 写法索引救不了先说结论同一个字段上的多个 OR只要值都是常量基本能转成 IN索引大多能走。但不同字段上的 OR或者字段上有函数包裹的 OR才是真正让索引报废的重灾区。典型场景可以分成这几类第一类跨字段 ORSELECT * FROM orders WHERE customer_id 10001 OR status PENDING;customer_id有索引status也有索引但优化器很难为两个不同字段的 OR 设计出一条既能高效利用两个索引又能合并结果不漏数据的路径。它通常的做法有几种要么对其中一个字段走索引然后把结果合并要么做位图合并要么干脆全表扫描。当两个条件的过滤性都一般时全表扫描反而常常是它选择的最优解。第二类函数包裹字段SELECT * FROM employee WHERE UPPER(emp_name) SMITH OR dept_id 10;对emp_name做了函数处理后即便emp_name上建了普通 B-tree 索引索引也没法直接用因为索引里存的是原始值不是UPPER(emp_name)的结果。在 Oracle 里你得建函数索引才补救得了但函数索引又增加了维护成本和写操作的负担。所以我一直建议能用应用层控制大小写就别在 SQL 里套函数。第三类OR 两边都走不上索引但优化器因为成本误判选了烂路径。比如低频值和高频值混在一起的时候统计信息里直方图没及时更新优化器对结果集规模的估算会被带偏计划一乱就彻底放飞。2.3 位图索引下的 BITMAP OR 和普通索引的差异很多人不知道OR在普通 B-tree 索引和位图索引下的执行策略差异非常大。在 B-tree 索引下Oracle 常常把多个OR条件转换成CONCATENATION串联执行你可以把它理解成“UNION ALL 的索引访问版”。执行计划里会出现类似CONCATENATION的步骤下面挂着多个INDEX RANGE SCAN。这种方式的好处是每个分支都能利用各自的索引然后再合并结果集。而在位图索引下Oracle 有机会执行BITMAP OR各个条件的位图先分别计算然后按位进行或操作一次性得到结果行位图。位图索引特别适合高基数字段的等值 OR在数据仓库场景里非常香。但代价也明显位图索引不适合高并发 OLTP 写入因为行锁会让并发更新变慢。普通业务系统里大多数还是 B-tree 索引所以遇到 OR 导致全表扫描时我的第一反应永远是“能不能改写 SQL”而不是立刻去加索引。3. OR 的三板斧优化套路3.1 同列 OR 改 IN最稳且收益最大如果 OR 连接的条件都在同一个字段上最优解永远是试着改成 IN-- 原始写法 SELECT * FROM orders WHERE customer_id 10001 OR customer_id 20002 OR customer_id 30003; -- 改写 SELECT * FROM orders WHERE customer_id IN (10001, 20002, 30003);为什么版本号再新我也推荐 IN核心原因有两个。第一语义完全等价改写零风险第二Oracle 对 IN 的处理非常成熟尤其是带绑定变量和批量值的场景可以走INLIST ITERATOR对列表中的每个值分别做索引扫描执行效率稳定。而 OR 的多个常量写法优化器有时候会犯迷糊产生不稳定的执行计划。我实测过一个生产案例一张 5000 万行的流水表原来用三个 OR 查近一年数据执行计划走全表扫描跑了 40 多秒。我把三个 OR 改成 IN 之后执行计划立刻变成INLIST ITERATOR 三个INDEX RANGE SCAN整体耗时降到 0.3 秒以内。改动只有一个关键字效果立竿见影。提示如果你把 OR 改成 IN 后执行计划没变先检查字段是否存在隐式类型转换。比如字段是 VARCHAR2你写的是customer_id IN (10001, 20002)Oracle 会尝试把数字转成字符串再匹配一旦做了隐式转换索引照样会被按在地上摩擦。3.2 异列 OR 改 UNION ALL各走各的索引跨字段的 OR 是优化器最头疼的。这时候UNION ALL就登场了。逻辑上A OR B等价于A 的行合并上 B 的行再去重而去重会带来额外成本所以如果你能确定 A 和 B 的结果集没有交集用UNION ALL是最优解如果有交集用UNION才保险。实际开发里我见过不少人无脑把 OR 改 UNION这就容易画蛇添足。看这个例子-- 原始有两个字段的 OR SELECT * FROM orders WHERE customer_id 10001 OR status PENDING;假设customer_id 10001的订单状态肯定不是PENDING比如它只能是已发货那么两个条件不可能同时命中同一行用UNION ALL就是安全且最省的SELECT * FROM orders WHERE customer_id 10001 UNION ALL SELECT * FROM orders WHERE status PENDING;执行计划会变成两个分支各走各的索引结果直接合并。相比全表扫描性能提升往往是数量级的。注意改写前一定要确认两个分支结果集是否可能重叠。如果可能重叠却用了UNION ALL结果会重复逻辑上就是错的。不确定的时候用UNION虽然多一步排序去重但至少不会返回错数。3.3 函数包裹字段时的处理思路与改写函数包裹字段的情况我的处理顺序是这样先看有没有可能把函数从字段这侧拿出来放到常量那侧。比如-- 原来字段被包了函数 WHERE UPPER(emp_name) SMITH; -- 改写常量被包函数 WHERE emp_name SMITH;如果能保证表里的emp_name已经在应用层统一存成了大写那这个改写只是举手之劳索引立刻就能用上。如果历史数据不规范改不动存储那就得考虑函数索引CREATE INDEX idx_emp_name_upper ON employee(UPPER(emp_name));但函数索引有很多限制你得保证应用里所有针对该字段的查询都统一走同一个函数表达式如果多个函数写法不一样有人UPPER有人LOWER有人INITCAP索引就失效了。这种场景我见得太多最后基本都是靠数据清洗把脏数据洗成统一格式从源头解决问题。4. 实战案例从故障排查到性能翻倍4.1 案例一客户表查询从 10 秒到毫秒级这个案例我记得很清楚。某公司客户表 2000 多万行业务方经常要做一种查询按电话或邮箱或客户编号找一个客户。原始 SQL 长这样SELECT * FROM customer WHERE phone 13800000000 OR email testexample.com OR cust_no C20240001;这个查询上线后经常要跑好几秒甚至十秒。我看执行计划三个字段上都有独立索引但优化器选择了TABLE ACCESS FULL因为三个 OR 分支的选择率都不高合并成本评估下来优化器认为全表扫更划算。我的改写方案是UNION ALL因为这三个字段在业务上跟客户编号是一一对应的不存在同一行通过不同条件命中两次的可能。改写后SELECT * FROM customer WHERE phone 13800000000 UNION ALL SELECT * FROM customer WHERE email testexample.com UNION ALL SELECT * FROM customer WHERE cust_no C20240001;执行计划立刻变成三段INDEX RANGE SCAN再合并单次查询从 10 秒掉到 50 毫秒以内。改动也只有一个词把 OR 替换成 UNION ALL。4.2 案例二分区表上的 OR 导致分区裁剪失效分区表是另一个 OR 重灾区。分区表本来可以通过分区裁剪只扫描相关分区。但一旦查询条件写成 OR分区裁剪常常直接失效。举个例子订单表按created_date做了范围分区。业务查询是这样SELECT * FROM orders WHERE created_date DATE 2024-01-01 OR order_status CLOSED;哪怕created_date是在分区键上因为order_status不是分区键而且两条件之间是 OR优化器必须考虑有可能一行满足order_status CLOSED但在 2024-01-01 之前。为了不丢数据它就必须扫描所有分区。改成 UNION ALL 就能保住分区裁剪SELECT * FROM orders WHERE created_date DATE 2024-01-01 UNION ALL SELECT * FROM orders WHERE order_status CLOSED AND created_date DATE 2024-01-01;第二个分支加上created_date DATE 2024-01-01是为了保证两个分支不重叠这里要用UNION ALL才安全。如果不加这个条件直接用UNION去重也行但代价是排序去重性能又打折扣。这个案例说明用 OR 改写优化时你的业务理解越深改写效果越好。你必须能判断分支是否重叠才能选择用 ALL 还是 DISTINCT。4.3 案例三OR 与 NULL 组合查询时漏数据的坑再看一个我实际帮人排查过的漏数问题。业务上他们要查所有“没有分配负责人”的员工以及“负责人叫张伟”的员工SELECT * FROM employee WHERE manager_name 张伟 OR manager_name ! 张伟;开发者的本意是查出全部员工结果却漏掉了manager_name IS NULL的人。排查的时候他们一度以为是数据同步丢了后来才定位到三值逻辑的坑。修复成SELECT * FROM employee WHERE manager_name 张伟 OR manager_name ! 张伟 OR manager_name IS NULL;这事看着简单但生产环境里因为 OR NULL 漏数的故障我见过不止一次尤其是频繁做数据统计、报表的人特别容易中招。我甚至见过有人为了“稳妥”给每个 OR 条件都套一层NVL反而把本来能走索引的字段搞成索引失效。所以我的态度一直是先理解三值逻辑再决定怎么写别盲目防卫式编程。5. 常见问题速查表与调试技巧5.1 问题速查表我把平时最常见的问题整理成一个速查表方便你排查时直接对照场景表现根本原因解决方向同字段多个 OR走全表扫描或计划不稳定选择率估算偏差OR 未转 IN改写为 IN 列表不同字段 OR走全表扫描无法高效合并多个索引结果改写为 UNION ALL确认不重叠函数包裹字段索引失效索引存原始值无法用于函数结果函数索引或数据清洗OR 分支含 NULL 判断漏数据三值逻辑理解不到位显式写IS NULL分支分区键 OR 非分区键分区裁剪失效OR 条件迫使扫描所有分区UNION ALL 拆分分区内外条件字段类型不同隐式类型转换OR 条件里类型不匹配统一数据类型或显式转换常量这个表你可以直接截图收藏以后写完 SQL 先对照一遍。5.2 一条 SQL 写完后的自查步骤我这里有一套自己用习惯的检查流程每写完一条复杂 SQL 都过一遍能拦住大多数问题第一步打开执行计划看有没有FULL TABLE SCAN。有的话先别急着骂 Oracle先确认是不是 OR 导致的。第二步检查 OR 两边的条件。是不是同一个字段是的话直接改 IN。不是的话问自己一句这两组结果集可能重叠吗可能重叠用 UNION不可能重叠用 UNION ALL。第三步检查有没有函数包字段。有的话要么把函数挪到常量侧要么考虑函数索引要么清洗数据。第四步检查字段类型和隐式转换。VARCHAR2 字段别拿数字去比DATE 字段别拿字符串去比。类型不统一索引大概率废掉。第五步看数据分布和统计信息。统计信息太久没收集优化器看不见直方图它对 OR 的选择率估算就会跑偏。定期DBMS_STATS.GATHER_TABLE_STATS能解决很多计划问题。这五步走完大部分 OR 相关的慢查询都能解决得七七八八。5.3 我踩过最深的坑把 UNION 当 UNION ALL 用最后再说一个我自己的黑历史。很早之前优化一条报表 SQL 时我把 OR 改成了 UNION因为当时怕有重复数据。结果 UNION 带了排序去重执行计划多了一个SORT (UNIQUE)步骤1900 万行的临时表排序直接把临时表空间撑爆了。后来我仔细核对业务规则确认两个条件分支的过滤键互斥一个是状态字段一个是来源字段两边逻辑上不可能同时成立把 UNION 改成 UNION ALL临时空间占用瞬间清零查询也快了几十倍。这次经历给我的教训是不要为了“逻辑绝对正确”而无脑加去重。该用 UNION ALL 就用 UNION ALL该用 UNION 才用 UNION。去重是有成本的而成本在数据量大了之后会成倍放大。写在最后的体会说实话OR 这个运算符在每个 SQL 入门教程里都排在很靠前的位置但真正把它用对需要在实战里摔几个跟头。我自己也是从全表扫描的泥潭里爬出来的所以特别明白那种“明明道理都懂一上线还是慢”的挫败感。如果你现在正在跟一个 OR 相关的慢查询较劲我建议你先别急着改 SQL花两分钟想想这段查询的业务上下文到底是什么两个条件分支有没有重叠能不能确认确认了之后再动手改写——想清楚业务再去调优往往比盲试各种写法高效得多。下一次再遇到 OR 的时候希望你能少踩一个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑