ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Oracle多条件SELECT查询从入门到性能优化详解

Oracle多条件SELECT查询从入门到性能优化详解 在实际的Oracle开发和运维工作里写一条select查询很容易但真正让查询应对复杂业务、跑得又快又准往往就卡在“多条件查询”这层窗户纸上。不管是写报表取数、做后台管理功能还是排查线上数据问题你每天都会跟多条件select打交道。这篇就集中把Oracle里select多条件查询的用法捋一遍从最基础的AND、OR、到IN、LIKE、NULL判断再到条件组合与索引性能一次说透。我尽量不用教科书那种端着讲的口吻而是按真实干活时的思路来写先明白条件怎么搭再看业务场景里怎么用最后聊一聊“为什么这么写跑得快”“为什么那么写会踩坑”。适合刚入门Oracle的人从头跟一遍也适合写过一阵子SQL但总在条件组合上出问题的开发者查漏补缺。1. 先搞懂多条件查询的三个基础逻辑1.1 AND所有条件都要满足多条件查询最核心的构件就是AND。它表示“同时满足”相当于一层层缩小筛选范围。SELECT employee_id, employee_name, department_id, salary FROM employees WHERE department_id 30 AND salary 5000;这条语句的含义是先从员工表里找出部门编号是30的员工再在这个结果里继续筛掉工资不超过5000的人。执行顺序上说两步是一个整体写成一个SQL对数据库来说仍然是“一次扫描、多条件判断”所以不必担心拆成多条语句会更快。AND最关键的一点是你加的每一个条件都必须成立最终结果才会被返回。很多新手在这里犯迷糊总觉得“加一个条件应该能查出更多数据”恰恰相反AND加得越多结果通常越少因为它是交集逻辑。写AND条件时我习惯按“先过滤大范围、再过滤小范围”的视觉顺序排列这样后续看SQL的人好理解。虽然优化器不一定会按你写的顺序执行但维护的人会按你的书写顺序去读代码这是职业习惯问题。1.2 OR满足其中一个就行OR和AND正好相反它表示“满足任意一个即可”结果是并集逻辑。同一个表、同一个查询里OR会把满足任一条件的所有行都拉出来。SELECT employee_id, employee_name, department_id FROM employees WHERE department_id 30 OR department_id 50;这条查询返回的是部门30和部门50的所有员工。注意一个关键点OR的两个条件只要用同一个字段那完全可以改用IN后面我会单独讲IN写起来更简洁理解成本也更低。OR最让人头疼的是它和AND混在一起时的逻辑问题这也是所有SQL新手必经的坑。1.3 括号的优先级别让逻辑跑偏我见过太多线上事故根源就是WHERE子句里AND和OR混用忘了加括号。Oracle里AND的优先级高于OR也就是说数据库会先计算AND再计算OR。看这个例子SELECT * FROM employees WHERE department_id 30 OR department_id 50 AND salary 5000;很多人写这条SQL时心里的逻辑是“部门30或部门50的人并且工资都要大于5000”。但数据库实际执行的逻辑是部门30的所有员工加上部门50中工资大于5000的员工。部门30里那些工资低于5000的人也会被查出来。因为AND先结合所以这个条件被解析成department_id 30 OR (department_id 50 AND salary 5000)想要实现“部门30或50且工资都大于5000”的正确写法必须加括号SELECT * FROM employees WHERE (department_id 30 OR department_id 50) AND salary 5000;这里我要多说一句多条件查询一旦涉及OR最稳妥的做法就是用括号把OR整体包起来再和AND组合。哪怕你觉得优先级很熟加了括号也能让下一个接手你SQL的人少一层误解。括号是给数据库看的更是给人看的。2. 实际业务里最常见的五类条件场景2.1 IN与NOT IN列表匹配的优雅写法当同一个字段需要匹配多个值时IN是首选SELECT employee_id, employee_name, department_id FROM employees WHERE department_id IN (10, 20, 30);这等价于三个OR条件连写但IN可读性更好尤其当列表值很多时优势明显。IN后面既能放固定值列表也能放子查询SELECT employee_id, employee_name FROM employees WHERE department_id IN ( SELECT department_id FROM departments WHERE location_id 1700 );子查询返回一组部门ID外层用IN匹配。这里有个实践心得IN子查询的结果集不要太大否则外层查询性能会受影响如果子查询结果集能控制在几千行以内通常问题不大。NOT IN相比之下就要谨慎了。NOT IN的逻辑是“不在这组值里面”但它和NULL混在一起时会发生很反直觉的事情。如果IN后面的列表里包含NULL比如SELECT employee_id, employee_name FROM employees WHERE department_id NOT IN (10, 20, NULL);这条SQL不会返回任何行。原因是SQL里NULL参与比较的结果是“未知”NOT IN遇到NULL时整条过滤条件无法判定为真所以结果为空。业务上如果允许空值宁可用NOT EXISTS替代NOT IN这是个经验之谈。2.2 BETWEEN AND区间筛选的正确姿势处理数值和日期区间时BETWEEN AND很直观SELECT employee_id, employee_name, salary FROM employees WHERE salary BETWEEN 5000 AND 10000;这个写法包含边界值等价于WHERE salary 5000 AND salary 10000日期区间是实际使用中最容易出错的点。假设要查2024年1月的记录SELECT order_id, order_date FROM orders WHERE order_date BETWEEN TO_DATE(2024-01-01, YYYY-MM-DD) AND TO_DATE(2024-01-31, YYYY-MM-DD);这样写有个隐患如果order_date是DATE类型它包含时分秒。那么2024年1月31日这一天任何时间晚于零点零分零秒的数据都会被排除在外。更稳妥的写法是用半开区间SELECT order_id, order_date FROM orders WHERE order_date DATE 2024-01-01 AND order_date DATE 2024-02-01;这种“大于等于起点小于终点”的写法才是日期区间查询不出错的关键。我当时也是踩了两次边界值的坑才养成的习惯。2.3 LIKE模糊匹配小心通配符和转义多条件查询里一旦有模糊搜索的需求就得用LIKE。Oracle里的通配符有两个%表示任意多个字符包括零个_表示单个字符。SELECT employee_id, employee_name FROM employees WHERE employee_name LIKE 张%;这条语句查所有姓“张”的员工。如果名字里包含“张”字不管在哪个位置WHERE employee_name LIKE %张%这里有一个很多人都会忽略的问题LIKE条件如果写成%关键字或以%开头索引通常无法正常使用。非要以通配符开头去模糊搜索又想要性能就得考虑函数索引或其他方案。所以业务上能限定“按前缀搜索”时尽量别做成“包含搜索”。LIKE通配符的转义也有讲究。如果业务数据里本身包含%或_字符比如查一个带百分号的折扣字段你需要显式指定转义符SELECT * FROM product WHERE discount_desc LIKE 10\%% ESCAPE \;这里ESCAPE \告诉数据库\后面的%当作普通字符第一个%是真实内容第二个%是通配符。另外补充一个思路如果只是“判断字符串是否包含某个子串”而且不需要通配符自由匹配可以用INSTR函数WHERE INSTR(employee_name, 张) 0它和LIKE %张%效果类似而且有些场景下写起来更自然。但要注意INSTR同样是无法走常规索引的和前置通配符一样有性能代价。2.4 IS NULL的判断空值的陷阱NULL在SQL里代表“未知值”它不等于0也不等于空字符串。用等号或不等号去判断NULL是永远不成立的。多条件查询里涉及NULL判断时必须用IS NULL或IS NOT NULL。SELECT employee_id, commission_pct FROM employees WHERE commission_pct IS NULL;这条语句查出所有没有提成比例的员工。与之相对WHERE commission_pct IS NOT NULL这里有一个业务上常见的需求查询某个字段“等于空或等于某个值”。比如查备注为空或者备注为“已确认”的数据WHERE remark IS NULL OR remark 已确认这种写法逻辑上没问题但性能上要注意如果这个字段上有索引OR和NULL组合在一起通常也没法高效走索引。如果表数据量巨大可以考虑改成UNION ALL把两个条件拆开查再合并性能有时会好很多。这个问题后面性能部分还会提到。2.5 组合条件去重与排序DISTINCT和ORDER BY联动多条件查询出来的结果如果存在重复行可以用DISTINCT去重。比如查公司里有哪些部门在用某个状态SELECT DISTINCT department_id, status_code FROM employee_task WHERE status_code ACTIVE;注意DISTINCT是作用于整行组合的不是只作用于第一个字段。也就是说上面这条SQL去重的是department_id和status_code的组合而不是只让department_id不重复。ORDER BY的排序则是在多条件查询结果基础上进行的。排序字段可以是查询列也可以是没查出来的列但ORDER BY后面用别名会有一个小坑Oracle里ORDER BY可以使用别名但WHERE中不能直接使用别名。举个例子SELECT employee_name AS name, salary * 12 AS annual_salary FROM employees WHERE salary 5000 ORDER BY annual_salary DESC;这段在Oracle里能正常运行因为ORDER BY比SELECT晚一步生效它能识别别名。但如果在WHERE里写WHERE annual_salary 100000就会报错。多条件查询里如果涉及对查询结果再做筛选得用子查询或HAVING这也是新手经常混淆的地方。3. 一个完整的实战案例从需求到SQL一步步落地3.1 需求梳理与表结构为了把多条件查询讲得更贴近实际我构造一个简化版的订单查询场景。假设业务是电商系统核心表结构如下CREATE TABLE orders ( order_id NUMBER(10) PRIMARY KEY, customer_name VARCHAR2(50), order_date DATE, order_amount NUMBER(10,2), order_status VARCHAR2(20), channel VARCHAR2(20) );表里数据量大概几十万行order_status可能的值有PENDING、PAID、SHIPPED、CANCELLEDchannel可能的值有APP、WEB、PHONE。业务需求来了运营要查2024年3月到5月期间APP渠道产生的、订单金额大于1000元、且状态不是“已取消”的订单要求按金额从高到低排序只取前100条。3.2 逐步构造多条件查询直接把这段需求翻译成SQL初学者最容易写出这样一条“看起来很全”的查询SELECT * FROM orders WHERE channel APP AND order_amount 1000 AND order_status ! CANCELLED AND order_date DATE 2024-03-01 AND order_date DATE 2024-06-01 ORDER BY order_amount DESC;这版写法在逻辑上已经正确了但离“可以直接落地的报表SQL”还差两步。一是不要用SELECT *特别是在多表连接和宽表场景下将来别人维护根本不知道你实际需要哪几列。二是“只取前100条”这个需求还没实现。Oracle取前N条的标准写法是借助ROWNUM但ROWNUM不能直接和ORDER BY同层使用否则会先取前100条再排序结果完全不对。必须嵌套一层SELECT * FROM ( SELECT order_id, customer_name, order_date, order_amount, order_status, channel FROM orders WHERE channel APP AND order_amount 1000 AND order_status ! CANCELLED AND order_date DATE 2024-03-01 AND order_date DATE 2024-06-01 ORDER BY order_amount DESC ) WHERE ROWNUM 100;这条SQL里最内层按业务条件过滤并排序外层再截断前100行。这里的内层别名可以不给但为了可读性我给外层限定一个别名会更清晰。另一个办法是用Oracle 12c以后支持的FETCH FIRST语法SELECT order_id, customer_name, order_date, order_amount, order_status, channel FROM orders WHERE channel APP AND order_amount 1000 AND order_status ! CANCELLED AND order_date DATE 2024-03-01 AND order_date DATE 2024-06-01 ORDER BY order_amount DESC FETCH FIRST 100 ROWS ONLY;在12c以上版本里这种写法更直观也少了一层嵌套。如果生产库是11g那还是用ROWNUM嵌套方案稳妥。补充一个业务细节需求里“状态不是已取消”我用了! CANCELLED。如果order_status字段里可能存在NULL这个条件就会把NULL状态的数据排除掉因为NULL与任何值做不等比较结果是“未知”。这一点要看业务到底怎么定义状态字段。如果状态列永远不会为空那没问题。如果可能为空而且空值在业务上也算“有效订单”就应该改成AND (order_status ! CANCELLED OR order_status IS NULL)这种细节正是在实际业务里写多条件查询时真正决定SQL“对不对”的地方。3.3 参数化拼接SQL的注意事项实际开发里上面这些条件往往不是写死在SQL里的而是由前端把筛选条件传过来后端拼SQL。这时候最容易出现两个问题一是SQL注入风险二是“多条件任意组合”导致SQL写得非常臃肿。以Java后端为例最朴素的做法是用MyBatis或JPA动态拼接。但如果你手写JDBC则要注意永远用占位符而不是字符串拼接值String sql SELECT order_id, customer_name, order_amount FROM orders WHERE channel ? AND order_amount ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, APP); ps.setBigDecimal(2, new BigDecimal(1000));不要图省事写成String sql SELECT order_id FROM orders WHERE channel channel ;这样写的风险不必多说一旦channel被注入恶意字符串整个查询就可能被改写。另一个开发中很常见的场景是“前端选了什么条件就拼什么条件”导致SQL里有多个OR条件来判断某个筛选是否启用。这种写法的缺点是条件一多Oracle的SQL解析时间变长而且执行计划可能变得不稳定。更推荐的做法是用代码里先拼接好WHERE部分的动态SQL所有过滤条件统一走参数绑定。比如MyBatis的where标签会自动处理多余的AND和OR省心不少。最后如果这个多条件查询是给报表系统或后台列表页用的建议把时间范围、金额范围这类区间条件都加上边界值验证。比如用户选了开始日期晚于结束日期程序里要先拦截不要把这问题留给SQL去扛。4. 多条件查询的性能优化与索引思路4.1 条件的书写顺序会影响执行计划吗很多初学者会问WHERE里谁先写数据库是不是就按这个顺序执行在Oracle里CBO基于成本的优化器会根据统计信息重新决定执行顺序你写的条件顺序并不直接改变最终执行计划。但有一种例外当查询里使用了提示或者特殊写法时顺序的作用才会体现。常规情况下真正影响性能的是条件能否使用索引、扫描的数据量大小而不是书写顺序。因此我不建议把时间浪费在“调整条件顺序”上而应该把精力放在索引设计和执行计划分析上。需要说明的是虽然执行计划不受书写顺序影响但从维护角度我依然建议把等值条件写在范围条件前面这样读SQL的人能更快抓住查询的核心过滤逻辑。4.2 避免写出让索引失效的条件多条件查询一旦数据量大性能瓶颈通常出现在某些写法导致索引失效。常见的有四种**第一对索引列使用函数。**比如WHERE TRUNC(order_date) DATE 2024-05-20如果order_date上有索引TRUNC函数包裹后普通索引无法使用。需要改成范围条件WHERE order_date DATE 2024-05-20 AND order_date DATE 2024-05-21**第二隐式类型转换。**比如order_date是DATE类型但条件里传入字符串WHERE order_date 2024-05-20Oracle会尝试把字符串转成日期如果转换规则与NLS设置有关不仅可能出错索引也容易失效。所以条件值的类型要和列类型匹配不能依赖Oracle的隐式转换。**第三OR条件组合。**OR即使每一个分支都用了索引列优化器也可能选择全表扫描尤其当OR连接的是不同列时。一个值得尝试的改写是用UNION ALLSELECT order_id, customer_name FROM orders WHERE channel APP UNION ALL SELECT order_id, customer_name FROM orders WHERE channel WEB;当然如果两个分支会重复用UNION去掉重复。到底是OR好还是UNION ALL好不是绝对的要结合数据分布和执行计划来判断。我的经验是OR分支越多越倾向用UNION ALL代替。第四前导通配符。LIKE %关键词这样的写法索引基本用不上。如果业务必须频繁做包含匹配可以考虑使用Oracle的全文索引或者额外的倒排表但这属于比较重的方案了小表直接扫描反而更快。4.3 多条件组合下的执行计划分析当你写完一条复杂的多条件查询尤其涉及IN子查询、OR、多表关联时不要拍脑袋觉得“差不多”要养成看执行计划的习惯。最简单的方式是在SQL客户端里执行EXPLAIN PLAN FOR SELECT order_id, customer_name, order_amount FROM orders WHERE channel APP AND order_amount 1000 AND order_date DATE 2024-03-01 AND order_date DATE 2024-06-01;然后查询执行计划SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);看执行计划时重点关注两个地方TABLE ACCESS FULL是否出现以及ROWS预估行数是否和实际数量差距过大。如果发现明明条件里有等值过滤却还是全表扫描优先检查该列上是否有索引以及条件写法是否触发了上面说的索引失效问题。另一个实用工具是查看SQL实际执行时的统计信息。在SQL*Plus里可以这样SET AUTOTRACE TRACEONLY STATISTICS SELECT ...;它会返回实际执行产生的逻辑读、物理读和排序次数这比只靠感觉要靠谱得多。多条件查询最容易出现的性能隐患是“条件越多越难走索引”。比如五个条件都能单独走索引但组合之后优化器可能选择一个都无法用或者成本更高。这时候除了看执行计划还要考虑是不是可以建立复合索引。复合索引的列顺序有讲究区分度高的、选择性好的列放前面常用于等值条件的列放前面范围条件尽量放后面。比如订单查询里channel是等值条件且区分度不高order_date是范围条件order_amount也是范围条件那么复合索引可以尝试(channel, order_amount, order_date)或(channel, order_date, order_amount)具体哪个更好最好用真实数据测试后再定。5. 常见报错与排查技巧实录5.1 报错类型速查表多条件查询写多了总会遇到几种固定报错。我把最常见的几类整理出来方便你遇到时报错后一眼定位。报错信息常见原因处理思路ORA-00933: SQL command not properly endedWHERE或ORDER BY位置写错或者语句末尾多了分号但位置不对检查关键词顺序确保WHERE在FROM后、ORDER BY在WHERE后ORA-00920: invalid relational operator条件里少了运算符如WHERE salary 5000检查条件列后面是否漏了、、、BETWEEN、LIKEORA-00979: not a GROUP BY expressionSELECT列中出现未包含在GROUP BY中的列多条件查询配合分组时SELECT中的非聚合列必须出现在GROUP BY里ORA-00904: invalid identifier列名写错或者列名大小写加了引号导致不匹配核实列名拼写Oracle默认列名是大小写不敏感的但加双引号后区分大小写ORA-01722: invalid number字符串转数字失败常出现在隐式类型转换中检查条件值是否是数字格式比如日期字段传入非日期字符串ORA-00907: missing right parenthesis括号不成对常见于IN子查询嵌套或OR括号遗漏逐层数括号尤其检查OR组合时左括号是否有对应右括号ORA-01428: argument is out of range某个函数的参数超出允许范围比如SUBSTR长度、日期计算参数异常检查函数传入参数是否合理常见于动态拼接SQL时参数被篡改这个表格里最常遇到的是ORA-00933和ORA-00920多数情况都是条件表达式书写顺序出了问题。比如把WHERE放到了GROUP BY后面或者漏写了运算符。遇到报错先别慌从关键词位置排查是最高效的。5.2 几个容易忽略的细节坑先说ROWNUM与ORDER BY的配合。多条件查询里如果先写ORDER BY再在外面套ROWNUM结果是对的。但如果直接写SELECT * FROM orders WHERE ROWNUM 100 ORDER BY order_amount DESC;Oracle先按ROWNUM截断前100行再对100行排序最终结果根本不是金额最大的前100条。这个问题我在新手阶段栽过后来养成了习惯涉及到“先排序再取前N行”的需求永远用子查询包一层。再说别名与WHERE的坑。多条件查询中如果对某个计算结果起了别名别名只能在ORDER BY或HAVING里使用WHERE中不认识别名。比如SELECT employee_name, salary * 12 AS annual_salary FROM employees WHERE annual_salary 100000;这条会直接报ORA-00904。想过滤计算结果要么把完整表达式写在WHERE里要么套一层子查询。然后是字符串比较的大小写问题。Oracle的字符串比较默认是大小写敏感的除非你设置NLS_COMP和NLS_SORT做大小写不敏感比较。业务上要求按状态或渠道筛选时生产数据里如果混入了大小写不一致的情况WHERE channel app就查不出APP记录。这种问题排查起来挺费劲的最稳妥的办法是写入时统一规范查询条件统一用固定值。还有一个冷门但很实用的排查点当多条件查询结果和预期不一致且SQL看起来没什么问题时先查一下是否有重复数据。比如一条订单有两条状态变更记录用DISTINCT或者直接把订单ID拉出来数一下重复次数往往就能发现问题。很多“SQL写错了”的现象其实是业务数据本身存在重复。5.3 动态拼接SQL的场景化避坑在实际项目中查询条件往往由用户在前端勾选后端动态拼接。这种情况下最常见的错误是用户一个条件都没选时SQL变成了SELECT * FROM orders WHERE ORDER BY order_date DESCWHERE后面紧跟ORDER BY直接报ORA-00933。解决办法是在拼接时维护一个条件列表先拼好WHERE子句再拼ORDER BY所有条件用AND串起来最后再执行。这个逻辑用代码写很繁琐但确实是动态查询的必要防御。一个更隐蔽的坑是“只根据用户勾选的条件追加过滤不处理默认条件”。比如列表页默认只看未删除数据那么deleted_flag 0这个条件必须始终拼接而不能依赖用户勾选否则漏数据。我在写这类动态查询时习惯先把过滤条件放到一个ListString里最后统一用AND连接并保证所有参数通过占位符传入。比如Java伪代码逻辑ListString conditions new ArrayList(); ListObject params new ArrayList(); if (channel ! null !channel.isEmpty()) { conditions.add(channel ?); params.add(channel); } if (minAmount ! null) { conditions.add(order_amount ?); params.add(minAmount); } String sql SELECT * FROM orders; if (!conditions.isEmpty()) { sql WHERE String.join( AND , conditions); } sql ORDER BY order_date DESC;这种写法的好处是条件动态增减但逻辑始终清晰不会出现WHERE后无条件的尴尬。还有一点是关于分页。Oracle经典分页写法用ROWNUM三层嵌套这在前面“取前N条”的场景中已经体现过。如果查询条件本身很复杂我强烈建议把过滤和排序先放在最内层子查询里再做分页避免把分页条件和业务过滤混在一起性能也好排查。我个人在实际项目里还做过一个优化当多条件查询里经常出现的组合比较固定时直接把组合条件建成视图应用层只查视图SQL会简洁得多。但要注意视图只是一个封装它内部SQL复杂时性能该差还是差所以视图建立的前提是内部查询本身已经调优过。踩过几次坑之后我现在的习惯是任何一条多条件查询上线前至少做两件事——看执行计划、用接近生产的数据量做一次实际跑批测试。不要因为表小就马虎数据量一上去条件组合会暴露各种意想不到的问题。多条件查询的用法说起来并不复杂核心就是理解布尔逻辑、掌握常用条件写法、懂得结合索引优化再积累一些报错排查经验。真正让你写出高质量SQL的不是背语法而是每一次查询都能问自己一句条件逻辑对不对这个条件能不能走索引数据量变大后还会不会快带着这三个问题去写时间久了自然就有感觉。
返回列表