• SQL优化 第一章


            此博客内容根据《SQL优化核心思想》的流程来进行书写,仅作为个人学习记录使用。当然,有问题可以一起来探讨!

    一、基数(CARDINALITY)

    某个列唯一键(Distinct_Keys)的数量叫作基数。性别列只有男女,那么基数为2。

    以test为例,其中的某个列的基数如下:

    select count(distinct owner),count(distinct object_id),count(*) from test;

    从中我们可以看出总共有72889行,OWNER列的基数为26,说明该列有大量的重复值,OBJECT_ID与总条数一至,可以理解为主键,OWNER分布如下:

    1. SQL> col OWNER for a15;
    2. SQL> select owner,count(*) from test group by owner order by 2 desc;

    总结:

            如果某个列基数很低,该列数据分布就会非常不均衡,由于该列数据 分布不均衡,会导致 SQL 查询可能走索引,也可能走全表扫描。在做 SQL 优化的时候,如果 怀疑列数据分布不均衡,我们可以使用 select 列,count(*) from 表 group by 列 order by 2 desc 来查看列的数据分布。

            如果 SQL 语句是单表访问,那么可能走索引,可能走全表扫描,也可能走物化视图扫描。 在不考虑有物化视图的情况下,单表访问要么走索引,要么走全表扫描。现在,回忆一下走索 引的条件:返回表中 5%以内的数据走索引,超过 5%的时候走全表扫描。

    二、选择性(SELECTIVITY)

    基数与总行数的比值再乘以 100%就是某个列的选择性。

    搜集某个表中的信息:

    1. BEGIN
    2. DBMS_STATS.GATHER_TABLE_STATS(ownname => 'user',
    3. tabname => 'xxx',
    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. /

    查看某个表中每个列的基数和选择性:

    1. #这里注意用户名和表明都需要大写,否则查不到信息!!!!!!!
    2. col column_name for a15;
    3. select a.column_name,
    4. b.num_rows,
    5. a.num_distinct Cardinality,
    6. round(a.num_distinct / b.num_rows * 100, 2) selectivity,
    7. a.histogram,
    8. a.num_buckets
    9. from dba_tab_col_statistics a, dba_tables b
    10. where a.owner = b.owner
    11. and a.table_name = b.table_name
    12. and a.owner = 'user'
    13. and a.table_name = 'xxx';

    查看占比最大列的详细数据分布:

    1. col OBJECT_NAME for a20;
    2. select *
    3. from (select object_name, count(*)
    4. from test
    5. group by object_name
    6. order by 2 desc)
    7. where rownum <= 10;

    所以不管 object_name 传入任何值,最多返回101 行数据。

    结论:

            当一个列出现在 where 条件中,该列没有创建索引并且选择性大于 20%,那么该列就必须创建索引,从而提升 SQL 查询性能。当然,如果表只有几 百条数据,那我们就不用创建索引了。

    那么如何快速找出哪些列需要创建索引呢?

    在使用脚本之前先刷新数据库监控信息:

    1. begin
    2. dbms_stats.flush_database_monitoring_info;
    3. end;
    4. /

    思路:

    1.查询出哪个表的哪个列出现在where 条件中

    1. select r.name owner, o.name table_name, c.name column_name
    2. from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r
    3. where o.obj# = u.obj#
    4. and c.obj# = u.obj#
    5. and c.col# = u.intcol#
    6. and r.name = 'SCOTT'
    7. and o.name = 'TEST';

    2.查询出选择性大于等于 20%的列

    1. select a.owner,
    2. a.table_name,
    3. a.column_name,
    4. round(a.num_distinct / b.num_rows * 100, 2) selectivity
    5. from dba_tab_col_statistics a, dba_tables b
    6. where a.owner = b.owner
    7. and a.table_name = b.table_name
    8. and a.owner = 'SCOTT'
    9. and a.table_name = 'TEST'
    10. and a.num_distinct / b.num_rows >= 0.2;

    3.确保这些列没有创建索引

    1. select table_owner, table_name, column_name, index_name
    2. from dba_ind_columns
    3. where table_owner = 'SCOTT'
    4. and table_name = 'TEST';
    总结

    如下:

    1. ----注意修改用户名称(owner)和表名(table_name)
    2. ----使用sysdba用户操作
    3. select owner, column_name, num_rows, Cardinality, selectivity, 'Need index' as notice
    4. from (select b.owner,a.column_name,b.num_rows,a.num_distinct Cardinality,
    5. round(a.num_distinct / b.num_rows * 100, 2) selectivity
    6. from dba_tab_col_statistics a, dba_tables b
    7. where a.owner = b.owner
    8. and a.table_name = b.table_name
    9. and a.owner = 'SCOTT'
    10. and a.table_name = 'TEST')
    11. where selectivity >= 20
    12. and column_name not in (select column_name
    13. from dba_ind_columns
    14. where table_owner = 'SCOTT'
    15. and table_name = 'TEST')
    16. and column_name in
    17. (select c.name
    18. from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r
    19. where o.obj# = u.obj#
    20. and c.obj# = u.obj#
    21. and c.col# = u.intcol#
    22. and r.name = 'SCOTT'
    23. and o.name = 'TEST');
    三、直方图(HISTOGRAM)

            当某个列基数很低,该列数据分布就会不均衡。数据分布不均衡会导致在查询该列的时候,要么走全表扫描,要么走索引扫描,这个时候很容易走错执行计划。 如果没有对基数低的列收集直方图统计信息,基于成本的优化器(CBO)会认为该列数据分布是均衡的。所以要对基数很低(<1%)的列进行直方图收集。当然,对没有出现在 where 条件中的列收集直方图完全是做无用功,浪费数据库资源。

    脚本如下:

    抓出必须创建直方图的列:

    1. select a.owner,
    2. a.table_name,
    3. a.column_name,
    4. b.num_rows,
    5. a.num_distinct,
    6. trunc(num_distinct / num_rows * 100,2) selectivity,
    7. 'Need Gather Histogram' notice
    8. from dba_tab_col_statistics a, dba_tables b
    9. where a.owner = 'SCOTT'
    10. and a.table_name = 'TEST'
    11. and a.owner = b.owner
    12. and a.table_name = b.table_name
    13. and num_distinct / num_rows<0.01
    14. and (a.owner, a.table_name, a.column_name) in
    15. (select r.name owner, o.name table_name, c.name column_name
    16. from sys.col_usage$ u, sys.obj$ o, sys.col$ c, sys.user$ r
    17. where o.obj# = u.obj#
    18. and c.obj# = u.obj#
    19. and c.col# = u.intcol#
    20. and r.name = 'SCOTT'
    21. and o.name = 'TEST')
    22. and a.histogram ='NONE';
    总结:

            直方图是用来帮助 CBO 在对基数很低、数据分布不均衡的列进行 Rows 估算的时候,可以得到更精确的Rows

    四、回表(TABLE ACCESS BY INDEX ROWID)

    什么叫回表:

            当对一个列创建索引之后,索引会包含该列的键值以及键值对应行所在的 rowid。通过索引中记录的 rowid 访问表中的数据就叫回表。回表一般是单块读,回表次数太多会严重影响 SQL 性能,如果回表次数太多,就不应该走索引扫描了,应该直接走全表扫描。

    例子:

    1. set line 150;
    2. set autot trace;
    3. select * from test where owner='SYS';

    执行计划中框起来的(TABLE ACCESS BY INDEX ROWID)就是回表。索引返回多少行 数据,回表就要回多少次,每次回表都是单块读(因为一个 rowid 对应一个数据块)。该 SQL 返回了52343行数据,那么回表一共就需要52343次。

    请思考:上面执行计划的性能是耗费在索引扫描中还是耗费在回表中?

    实验:

            为了消除 arraysize 参数对逻辑读的影响, 设置 arraysize=5000。arraysize 表示 Oracle 服务器每次传输多少行数据到客户端,默认为 15。如果一个块有 150 行数据,那么这个块就会被读 10 次,因为每次只传输 15 行数据到客户 端,逻辑读会被放大。设置了 arraysize=5000 之后,就不会发生一个块被读 n 次的问题了。

    1. set arraysize 5000
    2. select owner from test where owner='SYS';

    从上面可知,索引扫描只耗费了 74 个逻辑读。

    select * from test where owner='SYS';

    1. set autot off
    2. select count(distinct dbms_rowid.rowid_block_number(rowid)) blocks from test where owner = 'SYS';

            SQL 在有回表的情况下,一共耗费了 1318个逻辑读,那么这 1318个逻辑读是怎么来的呢? SQL 返回的 52343 条数据一共存储在 1185 个数据块中,访问这 1185 个数据块就需要消耗 1185 个逻辑读,加上索引扫描的 122 个逻辑读,再加上 11 个逻辑读[其中 11=ROUND(52343/5000)],这 样累计起来刚好就是 1318 个逻辑读。 因此我们可以判断,该 SQL 的性能确实绝大部分损失在回表中! 更糟糕的是:假设 52343 条数据都在不同的数据块中,表也没有被缓存在 buffer cache 中, 那么回表一共需要耗费 52343 个物理 I/O,简直不可想象。

    总结:

            在无法避免回表的情况下,走索引如果返回数据量太多,必然会导致回表次数太多,从而 导致性能严重下降。类似于这样的  Select * from table where ...  就必须回表,所以我们必须严禁使用 Select *。类似于这样的  Select count(*) from table  这样的 SQL 就不需要回表。所以返回表中 5%以内的数据走索引、超过表中 5%的数据 走全表扫描。

    五、集群因子(CLUSTERING FACTOR)

            集群因子用于判断索引回表需要消耗的物理 I/O 次数。

            根据算法我们知道集群因子介于表的块数和表行数之间。

            如果集群因子与块数接近,说明表的数据基本上是有序的,而且其顺序基本与索引顺序一 样。这样在进行索引范围或者索引全扫描的时候,回表只需要读取少量的数据块就能完成。

             如果集群因子与表记录数接近,说明表的数据和索引顺序差异很大,在进行索引范围扫描 或者索引全扫描的时候,回表会读取更多的数据块。

             集群因子只会影响索引范围扫描(INDEX RANGE SCAN)以及索引全扫描(INDEX FULL SCAN),因为只有这两种索引扫描方式会有大量数据回表。 集群因子不会影响索引唯一扫描(INDEX UNIQUE SCAN),因为索引唯一扫描只返回一 条数据。集群因子更不会影响索引快速全扫描(INDEX FAST FULL SCAN),因为索引快速全扫描不回表。

    实验:

            先对测试表 test 的 object_id 列创建一个索引 idx_id:

    create index idx_id on test(object_id);

           

            查看该索引的集群因子:

    1. col OWNER for a10;
    2. col CLUSTERING_FACTOR for a15;
    3. col index_name for a15;
    4. select owner, index_name, clustering_factor from dba_indexes where owner = 'SCOTT' and index_name = 'IDX_ID';

           

             索引 idx_id 的叶子块中有序地存储了索引的键值以及键值对应行所在的 ROWID。

    select * from (select object_id, rowid from test where object_id is not null order by object_id) where rownum<=5; 

            人工计算集群因子的 SQL 脚本:

    1. select sum(case
    2. when block#1 = block#2 and file#1 = file#2 then
    3. 0
    4. else
    5. 1
    6. end) CLUSTERING_FACTOR
    7. from (select dbms_rowid.rowid_relative_fno(rowid) file#1,
    8. lead(dbms_rowid.rowid_relative_fno(rowid), 1, null) over(order by object_id) file#2,
    9. dbms_rowid.rowid_block_number(rowid) block#1,
    10. lead(dbms_rowid.rowid_block_number(rowid), 1, null) over(order by object_id) block#2
    11. from test
    12. where object_id is not null);

            表的总块数如下可知:

    select count(distinct dbms_rowid.rowid_block_number(rowid)) blocks from test; 

    在第一节基数的时候我们知道,表的总行数为72889行,总块数为1412块,得知集群因子非常接近表的总块数。现在,我们来查看下面 SQL 语句的执行计

    1. set arraysize 5000
    2. set autot trace
    3. select * from test where object_id < 1000;

    耗费了25个逻辑读。

    接下来新建一个test2,来对数据进行随机排序。

    1. create table test2 as select * from test order by dbms_random.value;
    2. #在 object_id 列创建索引 idx_id2
    3. create index idx_id2 on test2(object_id);

    查看索引 idx_id2 的集群因子。

    1. set autot trace
    2. select owner, index_name, clustering_factor from dba_indexes where owner = 'SCOTT' and index_name = 'IDX_ID2';

    索引 idx_id2 的集群因子接近于表的总行数(72889),回表的时候会读取更多的数据块,现在我们 来看一下 SQL 的执行计划。

    1. set arraysize 5000
    2. set autot trace
    3. select /*+ index(test2) */ * from test2 where object_id <1000;

            从上图得知,进行了1K+逻辑读。

            通过上面实验我们得知,集群因子太大会严重影响索引回表的性能,集群因子影响的是索引回表的物理 I/O 次数。我们假 设索引范围扫描返回了 1 000 行数据,如果 buffer cache 中没有缓存表的数据块,假设这 1000 行数据都在同一个数据块中,那么回表需要耗费的物理 I/O 就只需要一个;假设这 1000 行数 据都在不同的数据块中,那么回表就需要耗费 1 000 个物理 I/O。因此,集群因子影响索引回 表的物理 I/O 次数。

            切记不要尝试重建索引来降低集群因子,这根本没用,因为表中的数据顺序始终没变。 唯一能降低集群因子的办法就是根据索引列排序对表进行重建(create table new_table as select * from old_table order by 索引列),但是这在实际操作中是不可取的,因为我们无法照顾到每 一个索引。

     总结:

            集群因子只影响索引范围扫描和索引全扫描。当索引范围扫描,索引全扫描不回表或者返回数据量很少 的时候,不管集群因子多大,对 SQL 查询性能几乎没有任何影响。在进行 SQL 优化的时候,往往会建立合适的组合索引消除回表,或者建 立组合索引尽量减少回表次数。

  • 相关阅读:
    Dive into TensorFlow系列(2)- 解析TF核心抽象op算子
    JWT解密和python反序列化之[CISCN2019 华北赛区 Day1 Web2]ikun
    ORACLE 19C pdb修改的参数保存在哪个数据字典中?
    2023最新版Android逆向教程——第3天:ADB原理及其常用命令
    javaweb 使用element + vue 完善项目 servlet 优化
    Oracle LiveLabs实验:Automatic Partitioning in Autonomous Database
    VB.NET—DataGridView控件教程详解
    黑产反诈有方法,异常识别我在行—欺诈反洗钱等领域用得最多的经典算法
    JUC第二十五讲:JUC线程池-CompletableFuture 实现原理与实践
    Spring Security OAuth实现GitHub快捷登录
  • 原文地址:https://blog.csdn.net/qq_63163795/article/details/138036112