读《SQL优化核心思想》:你不知道的优化技巧
SQL性能问题已经逐步发展成为数据库性能的首要问题,80%的数据库性能问题都是因SQL而导致。
1.1 基数(CARDINALITY)
某个列唯一键(Distinct_Keys)的数量叫作基数。比如性别列,该列只有男女之分,所以这一列基数是2。主键列的基数等于表的总行数。基数的高低影响列的数据分布。
以测试表test为例,owner列和object_id列的基数分别如下所示。
1 SQL> select count(distinct owner),count(distinct object_id),count(*) from test; 2 COUNT(DISTINCTOWNER) COUNT(DISTINCTOBJECT_ID) COUNT(*) 3 -------------------- ------------------------ ---------- 4 29 72462 72462
TEST表的总行数为72 462,owner列的基数为29,说明owner列里面有大量重复值,object_id列的基数等于总行数,说明object_id列没有重复值,相当于主键。owner列的数据分布如下。
SQL> select owner,count(*) from test group by owner order by 2 desc; OWNER COUNT(*) -------------------- ---------- SYS 30808 PUBLIC 27699 SYSMAN 3491 ORDSYS 2532 APEX_030200 2406 MDSYS 1509 XDB 844 OLAPSYS 719 SYSTEM 529 CTXSYS 366 WMSYS 316 EXFSYS 310 SH 306 ORDDATA 248 OE 127 DBSNMP 57 IX 55 HR 34 PM 27 FLOWS_FILES 12 OWBSYS_AUDIT 12 ORDPLUGINS 10 OUTLN 9 BI 8 SI_INFORMTN_SCHEMA 8 ORACLE_OCM 8 SCOTT 7 APPQOSSYS 3 OWBSYS 2
owner列的数据分布极不均衡,我们运行如下SQL。
select * from test where owner=\'SYS\';
SYS有30 808条数据,从72 462条数据里面查询30 808条数据,也就是说要返回表中42.5%的数据。
SQL> select 30808/72462*100 "Percent" from dual; Percent ---------- 42.5160774
那么请思考,你认为以上查询应该使用索引吗?现在我们换一种查询语句。
select * from test where owner=\'SCOTT\';
SCOTT有7条数据,从72 462条数据里面查询7条数据,也就是说要返回表中0.009%的数据。
select 7/72462*100 "Percent" from dual; Percent ---------- .009660236
请思考,返回表中0.009%的数据应不应该走索引?
如果你还不懂索引,没关系,后面的章节我们会详细介绍。如果你回答不了上面的问题,我们先提醒一下。当查询结果是返回表中5%以内的数据时,应该走索引;当查询结果返回的是超过表中5%的数据时,应该走全表扫描。
当然了,返回表中5%以内的数据走索引,返回超过5%的数据就使用全表扫描,这个结论太绝对了,因为你还没掌握后面章节的知识,这里暂且记住5%这个界限就行。我们之所以在这里讲5%,是怕一些初学者不知道上面问题的答案而纠结。
现在有如下查询语句。
select * from test where owner=:B1;
语句中,“:B1”是绑定变量,可以传入任意值,该查询可能走索引也可能走全表扫描。
现在得到一个结论:如果某个列基数很低,该列数据分布就会非常不均衡,由于该列数据分布不均衡,会导致SQL查询可能走索引,也可能走全表扫描。在做SQL优化的时候,如果怀疑列数据分布不均衡,我们可以使用select列,count(*) from表group by列order by 2 desc来查看列的数据分布。
如果SQL语句是单表访问,那么可能走索引,可能走全表扫描,也可能走物化视图扫描。在不考虑有物化视图的情况下,单表访问要么走索引,要么走全表扫描。现在,回忆一下走索引的条件:返回表中5%以内的数据走索引,超过5%的时候走全表扫描。相信大家读到这里,已经搞懂了单表访问的优化方法。
我们来看如下查询。
select * from test where object_id=:B1;
不管object_id传入任何值,都应该走索引。
我们再思考如下查询语句。
select * from test where object_name=:B1;
不管给object_name传入任何值,请问该查询应该走索引吗?
请你去查看object_name的数据分布。写到这里,其实有点想把本节名称改为“数据分布”。大家在以后的工作中一定要注意列的数据分布!
1.2 选择性(SELECTIVITY)
基数与总行数的比值再乘以100%就是某个列的选择性。
在进行SQL优化的时候,单独看列的基数是没有意义的,基数必须对比总行数才有实际意义,正是因为这个原因,我们才引出了选择性这个概念。
下面我们查看test表各个列的基数与选择性,为了查看选择性,必须先收集统计信息。关于统计信息,我们在第2章会详细介绍。下面的脚本用于收集test表的统计信息。
SQL> BEGIN 2 DBMS_STATS.GATHER_TABLE_STATS(ownname => \'SCOTT\', 3 tabname => \'TEST\', 4 estimate_percent => 100, 5 method_opt => \'for all columns size 1\', 6 no_invalidate => FALSE, 7 degree => 1, 8 cascade => TRUE); 9 END; 10 / PL/SQL procedure successfully completed.
下面的脚本用于查看test表中每个列的基数与选择性。
SQL> select a.column_name, 2 2 b.num_rows, 3 3 a.num_distinct Cardinality, 4 4 round(a.num_distinct / b.num_rows * 100, 2) selectivity, 5 5 a.histogram, 6 6 a.num_buckets 7 7 from dba_tab_col_statistics a, dba_tables b 8 8 where a.owner = b.owner 9 9 and a.table_name = b.table_name 10 10 and a.owner = \'SCOTT\' 11 11 and a.table_name = \'TEST\'; 12COLUMN_NAME NUM_ROWS CARDINALITY SELECTIVITY HISTOGRAM NUM_BUCKETS 13--------------- ---------- ----------- ----------- --------- ----------- 14OWNER 72462 29 .04 NONE 1 15OBJECT_NAME 72462 44236 61.05 NONE 1 16SUBOBJECT_NAME 72462 106 .15 NONE 1 17OBJECT_ID 72462 72462 100 NONE 1 18DATA_OBJECT_ID 72462 7608 10.5 NONE 1 19OBJECT_TYPE 72462 44 .06 NONE 1 20CREATED 72462 1366 1.89 NONE 1 21LAST_DDL_TIME 72462 1412 1.95 NONE 1 22TIMESTAMP 72462 1480 2.04 NONE 1 23STATUS 72462 1 0 NONE 1 24TEMPORARY 72462 2 0 NONE 1 25GENERATED 72462 2 0 NONE 1 26SECONDARY 72462 2 0 NONE 1 27NAMESPACE 72462 21 .03 NONE 1 28EDITION_NAME 72462 0 0 NONE 0 2915 rows selected.
请思考:什么样的列必须建立索引呢?
有人说基数高的列,有人说在where条件中的列。这些答案并不完美。基数高究竟是多高?没有和总行数对比,始终不知道有多高。比如某个列的基数有几万行,但是总行数有几十亿行,那么这个列的基数还高吗?这就是要引出选择性的根本原因。
当一个列选择性大于20%,说明该列的数据分布就比较均衡了。测试表test中object_name、object_id的选择性均大于20%,其中object_name列的选择性为61.05%。现在我们查看该列数据分布(为了方便展示,只输出前10行数据的分布情况)。
SQL> select * 2 2 from (select object_name, count(*) 3 3 from test 4 4 group by object_name 5 5 order by 2 desc) 6 6 where rownum <= 10; 7OBJECT_NAME COUNT(*) 8------------------ ---------- 9COSTS 30 10SALES 30 11SALES_CHANNEL_BIX 29 12COSTS_TIME_BIX 29 13COSTS_PROD_BIX 29 14SALES_TIME_BIX 29 15SALES_PROMO_BIX 29 16SALES_PROD_BIX 29 17SALES_CUST_BIX 29 18DBMS_REPCAT_AUTH 5 1910 rows selected.
由上面的查询结果我们可知,object_name列的数据分布非常均衡。我们查询以下SQL。
select * from test where object_name=:B1;
不管object_name传入任何值,最多返回30行数据。
什么样的列必须要创建索引呢?当一个列出现在where条件中,该列没有创建索引并且选择性大于20%,那么该列就必须创建索引,从而提升SQL查询性能。当然了,如果表只有几百条数据,那我们就不用创建索引了。
下面抛出SQL优化核心思想第一个观点:只有大表才会产生性能问题。
也许有人会说:“我有个表很小,只有几百条,但是该表经常进行DML,会产生热点块,也会出性能问题。”对此我们并不想过多地讨论此问题,这属于应用程序设计问题,不属于SQL优化的范畴。
下面我们将通过实验为大家分享本文第一个全自动优化脚本。
抓出必须创建索引的列(请读者对该脚本适当修改,以便用于生产环境)。
首先,该列必须出现在where条件中,怎么抓出表的哪个列出现在where条件中呢?有两种方法,一种是可以通过V$SQL_PLAN抓取,另一种是通过下面的脚本抓取。
先执行下面的存储过程,刷新数据库监控信息。
begin
dbms_stats.flush_database_monitoring_info;
end;
运行完上面的命令之后,再运行下面的查询语句就可以查询出哪个表的哪个列出现在where条件中。
1select r.name owner, 2 o.name table_name, 3 c.name column_name, 4 equality_preds, ---等值过滤 5 equijoin_preds, ---等值JOIN 比如where a.id=b.id 6 nonequijoin_preds, ----不等JOIN 7 range_preds, ----范围过滤次数 > >= < <= between and 8 like_preds, ----LIKE过滤 9 null_preds, ----NULL 过滤 10 timestamp 11 from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r 12 where o.obj# = u.obj# 13 and c.obj# = u.obj# 14 and c.col# = u.intcol# 15 and r.name = \'SCOTT\' 16 and o.name = \'TEST\';
下面是实验步骤。
我们首先运行一个查询语句,让owner与object_id列出现在where条件中。
1SQL> select object_id, owner, object_type 2 2 from test 3 3 where owner = \'SYS\' 4 4 and object_id < 100 5 5 and rownum <= 10; 6 OBJECT_ID OWNER OBJECT_TYPE 7---------- -------------------- ----------- 8 20 SYS TABLE 9 46 SYS INDEX 10 28 SYS TABLE 11 15 SYS TABLE 12 29 SYS CLUSTER 13 3 SYS INDEX 14 25 SYS TABLE 15 41 SYS INDEX 16 54 SYS INDEX 17 40 SYS INDEX 1810 rows selected.
其次刷新数据库监控信息。
1SQL> begin 2 2 dbms_stats.flush_database_monitoring_info; 3 3 end; 4 4 / 5PL/SQL procedure successfully completed.
然后我们查看test表有哪些列出现在where条件中。
1SQL> select r.name owner, o.name table_name, c.name column_name 2 2 from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r 3 3 where o.obj# = u.obj# 4 4 and c.obj# = u.obj# 5 5 and c.col# = u.intcol# 6 6 and r.name = \'SCOTT\' 7 7 and o.name = \'TEST\'; 8OWNER TABLE_NAME COLUMN_NAME 9---------- ---------- ------------------------------ 10SCOTT TEST OWNER 11SCOTT TEST OBJECT_ID
接下来我们查询出选择性大于等于20%的列。
1SQL> select a.owner, 2 2 a.table_name, 3 3 a.column_name, 4 4 round(a.num_distinct / b.num_rows * 100, 2) selectivity 5 5 from dba_tab_col_statistics a, dba_tables b 6 6 where a.owner = b.owner 7 7 and a.table_name = b.table_name 8 8 and a.owner = \'SCOTT\' 9 9 and a.table_name = \'TEST\' 10 10 and a.num_distinct / b.num_rows >= 0.2; 11OWNER TABLE_NAME COLUMN_NAME SELECTIVITY 12---------- ---------- ------------- ----------- 13SCOTT TEST OBJECT_NAME 61.05 14SCOTT TEST OBJECT_ID 100
最后,确保这些列没有创建索引。
1SQL> select table_owner, table_name, column_name, index_name 2 2 from dba_ind_columns 3 3 where table_owner = \'SCOTT\' 4 4 and table_name = \'TEST\'; 5未选定行
把上面的脚本组合起来,我们就可以得到全自动的优化脚本了。
1SQL> select owner, 2 2 column_name, 3 3 num_rows, 4 4 Cardinality, 5 5 selectivity, 6 6 \'Need index\' as notice 7 7 from (select b.owner, 8 8 a.column_name, 9 9 b.num_rows, 10 10 a.num_distinct Cardinality, 11 11 round(a.num_distinct / b.num_rows * 100, 2) selectivity 12 12 from dba_tab_col_statistics a, dba_tables b 13 13 where a.owner = b.owner 14 14 and a.table_name = b.table_name 15 15 and a.owner = \'SCOTT\' 16 16 and a.table_name = \'TEST\') 17 17 where selectivity >= 20 18 18 and column_name not in (select column_name 19 19 from dba_ind_columns 20 20 where table_owner = \'SCOTT\' 21 21 and table_name = \'TEST\') 22 22 and column_name in 23 23 (select c.name 24 24 from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r 25 25 where o.obj# = u.obj# 26 26 and c.obj# = u.obj# 27 27 and c.col# = u.intcol# 28 28 and r.name = \'SCOTT\' 29 29 and o.name = \'TEST\'); 30OWNER COLUMN_NAME NUM_ROWS CARDINALITY SELECTIVITY NOTICE 31---------- ------------- ---------- ----------- ----------- ---------- 32SCOTT OBJECT_ID 72462 72462 100 Need index