Oracle 11g New Features – Part 1


Oracle finally release the new Oracle 11g(11.1.0.6.0), so far we have only the Linux 32bits version available, and it comes with the promises of a much more stable first release, let’s see.


There are hundreds of new features, not sure yet which one will be really bringing benefits to the dba and also to the business, and the only way to discover is testing, so what I intend to do, is a series of articles covering all or almost all the new features.


Let’s start.


Case sensitive password


One of the first things I realized soon I started to test Oracle 11g is that finally the password is case sensitive, that was a big failure in terms of security in all the last releases, and after a few decades they realize that is important, well, better later than never.


E.G.:

SQL> conn / as sysdba

Connected.

SQL> create user sqlmag1 identified by SqlMag1;


User created.


SQL> grant dba to sqlmag1;


Grant succeeded.


SQL> conn sqlmag1/sqlmag1

ERROR:

ORA-01017: invalid username/password; logon denied



Warning: You are no longer connected to ORACLE.

SQL> conn sqlmag1/SQLMAG1

ERROR:

ORA-01017: invalid username/password; logon denied



SQL> conn sqlmag1/SqlMag1

Connected.

SQL> conn SQLMAG1/SqlMag1

Connected.


SQL> create user "sqlmag2" identified by sqlMAG1;


User created.


SQL> conn SQLMAG2/sqlMAG1

ERROR:

ORA-01017: invalid username/password; logon denied



Warning: You are no longer connected to ORACLE.

SQL> conn sqlmag2/sqlMAG1

ERROR:

ORA-01017: invalid username/password; logon denied



SQL> conn "sqlmag2"/sqlMAG1

ERROR:

ORA-01045: user sqlmag2 lacks CREATE SESSION privilege; logon denied



SQL> conn / as sysdba

Connected.

SQL> grant dba to sqlmag2;

grant dba to sqlmag2

*

ERROR at line 1:

ORA-01917: user or role 'SQLMAG2' does not exist



SQL> grant dba to "sqlmag2";


Grant succeeded.


SQL> conn "sqlmag2"/sqlMAG1

Connected.


SQL> select username from dba_users where lower (username) like '%sql%';


USERNAME

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

SQLMAG

sqlmag2

SQLMAG1


On the tests you can see that only the password is “naturally” case sensitive, to use a username case sensitive we need to create it using “ “, anyway I think this change was a good start.


Memory management


With Oracle 10g the new parameter SGA_TARGET was responsible to dynamically change the SGA components based on the utilization.


Now Oracle comes up with a new parameter MEMORY_TARGET and MEMORY_MAX_TARGET.


The MEMORY_TARGET parameter will not just dynamically change your SGA but also your PGA, replacing the need to set PGA_AGGREGATE_TARGET, and it will do that up to the value of MEMORY_MAX_TARGET.


Well, I’m not a fanatic by Automatic management, the main reason is because the applications in general always have some sort of problems like: lack of bind variable, queries bad written, etc., And I don’t believe the automatic management is able to handle that, for example, if you have an application that is not using bind variable for some high usage query, the automatic management is going to increase the size of your SHARED_POOL and decreasing other areas. Also if you have a query doing something wrong retrieving a cartesian product that was not supposed to exist, how is it going to handle that?


Sometimes the applications issues are simple to solve and you also can use the SQL tuning advisor as well, but I still trust in the DBA knowledge to handle those issues than in certain algorithms.


Adding a new column


Every time we need to add a column during business hours we may face some stress trying to do it, for example:


Session 1 – 10g:

SQL> create table users (id number, name varchar2(10));


Table created.


SQL> Insert into users values(1,'Rodrigo');


1 row created.


Session 2 – 10g:

SQL> alter table users add (phone number);

*

ERROR at line 1:

ORA-00054: resource busy and acquire with NOWAIT specified


Session 1 – 11g:

SQL> create table users (id number, name varchar2(10));


Table created.


SQL> Insert into users values(1,'Rodrigo');


1 row created.


Session 2 – 11g:

SQL> alter table users add (phone number);


Table altered.



DDL wait parameter


As we saw we just need to wait to modify and drop a column, and that is really frustrating when there are a lot of users doing DML on your table, so the solution for that is the parameter DDL_LOCK_TIMEOUT, when we use this parameter our commands wait the lock to be released and then execute it, that way we don’t need to be trying thousands of time, example:


