Thursday, September 2, 2021

Statistic Numbers in the cloud or not.


Thanks to my buddy Jared Still for pointing this out to me.  The Statistic Number for the consistent gets stat has been several different values over the last few releases (see below).   Which means that many statistic number have also been different over the releases as well. 

 

He put together a nifty little function that makes the script that I’ve been doing testing for timings more portable.  I used this script most recently in my last post (Inserting into multiple tables in the cloud or not). 

 

Here is the cool little function he created:

 

create or replace function get_cg_statnum return pls_integer deterministic

   is

      i_statnum pls_integer;

   begin

      select stname.statistic# into i_statnum

      from v$statname  stname

      where stname.name = 'consistent gets';

      return i_statnum;

   end;

/

 

You can use it like this:

 

select get_cg_statnum from dual;

 

To get the statistics number for consistent gets on your database.  Below I have a modified version of the script I used in the last post to use this function.  

 

Jared was able to pull the statistic number for consistent gets from a few versions:

 

VERSION           STATISTIC#

----------------- ----------

21.0.0.0.0               209

19.0.0.0.0               163

12.1.0.2.0               132

11.2.0.4.0                88

 

 

And one last thing.  Don’t make the rookie mistake I did.  To be able to select from v$statname in a function/procedure you have to be granted the select privilege directly, not thru a role.  If you don’t have it directly, the function will not be able to select from the table.  Duh!

 

Here is the new script:

 

-- setup

drop table emp_10 purge;

drop table emp_20 purge;

drop table emp_30 purge;

drop table emp_xx purge;

create table emp_10 as select * from emp where 1=0;

create table emp_20 as select * from emp where 1=0;

create table emp_30 as select * from emp where 1=0;

create table emp_xx as select * from emp where 1=0;

-- end setup 

 

set serveroutput on;

 

create or replace function get_cg_statnum return pls_integer deterministic

   is

      i_statnum pls_integer;

   begin

      select stname.statistic# into i_statnum

      from v$statname  stname

      where stname.name = 'consistent gets';

      return i_statnum;

   end;

/

 

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

    congets_num  pls_integer :=0; 

