Oracle开发者性能课第8课(如何更快地进行插入更新和删除)实验

Posted dingdingfish

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Oracle开发者性能课第8课(如何更快地进行插入更新和删除)实验相关的知识,希望对你有一定的参考价值。

概述

本实验参考DevGym中的实验指南

创建环境

创建表和过程,其中我加了注释:

-- 创建PL/SQL包,用于计时。
create or replace package timing_pkg as  
 
  start_time pls_integer; 
  time_taken pls_integer; 
   
  procedure set_start_time; 
  procedure calc_runtime (  
    operation varchar2 
  ); 
   
end; 
/

create or replace package body timing_pkg as  
   
  procedure set_start_time as 
  begin 
    start_time := dbms_utility.get_time; 
  end; 
   
  procedure calc_runtime (  
    operation varchar2 
  ) as 
  begin 
    time_taken :=  
      ( dbms_utility.get_time - start_time ); 
    dbms_output.put_line ( operation || ' ' || time_taken || ' hundredths of a second' ); 
  end; 
   
end; 
/

-- 测试表bricks
create table bricks ( 
  brick_id integer  
    not null  
    primary key, 
  colour   varchar2(10),  
  shape    varchar2(10), 
  weight   integer 
)
;

-- 下面这段代码不知何用。执不执行都无所谓。
begin 
  insert into bricks values ( 1, 'red', 'cylinder', 1 );  
  insert into bricks values ( 2, 'blue', 'cube', 1 );  
  insert into bricks values ( 3, 'green', 'cube', 1 );  
  delete bricks; 
  commit; 
end; 
/
truncate table bricks;

-- 此过程用于插入指定行数的记录到bricks表,插入前会清空表
create or replace procedure ins_rows ( num_rows int ) as
begin
  execute immediate 'truncate table bricks';
  dbms_random.seed ( 0 );
  insert into bricks  
  with rws as ( 
    select level x from dual 
    connect by level <= num_rows 
  ) 
    select rownum, 
           case mod ( rownum, 3 )  
             when 0 then 'red' 
             when 1 then 'blue' 
             when 2 then 'green' 
           end, 
           case mod ( rownum, 2 )  
             when 0 then 'cube' 
             when 1 then 'cylinder' 
           end, 
           round ( dbms_random.value ( 2, 10 ) ) 
    from   rws;
end ins_rows;
/

查看数据,目前表为空:

SQL> select * from bricks;

no rows selected

获取DML的执行计划

DML语句其实也是包括SELECT的,无论插入、删除还是更新,都是使用DBMS_XPlan获取执行计划:

-- 插入单条,不会用到索引
insert /*+ gather_plan_statistics */ into bricks values ( 0, 'red', 'cylinder', 1 ); 
  
select * from   table(dbms_xplan.display_cursor(format => 'iosTATS LAST'));

                                                                               PLAN_TABLE_OUTPUT
________________________________________________________________________________________________
SQL_ID  81b27pds9qjrw, child number 0
-------------------------------------
insert /*+ gather_plan_statistics */ into bricks values ( 0, 'red',
'cylinder', 1 )


---------------------------------------------------------------------------------------------
| Id  | Operation                | Name   | Starts | A-Rows |   A-Time   | Buffers | Reads  |
---------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT         |        |      1 |      0 |00:00:00.01 |      41 |      8 |
|   1 |  LOAD TABLE CONVENTIONAL | BRICKS |      1 |      0 |00:00:00.01 |      41 |      8 |
---------------------------------------------------------------------------------------------

Note
-----
   - cpu costing is off (consider enabling it)


17 rows selected.

-- CTAS,批量插入,不会用到索引
insert /*+ gather_plan_statistics */ into bricks 
  select level, 'red', 'cylinder', 1 
  from   dual
  connect by level <= 100;
  
select * from   table(dbms_xplan.display_cursor(format => 'IOSTATS LAST'));
                                                                                    PLAN_TABLE_OUTPUT
_____________________________________________________________________________________________________
SQL_ID  fpmz0m5yrx4kj, child number 0
-------------------------------------
insert /*+ gather_plan_statistics */ into bricks    select level,
'red', 'cylinder', 1    from   dual   connect by level <= 100

Plan hash value: 1236776825

--------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name   | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT              |        |      1 |        |      0 |00:00:00.01 |       5 |
|   1 |  LOAD TABLE CONVENTIONAL      | BRICKS |      1 |        |      0 |00:00:00.01 |       5 |
|   2 |   CONNECT BY WITHOUT FILTERING|        |      1 |        |    100 |00:00:00.01 |       0 |
|   3 |    FAST DUAL                  |        |      1 |      1 |      1 |00:00:00.01 |       0 |
--------------------------------------------------------------------------------------------------


16 rows selected.

-- 更新单条,用到了索引。因为表有主键
update /*+ gather_plan_statistics */ bricks 
set    shape = 'cube'
where  brick_id = 1;
  
select * from   table(dbms_xplan.display_cursor(format => 'IOSTATS LAST'));

                                                                              PLAN_TABLE_OUTPUT
_______________________________________________________________________________________________
SQL_ID  0zbg60ms26www, child number 0
-------------------------------------
update /*+ gather_plan_statistics */ bricks  set    shape = 'cube'
where  brick_id = 1

Plan hash value: 1792883719

--------------------------------------------------------------------------------------------
| Id  | Operation          | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
--------------------------------------------------------------------------------------------
|   0 | UPDATE STATEMENT   |             |      1 |        |      0 |00:00:00.01 |       2 |
|   1 |  UPDATE            | BRICKS      |      1 |        |      0 |00:00:00.01 |       2 |
|*  2 |   INDEX UNIQUE SCAN| SYS_C008657 |      1 |      1 |      1 |00:00:00.01 |       1 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("BRICK_ID"=1)


20 rows selected.

-- 批量删除,用到了索引。因为表有主键
delete /*+ gather_plan_statistics */ bricks where  brick_id <= 10;
  
select * from   table(dbms_xplan.display_cursor(format => 'IOSTATS LAST'));

                                                                             PLAN_TABLE_OUTPUT
______________________________________________________________________________________________
SQL_ID  2sdbmwz2j55y0, child number 0
-------------------------------------
delete /*+ gather_plan_statistics */ bricks where  brick_id <= 10

Plan hash value: 1389320603

-------------------------------------------------------------------------------------------
| Id  | Operation         | Name        | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------
|   0 | DELETE STATEMENT  |             |      1 |        |      0 |00:00:00.01 |      14 |
|   1 |  DELETE           | BRICKS      |      1 |        |      0 |00:00:00.01 |      14 |
|*  2 |   INDEX RANGE SCAN| SYS_C008657 |      1 |     11 |     11 |00:00:00.01 |       1 |
-------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("BRICK_ID"<=10)

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)


23 rows selected.

检查副作用

影响写的因素包括:

  • 触发器
  • 阻塞锁

触发器最好少用,但不是说不能用。否则发明它干嘛。

查看定义的触发器:

select * from user_triggers;

与 SELECTS 不同,其他会话中未提交的更改可以阻止写入完成。常见原因有:

  • 插入的主键或唯一键发生冲突
  • 另一个会话更新或删除相同的行

很难完全避免这个问题。例如,如果两个用户需要同时更新相同的行,则第二个用户必须等到第一个用户结束他们的事务。为尽量减少其影响,请确保尽快提交或回滚事务(也就是小事务)。如果这些问题仍然存在,您可能需要重新设计处理数据更改的方式。

单行插入与批量插入

以下为单行插入或逐行插入,是应该尽量避免的:

begin
  delete bricks;
  for i in 1 .. 10000 loop 
    insert into bricks  
      values (  
        i,  
        case mod ( i, 3 )  
          when 0 then 'red' 
          when 1 then 'blue' 
          when 2 then 'green' 
        end, 
        case mod ( i, 2 )  
          when 0 then 'cube' 
          when 1 then 'cylinder' 
        end, 
        round ( dbms_random.value ( 2, 10 ) ) 
       ); 
  end loop; 
end;
/

尽管单个插入的执行速度都很快,但这些加起使整个过程变慢。如果 SQL 来自中间层应用程序,这尤其成问题,因为每一行往返数据库的时间很长。

如果在上面循环中加上提交(逐行提交),整个过程会更慢!

可以使用批量处理FOR ALL加快此过程。例如:

delete bricks;
select count(*) from bricks;

declare
  type bricks_rec is record (
    brick_id integer, colour varchar2(10),
    shape    varchar2(10), weight integer
  );
  type bricks_array is table of bricks_rec
    index by pls_integer;
    
  brick_values bricks_array;
begin 
  for i in 1 .. 10000 loop
    brick_values ( i ) := bricks_rec (
      brick_id => i + 20000, 
      colour => case mod ( i, 3 )  
        when 0 then 'red' 
        when 1 then 'blue' 
        when 2 then 'green' 
      end, 
      shape => case mod ( i, 2 )  
        when 0 then 'cube' 
        when 1 then 'cylinder' 
      end, 
      weight => round ( dbms_random.value ( 2, 10 ) ) 
    );
  end loop;
  
  forall rws in 1 .. brick_values.count
    insert into bricks  
    values brick_values ( rws );
    
end; 
/

-- 下面的commit是我加的
commit;

select count(*) from bricks;

