Posts Tagged Performance Tuning

Real-Time Materialized Views in #Oracle 12c

helps

In 12cR2, a Materialized View that is STALE can still speed up queries while delivering correct results. The data from the stale MV is then on the fly combined with the change information from MV logs in an operation called ON QUERY COMPUTATION. The result is delivered slightly slower as if the MV were FRESH, so there is some overhead involved in the process. But it should be noticeable faster than having to do Full Table Scans as it was required in versions before 12c in that situation.

Operationally, that means that REFRESH can be done less frequently while keeping satisfactory query performance all the time. Let’s see that in action:

[oracle@uhesse ~]$ sqlplus adam/adam@pdb1

SQL*Plus: Release 12.2.0.1.0 Production on Thu Jan 5 14:31:00 2017

Copyright (c) 1982, 2016, Oracle.  All rights reserved.

Last Successful login time: Thu Jan 05 2017 10:57:35 +01:00

Connected to:
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

SQL> set timing on
SQL> select channel_id,sum(amount_sold) from sales group by channel_id;

CHANNEL_ID SUM(AMOUNT_SOLD)
---------- ----------------
	 1	    4000000
	 2	    4000000
	 4	    4000000
	 3	    4000000
	 0	    4000000

Elapsed: 00:00:03.47
SQL> set timing off

The query takes more than three seconds without an MV initially.

SQL> create materialized view log on sales
     with rowid, sequence(channel_id,amount_sold)
     including new values;   

Materialized view log created.

SQL> create materialized view mv1
     refresh fast on demand
     enable query rewrite 
     enable on query computation
     as
     select channel_id,
     sum(amount_sold),
     count(amount_sold),
     count(*)
     from sales
     group by channel_id;  

Materialized view created.

SQL> set timing on                                                     
SQL> select channel_id,sum(amount_sold) from sales group by channel_id;

CHANNEL_ID SUM(AMOUNT_SOLD)
---------- ----------------
	 1	    4000000
	 2	    4000000
	 4	    4000000
	 3	    4000000
	 0	    4000000

Elapsed: 00:00:00.07
SQL> set timing off

The FRESH MV speeds up the query – not yet new. The same kind of execution plan would have been used in 11g:

SQL> @lastplan

PLAN_TABLE_OUTPUT
-------------------------------------------------------------------------------------
SQL_ID	9wwp2am6pm4dz, child number 1
-------------------------------------
select channel_id,sum(amount_sold) from sales group by channel_id

Plan hash value: 2958490228

-------------------------------------------------------------------------------------
| Id  | Operation		     | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT	     |	    |	    |	    |	  3 (100)|	    |
|   1 |  MAT_VIEW REWRITE ACCESS FULL| MV1  |	  5 |	 30 |	  3   (0)| 00:00:01 |
-------------------------------------------------------------------------------------


13 rows selected.

Now I change something in the sales table, making the MV STALE:

SQL> update sales set amount_sold=2 where rownum<2; 

1 row updated. 

SQL> commit;

Commit complete.

SQL> select mview_name,staleness,on_query_computation from user_mviews;

MVIEW_NAME STALENESS	       O
---------- ------------------- -
MV1	   NEEDS_COMPILE       Y

In spite of the STALE MV, the next query is still fast, although not as fast as with the FRESH MV:

SQL> set timing on
SQL> select channel_id,sum(amount_sold) from sales group by channel_id;

CHANNEL_ID SUM(AMOUNT_SOLD)
---------- ----------------
	 2	    4000000
	 3	    4000000
	 4	    4000000
	 0	    4000000
	 1	    4000001

Elapsed: 00:00:00.12
SQL> set timing off

So what happens is roughly this:

realtime_mv

That there’s some work been done under the covers is revealed by looking at the (rather scary) execution plan now:

SQL> @lastplan

PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------------------------
SQL_ID	9wwp2am6pm4dz, child number 2
-------------------------------------
select channel_id,sum(amount_sold) from sales group by channel_id

Plan hash value: 2525395710

