目录
问题说明
解决方案 - 场景A
解决方案 - 场景B
解决方案 - 场景C
深入理解索引 - 索引的逻辑结构
深入理解索引 - 索引的三个特点
深入理解索引 - 索引的注意事项
问题说明:
系统上线后,用户数据不断增长,从最初的几十万记录发展至上千万、甚至上亿条记录。运维人员反馈原有的存储过程运行效率愈发低下。review时发现以下可改进之处:
场景A:数据表的索引与查询语句子条件(where)筛选字段无关,未合理创建索引;
场景B:业务逻辑分析不足,产生非必要的表关联,50%的语句可改写为单表查询语句;
场景C:数据表本身已有相关索引,但在查询时未正确使用索引;
各场景解决方案: 由于不方便用项目代码举例,以下sql涉及的字段名、数据表名均用别名替换。
场景A:.必要表连接 + 无索引,可调整为 必要表连接 + 索引检索
表b:file_name,file_month,file_type(索引I:file_month)
表d: fee,file_name,file_month(无索引)
SELECT fee
FROM d
WHERE file_name IN (SELECT file_name FROM b WHERE file_month=202002 and file_type= 'XXX');
【执行计划 - 必要表连接 + 无索引】
【优化分析】由于业务层面上,B与D表的file_month不等价,需保留表连接。执行计划中的索引扫描“INDEX(FAST FULL SCAN)”用到了b表的索引I(file_month),可在 表d 的file_name列新建索引提升查询性能。
【新建索引】CREATE INDEX d_file_name_idx ON d (file_name) ;
【执行计划 - 必要表连接 + 索引检索】
【成本优化】:269416 -- 1087
场景B:非必要表连接 + 无索引 ,可调整为 无表连接 + 索引检索:
表c:file_name, file_month
表d:fee,file_name,file_month(数据量 3018488)
SELECT fee
FROM d
WHERE file_name IN (SELECT file_name FROM c WHERE file_month=202002);
【执行计划 - 非必要表连接 + 无索引 】:
【优化分析】d表的file_month与c表的file_month在业务层面是等价的,因此,c、d表无需关联,并在file_month列建索引:
【无表连接】
SELECT fee
FROM d
WHERE file_month= 202002;
【执行计划 - 无表连接 + 无索引】
【新建索引】 CREATE INDEX d_file_month_idx ON d (file_month) ;
【执行计划 - 无表连接 + 索引检索】
【成本优化】268758 - 267987 - 5
场景C:索引列嵌套函数,未合理使用索引
表d:amount, file_day(索引)
SELECT dis_info_fee
FROM d
WHERE ROUND(file_day/100)=202002;
【执行计划 - 索引列嵌套函数】
【优化分析】表d的索引列file_day也在使用中嵌套了函数,数据查询时无法使用索引,为避免在索引列使用函数,需以业务逻辑为基准改写查询方式。业务需求为查询当月的数据,故修改为:
SELECT dis_info_fee
FROM d
WHERE file_day >= 202002*100+1 AND file_day < (202002+1)*100
【执行计划 - 索引列无附加函数】
【成本优化】268170 - 14930
深入理解索引:
1、索引的逻辑结构:
索引的逻辑结构分为三部分:Root(根块)、Branch(茎块)、Leaf(叶子块)。
Leaf(叶子块)存储【 key column value(索引列具体值)+ 数据块所在rowid】,即索引块,当一个Leaf(叶子块)存满后,则开辟新空间,存储到新的Leaf(叶子快),这时出现了两个Leaf(叶子块)的管理者 ---- Branch(茎块)。
Branch(茎块)存储指向Leaf(叶子块)的指针,当一个Branch(茎块)存满后,则开辟新空间,存储到新的Branch(茎块),这时出现了两个Branch(茎块)的管理者 ---- Root(根块);
... ... ... ...
以此类推,索引的逻辑结构可一直往上堆叠,呈现“金字塔”结构。
由于索引块只包含个别字段的信息,而数据块包含数据表所有字段的信息,使用索引列查询可提高查询效率。
2、索引的三个特点:
a.索引高度较低:
b.索引存储列值 + ROWID值,且 索引列取值可为空
c.索引本身有序
以下示例仍以表d为例, 数据量1.3亿。表d:fee,cdr_seq(索引列)
2.1 特点a应用 :索引列 = 某一具体值
SELECT *
FROM d
WHERE cdr_seq = 'XXXXXXX';
【执行计划】
按“金字塔”结构存储索引,五百万记录,索引高度可能为 两 层;
500G数据包含几百亿条记录,索引高度仅为 六 层;
通过索引检索一条数据只需完成(高度 + 1)次I/O操作。
需注意:查询字段包括除索引列的字段时,会产生回表(TABLE ACCESS (BY INDEX ROWID)),增加 I/O开销。
2.2 特点b应用 --- 统计函数 COUNT( ) / SUM( ) / AVG( )
SELECT COUNT(*)
FROM d --- 1.3亿,58.84s
【执行计划】
SELECT COUNT(*)
FROM d
WHERE cdr_seq IS NOT NULL --- 1.3亿,22.84s
【执行计划】
【成本优化】1086226 - 166539
本示例说明索引列取值可为空,若指定索引字段取值非空,COUNT(*) / SUM(*) / AVG(*)函数可以通过索引字段统计信息,提高统计效率。
2.3 特点c应用 --- ORDER BY / DISTINCT
SELECT cdr_seq
FROM d
WHERE cdr_seq >= 1230201 ORDER BY cdr_seq
【执行计划 - 无索引】
【执行计划 - 索引检索】
本示例说明新建索引后,因索引的有序性,可消除ORDER BY语句产生的排序(SORT (ORDER BY))
2.4 特点b/c综合应用 --- 统计函数 MAX( ) / MIN( )
SELECT MAX(cdr_seq)
FROM d;
【执行计划】
本示例并未限定索引列非空,在执行计划中仍然用到索引扫描,再次说明索引列可以取值为空。另一方面,索引本身是有序的,在读取最大值时,只需读取最右边叶子块的最后一条记录,即可获得最大值。
综上,总结四种索引扫描类型:
扫描类型 | 说明 | ||
---|---|---|---|
1 | INDEX RANGE SCAN |
局部扫描; 返回记录越少,效率越少; |
索引列 = 具体取值 |
2 | INDEX FAST FULL SCAN |
全局扫描,一次读取多个索引块;本身无序,可能需要额外排序; |
COUNT(),SUM(),AVG() |
3 | INDEX FAST SCAN |
全局扫描,一次读取一个索引块;本身有序,可消除排序; |
ORDER BY,DISTINCT |
4 | INDEX FULL SCAN(MIN/MAX) |
局部扫描,效率最高 |
MAX()只需读取最右边的叶子快;MN()只需读取最左边的叶子快; |
3、使用索引的注意事项:
a. 分区局部索引:若筛选条件只有索引字段,用不到分区字段,性能将会下降;
b. 可通过组合索引避免回表(TABLE ACCESS by INDEX ROWID)造成的性能下降;
c. 聚合因子越低 (表和索引两者的排列顺序相似度越高),排序计算量越低;
d. 分析执行计划时应关注成本(COST),必要时可用空间(BYTES)消耗换取成本(COST)优化;
e. 不可在索引列嵌套函数;
f. 根据业务场景精简表结构字段;