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.
Can you see the first improvement?, since the first session didn’t commit it is holding an exclusive lock that prevent us to do any ddl in the older releases, but now at least we can add a new column, to modify or drop we still have to wait the lock to be released .
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)
---------- ----------
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
TRUNCATE TABLE
SELECT FOR UPDATE
ALTER TABLE ADD/MODIFY/RENAME/DROP COLUMN
ALTER TABLE SET COLUMN UNUSED
ALTER TABLE DROP/TRUNCATE/EXCHANGE (SUB)PARTITION
ALTER TABLE UPGRADE INCLUDING DATA or ALTER TYPE CASCADE INCLUDING TABLE DATA for a type with read-only table dependents
Online redefinition
FLASHBACK TABLE
Allowed operations in read-only mode:
SELECT
CREATE/ALTER/DROP INDEX
ALTER TABLE ADD/MODIFY/DROP/ENABLE/DISABLE CONSTRAINT
ALTER TABLE for physical property changes
ALTER TABLE DROP UNUSED COLUMNS
ALTER TABLE ADD/COALESCE/MERGE/MODIFY/MOVE/RENAME/SPLIT (SUB)PARTITION
ALTER TABLE MOVE
ALTER TABLE ENABLE ROW MOVEMENT and ALTER TABLE SHRINK
RENAME TABLE and ALTER TABLE RENAME TO
DROP TABLE
ALTER TABLE DEALLOCATE UNUSED
ALTER TABLE ADD/DROP SUPPLEMENTAL LOG
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
http://dbabrain.blogspot.com/