Session 1 – 11g:

SQL> update users

2 set phone=11111111

3 where id=1;


1 row updated.


Session 2 – 11g:

SQL> alter table users modify (name varchar2(60));

alter table users modify (name varchar2(60))

*

ERROR at line 1:

ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired


We got the error now we try:


SQL> Alter session set ddl_lock_timeout=10;


Session altered.


SQL> alter table users modify (name varchar2(60));

Session hanging ….


Session 1 – 11g:

SQL> commit;


Commit complete. –lock released


Session 2 – 11g:

SQL> alter table users modify (name varchar2(60));


Table altered.


With this parameter our command hangs till the lock is released and then executes it.

If you want you can use the ALTER SYSTEM, and use a wait time for the whole instance.


New columns with default values – not null


In previous versions when we added a column with default value and not null, Oracle will add the column and update all the rows in the table with the default value, imagine doing that in a table with 50 million rows, well will take a long time and also will fill up your Undo tablespace.


Now Oracle 11g is more “intelligent” doing that, when we add a new column with default value it will not update the table for the existing records, just the new ones will have the value inserted, then when we select the data it will get the default value from the dictionary and show to us, very clever I would say.


Let’s do an example:


SQL> create table magtest(id number, name varchar2(10));


Table created.


SQL> insert into magtest values(1,'Rodrigo');


1 row created.


SQL> insert into magtest values(2,'Righetti');


1 row created.


SQL> commit;


Commit complete.


SQL> alter table magtest add country varchar2(10) default 'BRA' not null;


Table altered.


SQL> insert into magtest(id,name) values (3,'Oracle');


1 row created.


SQL> commit;


Commit complete.


SQL> select * from magtest;


ID NAME COUNTRY

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

1 Rodrigo BRA

2 Righetti BRA

3 Oracle BRA


You can see that all the rows are with the default value, but only the 3rd rows had it value inserted, now we are going to confirm that with a block dump:


Let’s check the block_id:


SQL> Select dbms_rowid.rowid_block_number(rowid) block, COUNT(1)

from magtest

GROUP BY dbms_rowid.rowid_block_number(rowid) ;


BLOCK COUNT(1)

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

  1. 3


The File_id:

SQL> select file_id from dba_EXTENTS where segment_name='MAGTEST';


FILE_ID

----------

4


SQL> ALTER SYSTEM DUMP DATAFILE 4 BLOCK MIN 1188 BLOCK MAX 1188;


System altered.


tl: 14 fb: --H-FL-- lb: 0x1 cc: 2

col 0: [ 2] c1 02

col 1: [ 7] 52 6f 64 72 69 67 6f

tab 0, row 1, @0x1f7b

tl: 15 fb: --H-FL-- lb: 0x1 cc: 2

col 0: [ 2] c1 03

col 1: [ 8] 52 69 67 68 65 74 74 69

tab 0, row 2, @0x1f6a

tl: 17 fb: --H-FL-- lb: 0x2 cc: 3

col 0: [ 2] c1 04

col 1: [ 6] 4f 72 61 63 6c 65

col 2: [ 3] 42 52 41



As you can see only the last row has 3 columns stored, all the other 2 don’t, and that in my opinion is a great improvement in terms of table management.


Virtual Columns


There are cases we need to have a column that change dynamically based on updates in other columns, normally we would use a trigger for that, but now we can use the virtual column.


For example:


We need to have a column in our stock table that will separate our products based in the amount we have in stock.


create table stock

( id number,

product varchar2(30),

quantity number,

status varchar2(10)

generated always as

( case

when quantity <= 10 then 'LOW'

when quantity > 11 and quantity <= 100 then 'MEDIUM'

when quantity > 101 and quantity <= 1000 then 'HIGH'

else 'OVERMUCH'

end ) virtual

);



insert into STOCK (ID, PRODUCT, QUANTITY) values (1,'TOY1',5);

insert into STOCK (ID, PRODUCT, QUANTITY) values (2,'TOY2',25);

insert into STOCK (ID, PRODUCT, QUANTITY) values (3,'TOY3',150);

insert into STOCK (ID, PRODUCT, QUANTITY) values (4,'TOY4',1200);


COMMIT;


SQL> SELECT * FROM STOCK;