FORALL 看起来和 FOR 循环类似。但与循环不同的是,它只处理内部的 DML 语句一次(一次性插入一个数组)。

单行插入与批量插入性能比较

原文代码如下:

declare
  num_rows pls_integer := 100000;
  type bricks_rec is record (
    brick_id integer, colour varchar2(10),
    shape    varchar2(10), weight integer
  );
  type bricks_array is table of bricks_rec
    index by pls_integer;
    
  brick_values bricks_array;
begin 
  delete bricks;
  commit;
  timing_pkg.set_start_time; 
  for i in 1 .. num_rows loop 
    insert into bricks  
      values (  
        i,  
        case mod ( i, 3 )  
          when 0 then 'red' 
          when 1 then 'blue' 
          when 2 then 'green' 
        end, 
        case mod ( i, 2 )  
          when 0 then 'cube' 
          when 1 then 'cylinder' 
        end, 
        round ( dbms_random.value ( 2, 10 ) ) 
       ); 
  end loop; 
  timing_pkg.calc_runtime ( 'Insert-loop' ); 
   
  rollback; 

  timing_pkg.set_start_time; 
  for i in 1 .. num_rows loop
    brick_values ( i ) := bricks_rec (
      brick_id => i, 
      colour => case mod ( i, 3 )  
        when 0 then 'red' 
        when 1 then 'blue' 
        when 2 then 'green' 
      end, 
      shape => case mod ( i, 2 )  
        when 0 then 'cube' 
        when 1 then 'cylinder' 
      end, 
      weight => round ( dbms_random.value ( 2, 10 ) ) 
    );
  end loop;

  forall rws in 1 .. brick_values.count
    insert into bricks  
    values brick_values ( rws );

  timing_pkg.calc_runtime ( 'Insert-forall' ); 

end; 
/

要产生输出,需要设置:

set serveroutput on

比较结果如下,批量插入快了近10倍。其中hundredths of a second表示计时单位为1/100秒:

Insert-loop 231 hundredths of a second
Insert-forall 25 hundredths of a second

许多单行插入 vs. 一个多行插入

可以使用 INSERT 将行从一个表复制到另一个表。此代码使用游标将 10,000行从一个表加载到另一个表:

begin
  delete bricks;
  for rw in ( 
    with rws as ( 
      select level x from dual 
      connect by level <= 10000
    ) 
    select rownum id, 
           case mod ( rownum, 3 )  
             when 0 then 'red' 
             when 1 then 'blue' 
             when 2 then 'green' 
           end colour, 
           case mod ( rownum, 2 )  
             when 0 then 'cube' 
             when 1 then 'cylinder' 
           end shape, 
           round ( dbms_random.value ( 2, 10 ) ) weight
    from   rws
  ) loop 
    insert into bricks  
    values (  
      rw.id,  
      rw.colour, 
      rw.shape,
      rw.weight
    ); 
  end loop; 
end;
/

以下为更快的INSERT-AS-SELECT方法,一次性添加所有 10,000 行:

delete bricks;
insert into bricks  
  with rws as ( 
    select level x from dual 
    connect by level <= 10000
  ) 
    select rownum, 
           case mod ( rownum, 3 )  
             when 0 then 'red' 
             when 1 then 'blue' 
             when 2 then 'green' 
           end, 
           case mod ( rownum, 2 )  
             when 0 then 'cube' 
             when 1 then 'cylinder' 
           end, 
           round ( dbms_random.value ( 2, 10 ) ) 
    from   rws; 

当然,之前的FORALL INSERT方法仍然可用:

declare
  type bricks_rec is record (
    brick_id integer, colour varchar2(10),
    shape    varchar2(10), weight integer
  );
  type bricks_array is table of bricks_rec
    index by pls_integer;
    
  brick_values bricks_array;
  num_rows pls_integer := 10000;
begin 
  delete bricks;
  with rws as ( 
    select level x from dual 
    connect by level <= num_rows 
  ) 
    select rownum, 
           case mod ( rownum, 3 )  
             when 0 then 'red' 
             when 1 then 'blue' 
             when 2 then 'green' 
           end, 
           case mod ( rownum, 2 )  
             when 0 then以上是关于Oracle开发者性能课第8课(如何更快地进行插入更新和删除)实验的主要内容,如果未能解决你的问题,请参考以下文章

Oracle开发者性能课第4课(如何创建索引)实验

Oracle开发者性能课第1课(如何阅读执行计划)实验

Oracle开发者性能课第9课(如何查找慢 SQL)实验

Oracle开发者性能课第7课(Join如何工作)实验

Oracle开发者性能课第3课(我的查询做了多少工作)实验

Oracle开发者性能课第5课(为什么我的查询不使用索引)实验