begin

    select get_cg_statnum into congets_num from dual; 

    select value into l_start_cr from v$mystat where STATISTIC# = congets_num;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

        INSERT

            WHEN (deptno=10) THEN

            INTO emp_10 (empno,ename,job,mgr,sal,deptno)

            VALUES (empno,ename,job,mgr,sal,deptno) 

            WHEN (deptno=20) THEN

            INTO emp_20 (empno,ename,job,mgr,sal,deptno)

            VALUES (empno,ename,job,mgr,sal,deptno) 

            WHEN (deptno=30) THEN

            INTO emp_30 (empno,ename,job,mgr,sal,deptno)

            VALUES (empno,ename,job,mgr,sal,deptno)

            ELSE

            INTO emp_xx (empno,ename,job,mgr,sal,deptno) 

            VALUES (empno,ename,job,mgr,sal,deptno)

            SELECT * FROM emp;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('Inserting into 4 tables with one command 100,000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    select value into l_end_cr from v$mystat where STATISTIC# = congets_num;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/

-- delete data from tables 

delete from emp_10;

delete from emp_20;

delete from emp_30;

delete from emp_xx;

 

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

    congets_num  pls_integer :=0; 

begin

    select get_cg_statnum into congets_num from dual; 

    select value into l_start_cr from v$mystat where STATISTIC# = congets_num;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

        INSERT

            INTO emp_10 (empno,ename,job,mgr,sal,deptno)

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno =10;

        INSERT

            INTO emp_20 (empno,ename,job,mgr,sal,deptno)

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno =20;

        INSERT

            INTO emp_30 (empno,ename,job,mgr,sal,deptno)

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno =30;

        INSERT

            INTO emp_xx (empno,ename,job,mgr,sal,deptno) 

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno is null;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('Inserting into 4 tables with four commands 100,000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    select value into l_end_cr from v$mystat where STATISTIC# = congets_num;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/

 

-- delete data from tables 

delete from emp_10;

delete from emp_20;

delete from emp_30;

delete from emp_xx;

 

Friday, August 20, 2021

Inserting into multiple tables in the cloud or not.

 


There is more to the INSERT command than most folks are aware.  

 

My buddy Dan Morgan (The man behind Morgan’s Library) remined me recently of this.  For example, you can use one statement to insert into multiple tables at once. 

 

 

The basic syntax looks like this:

 

 

INSERT

WHEN (<condition>) THEN

INTO <table_name> (<column_list>)

VALUES (<values_list>) 

WHEN (<condition>) THEN

INTO <table_name> (<column_list>)

VALUES (<values_list>)

ELSE

INTO <table_name> (<column_list>)

VALUES (<values_list>)

SELECT <column_list> FROM <table_name>;

 

 

You can have many WHEN conditions.  This syntax only would insert into the first table where condition is true.   An advantage of the one statement is the ELSE part.  This is nice to put stuff in a table that just doesn’t match the other conditions.  This could be tricky with multiple statements.  For my test below, it was simple, the DEPNO is one of four values, 10,20, 30 or NULL for the table.  Hence it was easy to come up with four statements to get all the rows into the four separate tables. I think you can image cases where this wouldn’t be as straight forward.

 

 

If you add ALL after the word INSERT, it would insert into any of the tables where the condition is true.  With the ALL version you could have no WHEN conditions at all and it would insert into all the tables in the statement.  This could be useful when you are moving a row from a wide table (many columns) and you are breaking that up into several narrow tables.  For example, you have a table that is not normalized and you want to normalize it into a set of tables.

 

 

This is super cool of course, but as a performance guy, how does it perform?  I set up a test to see at least in one case how it did. This test takes the good old EMP table and populates 4 other tables based on DEPTNO, I do this 100,000 times to have a reasonable amount of activity. (The code at the bottom if you’d like to try it yourself.)  The results are below from both a 21c super cool autonomous database and from a 12c database on one of my old windows laptop. 

 

 

As I do, each test was run multiple times to weed out any noise to get a good idea of what is happening.  In this test I ran the script with the SETUP part once, then commented the SETUP lines and reran it multiple times, this way the tables are already there and have been inserted into to weed out any noise with the high-water mark and such.  The results below are representative of these runs after the first one. 

 

 

For the 21c Database:

 

 

......................................................

Inserting into 4 tables with one command 100,000 times

......................................................

In hundredths of a second

**** TIME - 3133

**** CPU  - 3105

**** LIO  - 619022

 

 

......................................................

Inserting into 4 tables with four commands 100,000 times

......................................................

In hundredths of a second

**** TIME - 6874

**** CPU  - 6769

**** LIO  - 1230158

 

 

For the 12c Database:

 

 

......................................................

Inserting into 4 tables with one command 100,000 times

......................................................

In hundredths of a second

**** TIME - 1622

**** CPU  - 1111

**** LIO  - 707520

 

 

......................................................

Inserting into 4 tables with four commands 100,000 times

......................................................

In hundredths of a second

**** TIME - 2784

**** CPU  - 2627

**** LIO  - 1308283

 

 

Over all it took a bit more than twice as long to do the task with 4 separate statement rather than one, elapsed time and CPU time.  Not unexpected, I would think that one statement should perform better then several.  But it wasn’t 4 times as long as one might have thought, only about twice as long.   LIOs were just a bit less than twice as many when doing 4 statements rather than one. 

 

 

What does this mean?  It’s likely better to use one statement rather than multiple if you can.  And it might be slightly easier to maintain this code over time.  It does require a slightly different way of thinking about your inserts of course. 

 

 

Code for the test below.  Make sure you use the correct statistic number for the consistent gets statistic.  Its 139 for 12c and earlier, 209 for 21c. 

 

 

-- setup

drop table emp_10 purge;

drop table emp_20 purge;

drop table emp_30 purge;

drop table emp_xx purge;

create table emp_10 as select * from emp where 1=0;

create table emp_20 as select * from emp where 1=0;

create table emp_30 as select * from emp where 1=0;

create table emp_xx as select * from emp where 1=0;

-- end setup 

 

set serveroutput on;

 

 

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

begin

    --the consistent gets statistic is #139 in 12c and #209 in 21c 

    --select value into l_start_cr from v$mystat where STATISTIC# = 139;

    select value into l_start_cr from v$mystat where STATISTIC# = 209;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

        INSERT

            WHEN (deptno=10) THEN

            INTO emp_10 (empno,ename,job,mgr,sal,deptno)

            VALUES (empno,ename,job,mgr,sal,deptno) 

            WHEN (deptno=20) THEN

            INTO emp_20 (empno,ename,job,mgr,sal,deptno)

            VALUES (empno,ename,job,mgr,sal,deptno) 

            WHEN (deptno=30) THEN

            INTO emp_30 (empno,ename,job,mgr,sal,deptno)

            VALUES (empno,ename,job,mgr,sal,deptno)

            ELSE

            INTO emp_xx (empno,ename,job,mgr,sal,deptno) 

            VALUES (empno,ename,job,mgr,sal,deptno)

            SELECT * FROM emp;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('Inserting into 4 tables with one command 100,000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    --select value into l_end_cr from v$mystat where STATISTIC# = 139;

    select value into l_end_cr from v$mystat where STATISTIC# = 209;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/

-- delete data from tables 

delete from emp_10;

delete from emp_20;

delete from emp_30;

delete from emp_xx;

 

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

begin

    --the consistent gets statistic is #139 in 12c and #209 in 21c 

    --select value into l_start_cr from v$mystat where STATISTIC# = 139;

    select value into l_start_cr from v$mystat where STATISTIC# = 209;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

        INSERT

            INTO emp_10 (empno,ename,job,mgr,sal,deptno)

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno =10;

        INSERT

            INTO emp_20 (empno,ename,job,mgr,sal,deptno)

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno =20;

        INSERT

            INTO emp_30 (empno,ename,job,mgr,sal,deptno)

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno =30;

        INSERT

            INTO emp_xx (empno,ename,job,mgr,sal,deptno) 

            SELECT empno,ename,job,mgr,sal,deptno FROM emp where deptno is null;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('Inserting into 4 tables with four commands 100,000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    --select value into l_end_cr from v$mystat where STATISTIC# = 139;

    select value into l_end_cr from v$mystat where STATISTIC# = 209;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/

 

-- delete data from tables 

delete from emp_10;

delete from emp_20;

delete from emp_30;

delete from emp_xx;

Thursday, July 29, 2021

One Row to Rule Them All in Exadata on the cloud or not.


Way back in 2018 (Seems like a life time ago, doesn’t it?) I did a post about retrieving one row from a table you can see it
 HERE.   The other day I was looking at my blog and saw this post and wondered, what would this test be like on Exadata?  And thanks to Oracle having the super cool always free option in the cloud I can easily test it.  

 

One thing I found out quickly is that in 21c the statistic numbers have changed. (This may have change in 18 I’m not sure.)  The statistic number for “consistent gets” used to be 139 when I did this in my 12 and earlier databases.  It’s now 209.  There are a bunch of new statistics in the consistent gets group.  I believe that the change was made  to keep them all together they were all moved to the numbers of 209 to 222, they used to be 139 to 146.  Several of the new ones are PMEM for persistent memory.  I’ve not dived into that just yet to see all details about these, but just the name gives a decent clue about what those are about.

 

Back to my test.  In the original post I was showing that to get a single row from a table, it’s best to use an index.  A unique index is even better and the best was a unique covering index. 

 

But is that still true in Exadata?  

 

Yes, it is.  

 

Here is the run output.  As I did in the original post, I ran this several times and the results are consistent.   The test code, which is at the bottom of this post, creates a small table and then selects the 10 rows one at a time 100,000 times.  I time it  in both elapsed time and CPU time.  Also, I capture how many consistent gets (LIOs) were done.

 

 

......................................................

selecting all 10 rows without an index 100000 times

......................................................

In hundredths of a second

**** TIME - 4419

**** CPU  - 4214

**** LIO  - 7001404

 

......................................................

selecting all 10 rows with an index 100000 times

......................................................

In hundredths of a second

**** TIME - 968

**** CPU  - 932

**** LIO  - 2000003


......................................................

selecting all 10 rows with a unique index 100000 times

......................................................

In hundredths of a second

**** TIME - 855

**** CPU  - 831

**** LIO  - 2000004

 

......................................................

selecting all 10 rows with a unique covering index 100000 times

......................................................

In hundredths of a second

**** TIME - 824

**** CPU  - 816

**** LIO  - 1000002

 

 

The output didn’t surprise me, I figured it would be the same on Exadata was it was on a non-Exadata configuration, and it sure is good to have it prove out in a test.   

 

The key take away is that if all you want to get from a table is one row, the fastest way is to use and unique index.  If available, a covering index (one that has all the columns you want) is even better, in an Exadata configuration or non-Exadata.

 

What this doesn’t mean is that if you are doing a join on a primary key and joining a significant number of rows from this table to another, that the index is a good thing in Exadata.  A Smart Scan during a full table scan is very likely to be better.  I talk about this in a post HERE.

 

Here is the code I ran to do the above test.

 

rem testing to get one row out of a table.

rem SEP2018  RVD

rem JUL2021  RVD updated for 21c 

rem

set echo on feedback on serveroutput on

drop table iamsmall purge;

create table iamsmall (id_col number, data_col varchar(20));

begin

 for x in 1..10 loop

   insert into iamsmall values (x, (dbms_random.string('A',20)));

 end loop;

end;

/

 

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

begin

    --the consistent gets statistic is #139 in 12c and #209 in 21c 

    --select value into l_start_cr from v$mystat where STATISTIC# = 139;

    select value into l_start_cr from v$mystat where STATISTIC# = 209;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

     for i in 1 .. 10 loop

        select data_col into x1 from iamsmall where id_col = i;

     end loop;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('selecting all 10 rows without an index 100000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    --select value into l_end_cr from v$mystat where STATISTIC# = 139;

    select value into l_end_cr from v$mystat where STATISTIC# = 209;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/

 

rem create a nonunique index on id_col

create index ias_id on iamsmall(id_col);

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

begin

    select value into l_start_cr from v$mystat where STATISTIC# = 209;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

     for i in 1 .. 10 loop

        select data_col into x1 from iamsmall where id_col = i;

     end loop;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('selecting all 10 rows with an index 100000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    select value into l_end_cr from v$mystat where STATISTIC# = 209;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/

 

rem create a unique index on id_col

drop index ias_id;

create unique index ias_id on iamsmall(id_col);

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

begin

    select value into l_start_cr from v$mystat where STATISTIC# = 209;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

     for i in 1 .. 10 loop

        select data_col into x1 from iamsmall where id_col = i;

     end loop;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('selecting all 10 rows with a unique index 100000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    select value into l_end_cr from v$mystat where STATISTIC# = 209;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/

 

 

rem create a unique covering index on id_col,data_col

 

drop index ias_id;

create unique index ias_id on iamsmall(id_col, data_col);

declare

    x1           varchar2(20);

    l_start_time pls_integer;

    l_start_cpu  pls_integer;

    l_start_cr   pls_integer :=0;

    l_end_cr     pls_integer :=0;

begin

    select value into l_start_cr from v$mystat where STATISTIC# = 209;

    l_start_time := DBMS_UTILITY.GET_TIME;

    l_start_cpu  := DBMS_UTILITY.GET_CPU_TIME;

    for ii in 1 .. 100000 loop

     for i in 1 .. 10 loop

        select data_col into x1 from iamsmall where id_col = i;

     end loop;

    end loop;

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('selecting all 10 rows with a unique covering index 100000 times');

    DBMS_OUTPUT.put_line ('......................................................');

    DBMS_OUTPUT.put_line ('In hundredths of a second');

    DBMS_OUTPUT.put_line ('**** TIME - '||to_char(DBMS_UTILITY.GET_TIME - l_start_time));

    DBMS_OUTPUT.put_line ('**** CPU  - '||to_char(DBMS_UTILITY.GET_CPU_TIME - l_start_cpu));

    select value into l_end_cr from v$mystat where STATISTIC# = 209;

    DBMS_OUTPUT.put_line ('**** LIO  - '||to_char( l_end_cr - l_start_cr));

end;

/