ID PRODUCT QUANTITY STATUS

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

1 TOY1 5 LOW

2 TOY2 25 MEDIUM

3 TOY3 150 HIGH

4 TOY4 1200 OVERMUCH


SQL> UPDATE STOCK

2 SET QUANTITY = 50

3 WHERE ID=4;


1 row updated.


SQL> SELECT * FROM STOCK;


ID PRODUCT QUANTITY STATUS

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

1 TOY1 5 LOW

2 TOY2 25 MEDIUM

3 TOY3 150 HIGH

4 TOY4 50 MEDIUM



For me seems the Virtual column, works quite well, and with some performance tests I’ve done I didn’t see any overhead to return this column, I would say this is a really good new feature.


Also you can add this column with the alter table statement:


alter table stock add (status varchar2(10)

generated always as

( case

when quantity <= 10000 then 'LOW'

when quantity > 10000 and quantity <= 100000 then 'MEDIUM'

when quantity > 100000 and quantity <= 1000000 then 'HIGH'

else 'OVERMUCH'

end ) virtual

);



Another good point is that we can create indexes on it, also we can partition based on the virtual column, what we can’t do is insert or update this column.


SQL> create index stock_status on stock(status);


Index created.


Elapsed: 00:00:00.09

SQL> set autotrace on

SQL> select * from stock where status='HIGH';


ID PRODUCT QUANTITY STATUS

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

3 TOY3 150 HIGH


Elapsed: 00:00:00.00


Execution Plan

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

Plan hash value: 3923203641


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

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

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

| 0 | SELECT STATEMENT | | 1 | 50 | 2 (0)| 00:00:01 |

| 1 | TABLE ACCESS BY INDEX ROWID| STOCK | 1 | 50 | 2 (0)| 00:00:01 |

|* 2 | INDEX RANGE SCAN | STOCK_STATUS | 1 | | 1 (0)| 00:00:01 |



The index type is:


SQL> select index_type from user_indexes where index_name = 'STOCK_STATUS';


INDEX_TYPE

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

FUNCTION-BASED NORMAL


SQL> select column_expression from user_ind_expressions where index_name = 'STOCK_STATUS';


COLUMN_EXPRESSION

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

CASE WHEN "QUANTITY"<=10 THEN 'LOW' WHEN ("QUANTITY">11 AND "QUANTITY"<=100) TH

EN 'MEDIUM' WHEN ("QUANTITY">101 AND "QUANTITY"<=1000) THEN 'HIGH' ELSE 'OVERMUC

H' END


Invisible Indexes


Every new release Oracle brings new features to the optimizer, and invisible index is one of them.


What we can do is hide an index from the optimizer, and it will only be used if specified by a hint or an alter session command, this feature can be very helpful and also can be very painful if you forget you have made an index invisible.


Example:


SQL> create table obj as select * from dba_objects;


Table created.


SQL> create index obj_object_id_idx on obj(object_id);


Index created.


SQL> analyze table obj compute statistics;


Table analyzed.


SQL> set autotrace trace explain

SQL> select object_name from obj where object_id=57896;

Elapsed: 00:00:00.01


Execution Plan

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

Plan hash value: 3628079472


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

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

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

| 0 | SELECT STATEMENT | | 1 | 28 | 2 (0)| 00:00:01 |

| 1 | TABLE ACCESS BY INDEX ROWID| OBJ | 1 | 28 | 2 (0)| 00:00:01 |

|* 2 | INDEX RANGE SCAN | OBJ_OBJECT_ID_IDX | 1 | | 1 (0)| 00:00:01 |


SQL> alter index OBJ_OBJECT_ID_IDX invisible;


Index altered.


SQL> select object_name from obj where object_id=57896;

Elapsed: 00:00:00.01


Execution Plan

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

Plan hash value: 730912574


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

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

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

| 0 | SELECT STATEMENT | | 1 | 28 | 282 (1)| 00:00:04 |

|* 1 | TABLE ACCESS FULL| OBJ | 1 | 28 | 282 (1)| 00:00:04 |


SQL> alter session set optimizer_use_invisible_indexes =true;


Session altered.


Elapsed: 00:00:00.00

SQL> select object_name from obj where object_id=57896;

Elapsed: 00:00:00.01


Execution Plan

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

Plan hash value: 3628079472


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

| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |

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

| 0 | SELECT STATEMENT | | 1 | 28 | 2 (0)| 00:00:01 |

| 1 | TABLE ACCESS BY INDEX ROWID| OBJ | 1 | 28 | 2 (0)| 00:00:01 |

|* 2 | INDEX RANGE SCAN | OBJ_OBJECT_ID_IDX | 1 | | 1 (0)| 00:00:01 |

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


SQL> select visibility from user_indexes where index_name = 'OBJ_OBJECT_ID_IDX';


VISIBILIT

---------

INVISIBLE


You can also use the invisible index to make a safer clean up, what I mean is if you have an over indexed table you can monitor them and also you can hide them before you really drop it, since the invisible index it is kept up to date, all dml will take effect on the index, if you have hidden a good index you just need to make it visible again, without the pain of recreate it.


Read only tables


Now we have one more feature to control access on a table, not only for security purposes but also for maintenance reasons.


When we put a table in read only mode, all commands that intend to change the data will not work:


Example:


SQL> alter table obj read only;


Table altered.


SQL> delete from obj;

delete from obj

*

ERROR at line 1:

ORA-12081: update operation not allowed on table "SQLMAG"."OBJ"


SQL> truncate table obj;

truncate table obj

*

ERROR at line 1:

ORA-12081: update operation not allowed on table "SQLMAG"."OBJ"


SQL> create index obj_name_idx on obj(object_name);


Index created.


SQL> alter table obj move tablespace users;


Table altered.


As you can see there are some ddl you can perform and others you can not, as mentioned above, just the ones that will not change the data or the table structure can be executed, below is the list of commands that you can and can not execute:

Not allowed operations in read-only mode:

All DML operations on the table or any of its partitions

Allowed operations in read-only mode:

Sequence in PL/SQL expressions


Normally when we want to get a value from a sequence using PL/SQL we use:


Select sequence_name.nextval into var1 from table;


Now we can use it direct on the pl/sql variable:


Var1 := sequence_name.nextval;


Example:

SQL> create sequence obj_seq

2 start with 1

3 increment by 1;


Sequence created.


SQL> set serverout on


declare

var1 number;

begin

for i in 1..10 loop

var1:= obj_seq.nextval;

dbms_output.put_line(var1);

end loop;

end;

SQL> /

1

2

3

4

5

6

7

8

9

10


Enhancements to the Regular Expressions and new function


Regular expressions were created on Oracle 10g and now they come with some improvements and with a new function, they are a very powerful tool to work with these data types:

CHAR, NCHAR, CLOB, NCLOB, NVARCHAR2, and VARCHAR2


Regular expressions are very common in Unix tools to work with character data types, they can be used to find, replace, look for white spaces, etc. any kind of character manipulation, and this kind of operation was brought to the database side.


A full article can be written about it, but let’s check at least a few examples:


The functions are:

REGEXP_LIKE , REGEXP_INSTR, REGEXP_SUBSTR, REGEXP_REPLACE and REGEXP_COUNT (new on 11g).


To understand it better you can relate them with the common functions LIKE, INSTR, SUBSTR and REPLACE, the difference is the regular expressions are much more powerful.


To use them we need to know the metacharacters, for example:

(.) – Used to find any character less a new line – e.g.: r.d – that means I want to find any pattern that has the letter r (any letter) o, like: rod, Rodrigo, production

(^) – Used to point the beginning of a line

($) – Used to point the end of a line

e.g.: ^r.d$ - it is going to look for - rod

(*) – Multiple characters similar to % using the function LIKE


The regular expressions also work with POSIX(Portable Operating System Interface), that means you can look for any kind of “type” of character like lower cases.


E.G.:

[[:lower:]]{8} - This example will look for 8 consecutives lower case characters.


There is a huge list of metacharacters to be used, all of them are available at the Oracle documentation, so just showing a few examples now:


Examples:


REGEXP_LIKE


create table toys

(id number,

descr varchar2(20));


insert into toys values (1,'TOY1');

insert into toys values (2,'T1OY2');

insert into toys values (3,'1TOY1');

insert into toys values (4,'999');

insert into toys values (5,'888');

insert into toys values (6,'TOY2007');


COMMIT;


SELECT DESCR

FROM TOYS

WHERE REGEXP_LIKE(DESCR, '[^[:digit:]]');


DESCR

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

TOY1

T1OY2

1TOY1

TOY2007


This example is looking for alphanumeric strings.


REGEXP_INSTR


SQL> SELECT REGEXP_INSTR('Rodrigo Righetti, JD. Aurelia - Campinas - SP 13033030', '[[:digit:]]{8}$') as zip_code_position

FROM dual 2 3 4 ;


ZIP_CODE_POSITION

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

47


You can see this regular expressions result the same as INSTR, but you can ask for any kind of position, I’ve asked the position of the 8 consecutive digits at the end of the line.


SQL> SELECT REGEXP_INSTR('Rodrigo Righetti, JD. Aurelia - Campinas - SP 13033-030','[[:digit:]]{5}(-[[:digit:]]{3})?$') as zip_code_position

FROM dual

2 3 ;


ZIP_CODE_POSITION

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

47


One this one I’ve asked for 2 pattern at the end of the line.


REGEXP_SUBSTR


SQL> SELECT REGEXP_SUBSTR('first name , middle name , last name',', [^,]*,')

FROM dual;


REGEXP_SUBSTR('

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

, middle name ,


Here we want the piece between “space , any character ,”.


REGEXP_REPLACE


SQL> SELECT REGEXP_REPLACE('Rodrigo Righetti','( ){5,}', ' ')

FROM dual;


REGEXP_REPLACE('

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

Rodrigo Righetti


Here we are replacing 5 empty spaces by one.


REGEXP_COUNT – new on 11g


SQL> select REGEXP_COUNT('Rodrigo Righetti', 'i', 2, 'c')

from dual; 2


REGEXP_COUNT('RODRIGORIGHETTI','I',2,'C')

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

3


The expression REGEXP_COUNT will count the number of times the defined pattern appears, in this example, I’m looking for the letter “I”, starting to search from the 2nd letter and case sensitive only.


Besides the new function you now can use the REGEXP_SUBSTR and REGEXP_INSTR to look for expressions between tags like <tag1> string </tag1>, that is an enhancement on 11g.


But as I’ve said this is a rich subject in content so I suggest a deep study in the Oracle documentation to take more of this feature.

Compressed tables

Oracle 11g brings the promise of seamless compressed tables for OLTP environment, that means DML (insert, update, delete) should work properly with a very small overhead using compressed tables. And in theory it will provide a fast read operation.

To compress a table we use the COMPRESS clause, on a CREATE TABLE or ALTER TABLE command, if we use it on an ALTER TABLE, all the data inserted before will remain uncompressed, just the new insert will be compressed, the same is going to happen if you use ALTER TABLE … NOCOMPRESS, then all the compressed data will remain compressed and the new ones will be uncompressed.

Using just the clause COMPRESS or COMPRESS FOR DIRECT_LOAD OPERATIONS we are enabling compression just for direct path load operations, to enable it for all operations we need to use COMPRESS FOR ALL OPERATIONS.

Let’s test it:

SQL> create table obj_nocompress

2 as

3 select * from dba_objects where 1=2;


Table created.


SQL> create table obj_compress compress for all operations

2 as

3 select * from obj where 1=2;


Table created.

NO COMPRESSED

SQL> set timing on

begin

for i in 1..100 loop

insert into obj_nocompress

select * from dba_objects;

end loop;

commit;

end;

/SQL> 2 3 4 5 6 7 8


PL/SQL procedure successfully completed.


Elapsed: 00:01:32.56



COMPRESSED – loaded with normal insert

SQL> begin

for i in 1..100 loop

insert into obj_compress

select * from dba_objects;

end loop;

commit;

end;

/ 2 3 4 5 6 7 8


PL/SQL procedure successfully completed.


Elapsed: 00:04:29.38


SQL> select segment_name, bytes

2 from dba_segments

3 where segment_name in ('OBJ_COMPRESS','OBJ_NOCOMPRESS');



SEGMENT_NAME BYTES

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

OBJ_NOCOMPRESS 828375040

OBJ_COMPRESS 394264576


As you can see it has compressed more than 50% for the normal insert operation, now look the difference for a direct path insert:


Also this table I will allow compression only for direct-path.


SQL> create table obj1_compress compress

2 as

3 select * from obj where 1=2;


SQL> begin

for i in 1..100 loop

insert /*+ append */ into obj1_compress

select * from dba_objects;

commit;

end loop;

end;

/


PL/SQL procedure successfully completed.


Elapsed: 00:01:19.90


SQL> SELECT SEGMENT_NAME, BYTES FROM DBA_SEGMENTS

2 WHERE SEGMENT_NAME IN ('OBJ_NOCOMPRESS','OBJ_COMPRESS','OBJ1_COMPRESS');


SEGMENT_NAME BYTES

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

OBJ1_COMPRESS 258998272

OBJ_COMPRESS 394264576

OBJ_NOCOMPRESS 828375040


Now our compression is almost 70%, but we can see the compression for normal DML operation is still pretty good.


Let’s check the performance:


SQL> select count(1) from obj_nocompress;


COUNT(1)

----------

6834100


Elapsed: 00:00:07.27 – 1st execution

Elapsed: 00:00:02.05 – 2nd execution


SQL> select count(1) from obj_compress;


COUNT(1)

----------

6834100


Elapsed: 00:00:05.64 – 1st execution

Elapsed: 00:00:01.34 – 2nd execution


SQL> select count(1) from obj1_compress;


COUNT(1)

----------

6834100


Elapsed: 00:00:03.31 – 1st execution

Elapsed: 00:00:00.82 – 2nd execution


This basic test shows us that the compression doesn’t affect the read performance, instead that it improves it since the table is pretty small.


MASSIVE UPDATE TEST:


NO COMPRESS

SQL> update obj_nocompress

2 set created=sysdate;


6834100 rows updated.


Elapsed: 00:03:06.55 – 1st execution

Elapsed: 00:02:33.68 – 2nd execution



Undo Blocks Undo Records

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

100276 7235904


redo entries - 7338913

redo size - 2104790944 bytes

Table Size - 828375040 bytes


COMPRESS FOR ALL OPERATIONS


SQL> set timing on

SQL> update obj_compress

2 set created=sysdate;


Undo Blocks Undo Records

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

11757 946603



redo entries - 1013077

redo size - 325390708 bytes

OBJ_COMPRESS – 394264576 bytes


After 2 hours waiting and with less than 15% of the process completed I decided to stop. As we can see this is not a feasible operation.


COMPRESS FOR DIRECT PATH


SQL> update obj1_compress

2 set created=sysdate;

6834100 rows updated.


Elapsed: 00:21:20.56 – 1st execution

Elapsed: 00:03:36.36 – 2nd execution


Undo Blocks Undo Records

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

279256 24289425 – 1st execution

146331 12221336 - 2nd execution


redo entries - 26207080 – 1st execution

redo size - 7072436608 bytes – 1st execution

redo entries - 12342254 2nd execution

redo size - 3195432460 bytes - 2nd execution


OBJ1_COMPRESS – 988807168 bytes


We have 2 situations here, compressed for all operations and compressed with direct-path.


The data compressed for all operations(OBJ_COMPRESS table), keeps the data compressed, but in the other hand an update like that is not so feasible, it will consume too much resource in terms of IO and CPU, also the time of this operations is crazy.


Now the compressed table just for direct-path is no more compressed, actually is bigger than the normal one, it has generated 3.5 times redo size, redo entries and undo. And about the execution time 7 times bigger, that means we really need to use this table just for direct loads, otherwise it will lose its meaning, as you can see on the second execution the figures are closer to the normal table.


Updating row by row using rowid - 1000 rows test:


NO COMPRESS


SQL> begin

for i in (select rowid from obj_nocompress where rownum < 10001) loop

update obj_nocompress

set created = sysdate

where rowid=i.rowid;

commit;

end loop;

end;

/ 2 3 4 5 6 7 8 9


PL/SQL procedure successfully completed.


Elapsed: 00:00:02.35


COMPRESS FOR ALL OPERATIONS


SQL> begin

for i in (select rowid from obj_compress where rownum < 10001) loop

update obj_compress

set created = sysdate

where rowid=i.rowid;

commit;

end loop;

end;

/ 2 3 4 5 6 7 8 9


PL/SQL procedure successfully completed.


Elapsed: 00:00:04.91


COMPRESS FOR DIRECT PATH


SQL> begin

for i in (select rowid from obj1_compress where rownum < 10001) loop

update obj1_compress

set created = sysdate

where rowid=i.rowid;

commit;

end loop;

end;

/ 2 3 4 5 6 7 8 9


PL/SQL procedure successfully completed.


Elapsed: 00:00:05.04


We can see here that even doing an update row by row, there is a good difference in terms of performance, but when executing few updates in different transactions, I mean 1 or 2 rows, like a normal oltp application, the difference on the performance was insignificant.


For sure more tests are necessary to take a better conclusion, but so far it seems to me, that we really should consider using it for DW environments, and for OLTP only for big tables that will suffer more inserts than updates, we saw the a massive update is terrible, so if you have a compressed table that will need to be completely updated I recommend create a copy uncompressed, execute the update and them compress it again.


Now just for curiosity let’s try to understand how the compression works,

First we are going to create 2 tables with compression and without, and insert 3 rows on them, to compare the dump:


Just a tip, before going forward on this test read my other article about dumping a datafile, for sure it will help to understand.


SQL> create table test_dump

(id number,

name varchar2(10),

birthdate date);


SQL> create table test_dump_comp

(id number,

name varchar2(10),

birthdate date)

compress for all operations;


insert into test_dump

values(1,'RODRIGO',to_date('29/06/1978 10:30:00','dd/mm/yyyy hh24:mi:ss'));

insert into test_dump

values(2,'LETICIA',to_date('04/02/1997 21:30:00','dd/mm/yyyy hh24:mi:ss'));

insert into test_dump

values(3,'RENATA',to_date('22/05/2000 22:00:00','dd/mm/yyyy hh24:mi:ss'));


insert into test_dump_comp

values(1,'RODRIGO',to_date('29/06/1978 10:30:00','dd/mm/yyyy hh24:mi:ss'));

insert into test_dump_comp

values(2,'LETICIA',to_date('04/02/1997 21:30:00','dd/mm/yyyy hh24:mi:ss'));

insert into test_dump_comp

values(3,'RENATA',to_date('22/05/2000 22:00:00','dd/mm/yyyy hh24:mi:ss'));

commit;



Both tables with the same values, now we are going to dump the block using the unix tool dd and od, the below sql is creating the commands to dump the block:


TEST_DUMP


select distinct 'dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip='||dbms_rowid.rowid_block_number(rowid)||' count=1 of=test_dump_'||dbms_rowid.rowid_block_number(rowid) as commands

from test_dump

union all

select distinct 'od -x test_dump_'|| dbms_rowid.rowid_block_number(rowid) ||' > test_dump_'|| dbms_rowid.rowid_block_number(rowid)||'.txt' as commands

from test_dump;


COMMANDS

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

dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip=196703 count=1 of=test_dump_196703

od -x test_dump_196703 > test_dump_196703.txt


TEST_DUMP_COMP


select distinct 'dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip='||dbms_rowid.rowid_block_number(rowid)||' count=1 of=test_dump_comp_'||dbms_rowid.rowid_block_number(rowid) as commands

from test_dump_comp

union all

select distinct 'od -x test_dump_comp_'|| dbms_rowid.rowid_block_number(rowid) ||' > test_dump_comp_'|| dbms_rowid.rowid_block_number(rowid)||'.txt' as commands

from test_dump_comp;


COMMANDS

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

dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip=196711 count=1 of=test_dump_comp_196711

od -x test_dump_comp_196711 > test_dump_comp_196711.txt


The result of this dump:


TEST_DUMP

0017660 0000 0000 0d4a 010a 0000 2c01 0301 c102

0017700 0604 4552 414e 4154 7807 0564 1716 0101

0017720 012c 0203 03c1 4c07 5445 4349 4149 7707

0017740 02c5 1604 011f 012c 0203 02c1 5207 444f

0017760 4952 4f47 7707 06b2 0b1d 011f 0605 f0c5


TEST_DUMP_COMP

0017660 0000 0000 06c3 010a 0000 2c01 0301 c102

0017700 0604 4552 414e 4154 7807 0564 1716 0101

0017720 012c 0203 03c1 4c07 5445 4349 4149 7707

0017740 02c5 1604 011f 012c 0203 02c1 5207 444f

0017760 4952 4f47 7707 06b2 0b1d 011f 0605 f0c5


As you can see the dumps are exact the same, that means no compression happened here, let’s see another test:


SQL> truncate table test_dump;


SQL> truncate table test_dump_comp;


SQL> begin

for i in 1..3000 loop

insert into test_dump

values(1,'RODRIGO',to_date('29/06/1978 10:30:00','dd/mm/yyyy hh24:mi:ss'));

end loop;

commit;

end;

/ 2 3 4 5 6 7 8


PL/SQL procedure successfully completed.


SQL> select dbms_rowid.rowid_block_number(rowid) block, COUNT(1) Rows_per_block

from TEST_dump

GROUP BY dbms_rowid.rowid_block_number(rowid) ;

BLOCK Rows_per_block

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

196701 302

196702 92

196704 302

196703 302

196700 302


SQL> begin

for i in 1..1300 loop

insert into test_dump_comp

values(1,'RODRIGO',to_date('29/06/1978 10:30:00','dd/mm/yyyy hh24:mi:ss'));

end loop;

commit;

end;

/ 2 3 4 5 6 7 8


PL/SQL procedure successfully completed.


SQL> select dbms_rowid.rowid_block_number(rowid) block, COUNT(1) Rows_per_block

2 from TEST_dump_comp

3 GROUP BY dbms_rowid.rowid_block_number(rowid) ;


BLOCK Rows_per_block

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

196711 620

196708 60

196712 620


On this one we must have some sort of compression, because for the same number of rows I’m using 3 blocks instead of 6, with the compression I’ve doubled the number of rows per block.


Now let’s check the dump of the 1st block :


TEST_DUMP


select distinct 'dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip='||dbms_rowid.rowid_block_number(rowid)||' count=1 of=test_dump_'||dbms_rowid.rowid_block_number(rowid) as commands

from test_dump

where rownum < 2

union all

select distinct 'od -x test_dump_'|| dbms_rowid.rowid_block_number(rowid) ||' > test_dump_'|| dbms_rowid.rowid_block_number(rowid)||'.txt' as commands

from test_dump

where rownum < 2;


COMMANDS

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

dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip=196700 count=1 of=test_dump_196700

od -x test_dump_196700 > test_dump_196700.txt



TEST_DUMP_COMP

select distinct 'dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip='||dbms_rowid.rowid_block_number(rowid)||' count=1 of=test_dump_comp_'||dbms_rowid.rowid_block_number(rowid) as commands

from test_dump_comp

where rownum < 2

union all

select distinct 'od -x test_dump_comp_'|| dbms_rowid.rowid_block_number(rowid) ||' > test_dump_comp_'|| dbms_rowid.rowid_block_number(rowid)||'.txt' as commands

from test_dump_comp

where rownum < 2;


COMMANDS

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

dd if=/oracle/oradata/test/users01.dbf ibs=8192 skip=196708 count=1 of=test_dump_comp_196708

od -x test_dump_comp_196708 > test_dump_comp_196708.txt



Just the bottom of the DUMPS:

TEST_DUMP


0b1d 011f 012c 0203 02c1 5207 444f 4952

4f47 7707 06b2 0b1d 011f 012c 0203 02c1

5207 444f 4952 4f47 7707 06b2 0b1d 011f

012c 0203 02c1 5207 444f 4952 4f47 7707

06b2 0b1d 011f 012c 0203 02c1 5207 444f

4952 4f47 7707 06b2 0b1d 011f 0601 f374



TEST_DUMP_COMP


0003 012c 0301 2c00 0101 0003 012c 0301

2c00 0101 0003 012c 0301 2c00 0101 0003

012c 0301 2c00 0101 0003 012c 0301 2c00

0101 0003 012c 0301 2c00 0101 0003 012c

0301 2c00 0101 0003 012c 0301 2c00 0101

0003 012c 0301 2c00 0101 0003 012c 0301

2c00 0101 0003 5002 ca03 02c1 52cf 444f

4952 4f47 77cf 06b2 0b1d 011f 0601 f38f


I know that is not so simple to understand that, so that is why I’ve suggested you to read the dump datafiles article, anyway, what Oracle is doing is creating a sort of mapping for rows that use the same values, so it doesn’t need to store it again, for sure there are more behind of that, thinking on the table block structure.


That is it for this article, on the next one I will talk about Flashback, Rman and some other features that I bump into on the way.


Rodrigo Righetti

Rodrigo.righetti@gmail.com

Dba_9iocp@hotmail.com

http://dbabrain.blogspot.com/