想把 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,就不会再去算 b,b 里面哪怕写了注定报错的表达式也能安全跳过。于是到了 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相关面试题里最经典的考点,也是实际生产环境里最容易漏数据的元凶。先看一段 SQL:
SELECT * FROM employee WHERE emp_name = '张三' OR emp_name <> '张三';这句的意图是:反正等于不等于都查出来了,那应该能取出全部员工,对吧?
但不对。因为emp_name如果是NULL,那么:
NULL = '张三'结果是 UNKNOWN;NULL <> '张三'结果也是 UNKNOWN;- UNKNOWN OR 任何值,只要另一边不是 TRUE,最终都是 UNKNOWN。
- 查询条件是 UNKNOWN 的行不会被返回。
所以上面这条 SQL 会漏掉所有 emp_name 为 NULL 的员工。
这是OR和NULL结合时最常见的隐患。解决办法也很直白:如果你明确需要把空值也查出来,就显式加上OR emp_name IS NULL:
SELECT * 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,才是真正让索引报废的重灾区。典型场景可以分成这几类:
第一类,跨字段 OR:
SELECT * 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 = 'test@example.com' OR cust_no = 'C20240001';这个查询上线后经常要跑好几秒甚至十秒。我看执行计划,三个字段上都有独立索引,但优化器选择了TABLE ACCESS FULL,因为三个 OR 分支的选择率都不高,合并成本评估下来优化器认为全表扫更划算。
我的改写方案是UNION ALL,因为这三个字段在业务上跟客户编号是一一对应的,不存在同一行通过不同条件命中两次的可能。改写后:
SELECT * FROM customer WHERE phone = '13800000000' UNION ALL SELECT * FROM customer WHERE email = 'test@example.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 的时候,希望你能少踩一个坑。