---------------------------------------------------------------------------------------------------
| Id  | Operation			    | Name	  | Rows  | Bytes | Cost (%CPU)| Time	  |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT		    |		  |	  |	  |    18 (100)|	  |
|   1 |  VIEW				    |		  |   363 |  9438 |    18  (23)| 00:00:01 |
|   2 |   UNION-ALL			    |		  |	  |	  |	       |	  |
|*  3 |    VIEW 			    | VW_FOJ_0	  |   100 |  2900 |	7  (15)| 00:00:01 |
|*  4 |     HASH JOIN FULL OUTER	    |		  |   100 |  4300 |	7  (15)| 00:00:01 |
|   5 |      VIEW			    |		  |	5 |   160 |	3   (0)| 00:00:01 |
|   6 |       MAT_VIEW ACCESS FULL	    | MV1	  |	5 |    60 |	3   (0)| 00:00:01 |
|   7 |      VIEW			    |		  |   100 |  1100 |	4  (25)| 00:00:01 |
|   8 |       HASH GROUP BY		    |		  |	  |	  |	4  (25)| 00:00:01 |
|*  9 |        TABLE ACCESS FULL	    | MLOG$_SALES |	2 |    74 |	3   (0)| 00:00:01 |
|  10 |    VIEW 			    |		  |   263 |  6838 |    11  (28)| 00:00:01 |
|  11 |     UNION-ALL			    |		  |	  |	  |	       |	  |
|* 12 |      FILTER			    |		  |	  |	  |	       |	  |
|  13 |       NESTED LOOPS OUTER	    |		  |   250 | 16000 |	4  (25)| 00:00:01 |
|  14 |        VIEW			    |		  |   100 |  5200 |	4  (25)| 00:00:01 |
|* 15 | 	FILTER			    |		  |	  |	  |	       |	  |
|  16 | 	 HASH GROUP BY		    |		  |	  |	  |	4  (25)| 00:00:01 |
|* 17 | 	  TABLE ACCESS FULL	    | MLOG$_SALES |	2 |    74 |	3   (0)| 00:00:01 |
|* 18 |        INDEX UNIQUE SCAN	    | I_SNAP$_MV1 |	3 |    36 |	0   (0)|	  |
|  19 |      MERGE JOIN 		    |		  |    13 |   871 |	7  (29)| 00:00:01 |
|  20 |       MAT_VIEW ACCESS BY INDEX ROWID| MV1	  |	5 |    60 |	2   (0)| 00:00:01 |
|  21 |        INDEX FULL SCAN		    | I_SNAP$_MV1 |	5 |	  |	1   (0)| 00:00:01 |
|* 22 |       FILTER			    |		  |	  |	  |	       |	  |
|* 23 |        SORT JOIN		    |		  |   100 |  5500 |	5  (40)| 00:00:01 |
|  24 | 	VIEW			    |		  |   100 |  5500 |	4  (25)| 00:00:01 |
|  25 | 	 SORT GROUP BY		    |		  |	  |	  |	4  (25)| 00:00:01 |
|* 26 | 	  TABLE ACCESS FULL	    | MLOG$_SALES |	2 |    74 |	3   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------

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

   3 - filter("AV$0"."OJ_MARK" IS NULL)
   4 - access(SYS_OP_MAP_NONNULL("SNA$0"."CHANNEL_ID")=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
   9 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-01-05 14:32:07', 'syyyy-mm-dd hh24:mi:ss'))
  12 - filter(CASE  WHEN ROWID IS NOT NULL THEN 1 ELSE NULL END  IS NULL)
  15 - filter(SUM(DECODE(DECODE("MAS$"."OLD_NEW$$",'N','I','D'),'I',1,(-1)))>0)
  17 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-01-05 14:32:07', 'syyyy-mm-dd hh24:mi:ss'))
  18 - access("MV1"."SYS_NC00005$"=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
  22 - filter("MV1"."COUNT(*)"+"AV$0"."D0">0)
  23 - access("MV1"."SYS_NC00005$"=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
       filter("MV1"."SYS_NC00005$"=SYS_OP_MAP_NONNULL("AV$0"."GB0"))
  26 - filter("MAS$"."SNAPTIME$$">TO_DATE(' 2017-01-05 14:32:07', 'syyyy-mm-dd hh24:mi:ss'))

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


60 rows selected.

But the query delivers the correct result – so that is not simply using the STALE MV only like QUERY_REWRITE_INTEGRITY=STALE_TOLERATED does. Just to confirm:

SQL> show parameter query_rewrite_integrity

NAME				     TYPE	 VALUE
------------------------------------ ----------- ------------------------------
query_rewrite_integrity 	     string	 enforced

Still REFRESH should be done from time to time like here:

SQL> exec dbms_mview.refresh('MV1','F')

PL/SQL procedure successfully completed.

SQL> set timing on
SQL> select channel_id,sum(amount_sold) from sales group by channel_id;

CHANNEL_ID SUM(AMOUNT_SOLD)
---------- ----------------
	 1	    4000001
	 2	    4000000
	 4	    4000000
	 3	    4000000
	 0	    4000000

Elapsed: 00:00:00.06

Isn’t it nice that also the good old stuff gets enhanced instead of only the fancy new things like the In-Memory Option? At least I think so 🙂

Watch me on YouTube explaining and demonstrating the above:

, ,

6 Comments

How to reduce Buffer Busy Waits with Hash Partitioned Tables in #Oracle

fight_contention_2

Large OLTP sites may suffer from Buffer Busy Waits. Hash Partitioning is one way to reduce it on both, Indexes and Tables. My last post demonstrated that for Indexes, now let’s see how it looks like with Tables. Initially there is a normal table that is not yet hash partitioned. If many sessions do insert now simultaneously, the problem shows:

Contention with a heap table

Contention with a heap table

The last extent becomes a hot spot; all inserts go there and only a limited number of blocks is available. Therefore we will see Buffer Busy Waits. The playground:

SQL> create table t (id number, sometext varchar2(50));

Table created.

create sequence id_seq;

Sequence created.

create or replace procedure manyinserts as
begin
 for i in 1..10000 loop
  insert into t values (id_seq.nextval, 'DOES THIS CAUSE BUFFER BUSY WAITS?');
 end loop;
 commit;
end;
/

Procedure created.

create or replace procedure manysessions as
v_jobno number:=0;
begin
FOR i in 1..100 LOOP
 dbms_job.submit(v_jobno,'manyinserts;', sysdate);
END LOOP;
commit;
end;
/

Procedure created.

The procedure manysessions is the way how I simulate OLTP end user activity on my demo system. Calling it leads to 100 job sessions. Each does 10.000 inserts:

SQL> exec manysessions

PL/SQL procedure successfully completed.

SQL> select count(*) from t;

  COUNT(*)
----------
   1000000

SQL> select object_name,subobject_name,value from v$segment_statistics 
     where owner='ADAM' 
     and statistic_name='buffer busy waits'
     and object_name = 'T';

OBJECT_NAM SUBOBJECT_	   VALUE
---------- ---------- ----------
T			    2985

So we got thousands of Buffer Busy Waits that way. Now the remedy:

SQL> drop table t purge;

Table dropped.

SQL> create table t (id number, sometext varchar2(50))
     partition by hash (id) partitions 32;

Table created.

 
SQL> alter procedure manyinserts compile;

Procedure altered.

SQL> alter procedure manysessions compile;

Procedure altered.

SQL> exec manysessions 

PL/SQL procedure successfully completed.

SQL> select count(*) from t;

  COUNT(*)
----------
   1000000

SQL> select object_name,subobject_name,value from v$segment_statistics 
     where owner='ADAM' 
     and statistic_name='buffer busy waits'
     and object_name = 'T';  

OBJECT_NAM SUBOBJECT_	   VALUE
---------- ---------- ----------
T	   SYS_P249	       0
T	   SYS_P250	       1
T	   SYS_P251	       0
T	   SYS_P252	       0
T	   SYS_P253	       0
T	   SYS_P254	       0
T	   SYS_P255	       0
T	   SYS_P256	       1
T	   SYS_P257	       0
T	   SYS_P258	       0
T	   SYS_P259	       1
T	   SYS_P260	       0
T	   SYS_P261	       0
T	   SYS_P262	       0
T	   SYS_P263	       0
T	   SYS_P264	       1
T	   SYS_P265	       1
T	   SYS_P266	       0
T	   SYS_P267	       0
T	   SYS_P268	       0
T	   SYS_P269	       0
T	   SYS_P270	       0
T	   SYS_P271	       1
T	   SYS_P272	       0
T	   SYS_P273	       0
T	   SYS_P274	       0
T	   SYS_P275	       1
T	   SYS_P276	       0
T	   SYS_P277	       0
T	   SYS_P278	       0
T	   SYS_P279	       2
T	   SYS_P280	       0

32 rows selected.

SQL> select sum(value) from v$segment_statistics 
     where owner='ADAM' 
     and statistic_name='buffer busy waits'
     and object_name = 'T';

SUM(VALUE)
----------
	 9

SQL> select 2985-9 as waits_gone from dual;

WAITS_GONE
----------
      2976

The hot spot is gone:

hash_part_table

This emphasizes again that Partitioning is not only for the Data Warehouse. Hash Partitioning in particular can be used to fight contention in OLTP environments.

,

1 Comment

How to reduce Buffer Busy Waits with Hash Partitioned Indexes in #Oracle

fight_contention

Buffer Busy Waits can be a serious problem for large OLTP systems on both tables and indexes. If e.g. many inserts from multiple sessions occur simultaneously, they may have to compete about the same index leaf blocks like the picture below shows:

Index Leaf Block Contention

Index Leaf Block Contention

For the demo below, I’m using 100 jobs running at the same time to simulate 100 end user session that do inserts into table t with an ordinary index i that is not yet partitioned:

SQL> create table t (id number, sometext varchar2(50));

Table created.

SQL> create index i on t(id);

Index created.

SQL> create sequence id_seq;

Sequence created.

SQL> create or replace procedure manyinserts as
     begin
      for i in 1..10000 loop
       insert into t values (id_seq.nextval, 'DOES THIS CAUSE BUFFER BUSY WAITS?');
      end loop;
      commit;
     end;
     /  

Procedure created.

SQL> create or replace procedure manysessions as
     v_jobno number:=0;
     begin
      for i in 1..100 loop
       dbms_job.submit(v_jobno,'manyinserts;', sysdate);
      end loop;
      commit;
     end;
     /

Procedure created.

SQL> exec manysessions

PL/SQL procedure successfully completed.

After a couple of minutes the jobs are done and the table is populated:

SQL> select count(*) from t;

  COUNT(*)
----------
   1000000

SQL> select object_name,subobject_name,value 
     from v$segment_statistics where owner='ADAM' 
     and statistic_name='buffer busy waits'
     and object_name = 'I';

OBJECT_NAM SUBOBJECT_	   VALUE
---------- ---------- ----------
I			  167363

There have been Buffer Busy Waits on the table t as well of course, but let’s focus on the index here. Now the same load but with a Hash Partitioned index instead:

SQL> drop index i;

Index dropped.

SQL> truncate table t;

Table truncated.

SQL> create index i on t(id) global
     partition by hash(id) partitions 32;
 
Index created.

Notice that you have to say GLOBAL even though the table is not partitioned itself, so LOCAL is impossible. How about the effect?

SQL> exec manysessions

PL/SQL procedure successfully completed.

SQL> select count(*) from t;

  COUNT(*)
----------
   1000000

SQL> select object_name,subobject_name,value 
     from v$segment_statistics where owner='ADAM' 
     and statistic_name='buffer busy waits'
     and object_name = 'I';


OBJECT_NAM SUBOBJECT_	   VALUE
---------- ---------- ----------
I	   SYS_P249	     138
I	   SYS_P250	     122
I	   SYS_P251	     138
I	   SYS_P252	     120
I	   SYS_P253	     134
I	   SYS_P254	     116
I	   SYS_P255	     132
I	   SYS_P256	     129
I	   SYS_P257	     126
I	   SYS_P258	     140
I	   SYS_P259	     126
I	   SYS_P260	     129
I	   SYS_P261	     142
I	   SYS_P262	     142
I	   SYS_P263	     156
I	   SYS_P264	     155
I	   SYS_P265	     165
I	   SYS_P266	     121
I	   SYS_P267	     142
I	   SYS_P268	     148
I	   SYS_P269	     120
I	   SYS_P270	     112
I	   SYS_P271	     168
I	   SYS_P272	     130
I	   SYS_P273	     129
I	   SYS_P274	     137
I	   SYS_P275	     147
I	   SYS_P276	     131
I	   SYS_P277	     132
I	   SYS_P278	     136
I	   SYS_P279	     124
I	   SYS_P280	     138

32 rows selected.

Instead of having just one hot part, we now have as many ‘warm parts’ as there are partitions, like the picture below tries to show:

Reduced contention with hash partitioned index

Reduced contention with hash partitioned index

Precisely this was achieved by the solution:

SQL> select sum(value) from v$segment_statistics 
     where owner='ADAM' 
     and statistic_name='buffer busy waits'
     and object_name = 'I'; 

SUM(VALUE)
----------
      4325

SQL> select 167363-4325 as waits_gone from dual;

WAITS_GONE
----------
    163038

Give me an Oracle Database and I don’t need a calculator 🙂

Watch me explaining the above on YouTube:

,

4 Comments

%d bloggers like this: