开发转兼职DBA(二):执行计划教我做事
查询慢了不知道为什么,加了索引还是慢,复合索引怎么建,执行计划怎么看——这些不是DBA的专利,是每个写SQL的开发者迟早要面对的事。
文章目录
- 开发转兼职DBA(二):执行计划教我做事
- 起点:查询慢了怎么办
- 执行计划是什么
- Oracle怎么看执行计划
- 已经在跑的SQL怎么看
- 我遇到的第一个问题:全表扫描
- 解决:加索引 + 改查询
- 索引的本质
- 第二个问题:加了索引还是慢
- 第三个问题:索引不是越多越好
- 常见的执行计划操作类型
- 统计信息:执行计划的基石
- 我学到的几条规矩
- 从"SQL消费者"到"SQL思考者"
起点:查询慢了怎么办
上一篇说到,社保系统查一个人要30秒。我当时的排查思路是:
- 重启数据库 → 没用
- 重启应用服务器 → 没用
- 清一下缓存 → 没用
- 问同事 → 他也不知道
- 百度"Oracle查询慢怎么办" → 有人说看执行计划
就这样,我第一次打开了执行计划。
执行计划是什么
执行计划就是数据库的施工图。
你写一条SQL,数据库不会直接执行。它会先"想"一下:这条SQL有几种执行方式?每种方式大概花多少成本?选成本最低的那种。
这个"想"的过程叫查询优化(Query Optimization),"想"出来的方案就是执行计划(Execution Plan)。
Oracle怎么看执行计划
EXPLAINPLANFORSELECT*FROMkc22WHEREsfzh='110101199001011234';SELECT*FROMTABLE(DBMS_XPLAN.DISPLAY);输出大概这样:
Plan hash value: 1234567890 | Id | Operation | Name | Rows | Bytes | Cost | |----|-----------------------------|------------|------|-------|------| | 0 | SELECT STATEMENT | | 1 | 100 | 2 | | 1 | TABLE ACCESS BY INDEX ROWID| KC22 | 1 | 100 | 2 | | 2 | INDEX UNIQUE SCAN | PK_KC22_SFZH | 1 | | 1 |关键看几个东西:
- Operation:数据库在干什么。
TABLE ACCESS FULL是全表扫描,INDEX RANGE SCAN是走索引,INDEX UNIQUE SCAN是走唯一索引 - Cost:数据库估算的成本。越低越好
- Rows:数据库估算会返回多少行。和实际差太远说明统计信息不准
已经在跑的SQL怎么看
生产环境的SQL已经在执行了,怎么回头看它的执行计划?
SELECT*FROMTABLE(DBMS_XPLAN.DISPLAY_CURSOR(sql_id=>'abc123',format=>'ALL'));或者查正在跑的SQL:
SELECTsql_id,sql_textFROMv$sqlWHEREsql_textLIKE'%kc22%';拿到sql_id再去查执行计划。
我遇到的第一个问题:全表扫描
SELECT*FROMkc22WHERExmLIKE'%张%';执行计划:
| Id | Operation | Name | Rows | Cost | |----|-------------------|------|-------|------| | 0 | SELECT STATEMENT | | 30000 | 200 | | 1 | TABLE ACCESS FULL| KC22 | 30000 | 200 |TABLE ACCESS FULL——30万行全扫一遍。原因有两个:
LIKE '%张%',通配符在前面,索引用不上xm字段上根本没有索引
解决:加索引 + 改查询
CREATEINDEXidx_kc22_xmONkc22(xm);然后改查询,通配符放后面:
SELECT*FROMkc22WHERExmLIKE'张%';执行计划变了:
| Id | Operation | Name | Rows | Cost | |----|-----------------------------|------------|------|------| | 0 | SELECT STATEMENT | | 500 | 5 | | 1 | TABLE ACCESS BY INDEX ROWID| KC22 | 500 | 5 | | 2 | INDEX RANGE SCAN | IDX_KC22_XM| 500 | 2 |Cost从200降到5。查询从30秒降到毫秒级。
这一刻我理解了一件事:索引不是加不加的问题,是加在哪、怎么查的问题。
索引的本质
索引就是书的目录。没有目录,找"张三"要从第一页翻到最后一页。有了目录,先查目录知道"张三"在第37页,直接翻过去。
数据库的索引通常是B+树。B+树的特点:
- 叶子节点有序——支持范围查询(
BETWEEN、LIKE '张%') - 叶子节点有指针——范围扫描时不需要回溯父节点
- 非叶子节点只存键值——树的高度很低,通常3~4层就能索引几千万行
查找过程:
根节点 → 比较键值 → 走对应的子节点 → ... → 叶子节点 → 找到行号 → 回表取数据"回表"是关键概念——索引里存的是键值+行号(ROWID),拿到行号后还要回原表取其他字段。如果SELECT只要索引里的字段,就不需要回表,这叫索引覆盖。
第二个问题:加了索引还是慢
社保系统有个查询:按身份证号和姓名同时查。
SELECT*FROMkc22WHEREsfzh='110101199001011234'ANDxm='张三';我加了两个索引:
CREATEINDEXidx_kc22_sfzhONkc22(sfzh);CREATEINDEXidx_kc22_xmONkc22(xm);以为够了吧?执行计划一看:
| Id | Operation | Name | Rows | Cost | |----|-----------------------------|---------------|------|------| | 0 | SELECT STATEMENT | | 1 | 4 | | 1 | TABLE ACCESS BY INDEX ROWID| KC22 | 1 | 4 | | 2 | INDEX UNIQUE SCAN | PK_KC22_SFZH | 1 | 2 |只走了sfzh上的索引。xm上的索引根本没用。
为什么?Oracle的优化器判断:身份证号已经能唯一定位一条记录了,再查姓名是多余的。所以只用了sfzh的索引。
那这个查询其实没问题——身份证号能唯一定位,效率已经很高了。
但换一个场景:按姓名和性别查。
SELECT*FROMkc22WHERExm='张三'ANDxb='1';这时候只有一个索引能用。如果"张三"有500个,数据库要先从索引里找到500个行号,再回表500次去检查性别。
更好的做法:复合索引。
CREATEINDEXidx_kc22_xm_xbONkc22(xm,xb);复合索引的排列顺序很重要。原则是选择性高的放前面:
- 姓名(选择性高,"张三"在30万人里可能有几百个)放前面
- 性别(选择性低,只有"男""女"两个值)放后面
为什么?因为复合索引是按定义顺序组织的。先按姓名排序,相同姓名再按性别排序。查询时先用姓名缩小范围,再用性别进一步过滤。如果性别放前面,索引的第一层只有两个分支,几乎没有过滤效果。
第三个问题:索引不是越多越好
有段时间我给每个查询字段都加了索引。结果:
- INSERT变慢了——每插一条数据,要同时更新所有索引
- UPDATE变慢了——改了一个索引列的值,索引要重新组织
- 存储空间涨了——索引也要占磁盘
一个表5个索引,意味着每次INSERT要写6个地方(1个表+5个索引)。
经验值:一个表的索引不要超过5~6个,高频查询的字段才加。可以通过监控V$SQL找出真正慢的查询,有针对性地加。
常见的执行计划操作类型
| 操作 | 含义 | 什么时候出现 |
|---|---|---|
| TABLE ACCESS FULL | 全表扫描 | 没有索引,或索引不适用 |
| INDEX UNIQUE SCAN | 唯一索引扫描 | 等值查询唯一索引列 |
| INDEX RANGE SCAN | 索引范围扫描 | 范围查询、前缀LIKE |
| TABLE ACCESS BY INDEX ROWID | 通过索引回表 | 索引里没有所需的所有列 |
| SORT ORDER BY | 排序 | ORDER BY且没有索引支持 |
| HASH JOIN | 哈希连接 | 大表关联,等值连接 |
| NESTED LOOPS | 嵌套循环 | 小表驱动大表 |
| MERGE JOIN | 合并连接 | 两个表都按连接列排序 |
看到TABLE ACCESS FULL不要慌——小表全表扫描比走索引还快。大表(超过几万行)全表扫描才是问题。
统计信息:执行计划的基石
有一次,我明明加了索引,执行计划还是走全表扫描。
折腾半天才发现:Oracle的统计信息过期了。
执行计划是优化器根据统计信息做决策的。统计信息包括:表有多少行、每个列有多少不同的值、数据分布情况等。如果统计信息不准,优化器就会做出错误判断。
-- 手动收集统计信息BEGINDBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT',tabname=>'KC22',cascade=>TRUE);END;/收集完,执行计划立刻变了。走了索引。
生产环境要定期收集统计信息。Oracle有自动任务,但有时候不够——大批量数据导入后、大量删除后,统计信息可能严重失真。
我学到的几条规矩
- 写SQL之前先想执行计划——这条SQL会扫多少行?能走索引吗?
- 索引加在查询条件上——SELECT的列不影响索引选择,WHERE的列才影响
- 复合索引注意顺序——选择性高的放前面
- 不要盲目加索引——索引有代价,写入变慢
- 慢查询先看执行计划——别猜,让数据库告诉你它在干什么
- 定期收集统计信息——执行计划的准确性依赖统计信息
从"SQL消费者"到"SQL思考者"
这个阶段最大的变化:以前写SQL只考虑"对不对",现在开始考虑"快不快"。
执行计划就是那个转折点。它让数据库告诉你它在干什么,而不是让你猜。
下一篇要进入更深的水域——当查询优化解决不了问题,数据库本身挂了的时候。
标签:#DBA #Oracle #执行计划 #索引 #全表扫描 #复合索引 #统计信息 #SQL优化