Monday, February 23, 2009

Remote dependencies

When changing plsql code in a production system the dependencies between objects can cause some programs to be invalidated. Making sure all programs are valid before opening the system to users is very important for application availability. Oracle automatically compiles invalid programs on their first execution, but a successfull compilation may not be possible because the error may need code correction or the compilation may cause library cache locks when applications are running.

Dependencies between plsql programs residing in the same database are easy to handle. When you compile a program the programs dependent on that one are invalidated right away and we can see the invalid programs and compile or correct them before opening the system to the users.

If you have db links and if your plsql programs are dependent on programs residing in other databases you have a problem handling the invalidations. There is an initialization parameter named remote_dependencies_mode that handles this dependency management. If it is set to TIMESTAMP the timestamp of the local program is compared to that of the remote program. If the remote program's timestamp is more recent than the local program, the local program is invalidated in the first run. If the parameter is set to SIGNATURE the remote program's signature (number and types of parameters, subprogram names, etc...) is checked, if there has been a change the local program is invalidated in the first run.

The problem here is; if you change the remote program's signature or timestamp you cannot see that the local program is invalidated because it will wait for the first execution to be invalidated. If you open the system to the users before correcting this you may face a serious application problem leading to downtime.

Here is a simple test case to see the problem.




I start by creating a plsql package in the database TEST.

SQL> create package test_pack as
2 procedure test_proc(a number);
3 end;
4 /

Package created.

SQL> create package body test_pack as
2 procedure test_proc(a number) is
3 begin
4 null;
5 end;
6 end;
7 /

Package body created.


On another database let's create a database link pointing to the remote database TEST, create a synonym for the remote package and create a local package calling the remote one.

SQL> create database link test.world connect to yas identified by yas using 'TEST';

Database link created.

SQL> select * from dual@yas.world;

D
-
X

SQL> create synonym test_pack for test_pack@yasbs.world;

Synonym created.

SQL> create package local_pack as
2 procedure local_test(a number);
3 end;
4 /

Package created.

SQL> r
1 create or replace package body local_pack as
2 procedure local_test(a number) is
3 begin
4 test_pack.test_proc(1);
5 end;
6* end;

Package body created.


If we look at the status of this local package we see that it is valid.


SQL> col object_name format a30
SQL> r
1* select status,object_name,object_type from user_objects where object_name='LOCAL_PACK'

STATUS OBJECT_NAME OBJECT_TYPE
------- ------------------------------ ------------------
VALID LOCAL_PACK PACKAGE
VALID LOCAL_PACK PACKAGE BODY


We can execute it without any problems.


SQL> exec LOCAL_PACK.local_test(1);

PL/SQL procedure successfully completed.


Now, let's change the remote package code including the package specification.


SQL> create or replace package test_pack as
2 procedure test_proc(b number,a number default 1);
3 end;
4 /

Package created.

SQL> create or replace package body test_pack as
2 procedure test_proc(b number,a number default 1) is
3 begin
4 null;
5 end;
6 end;
7 /

Package body created.


When we look at the local package we see its status as valid.


SQL> r
1* select status,object_name,object_type from user_objects where object_name='LOCAL_PACK'

STATUS OBJECT_NAME OBJECT_TYPE
------- ------------------------------ ------------------
VALID LOCAL_PACK PACKAGE
VALID LOCAL_PACK PACKAGE BODY


But when it is executed we get an error.


SQL> exec LOCAL_PACK.local_test(1);
BEGIN LOCAL_PACK.local_test(1); END;

*
ERROR at line 1:
ORA-04068: existing state of packages has been discarded
ORA-04062: signature of package "YASBS.TEST_PACK" has been changed
ORA-06512: at "YASBS.LOCAL_PACK", line 4
ORA-06512: at line 1


SQL> select status,object_name,object_type from user_objects where object_name='LOCAL_PACK';

STATUS OBJECT_NAME OBJECT_TYPE
------- ------------------------------ ------------------
VALID LOCAL_PACK PACKAGE
INVALID LOCAL_PACK PACKAGE BODY

SQL> exec LOCAL_PACK.local_test(1);

PL/SQL procedure successfully completed.

SQL> select status,object_name,object_type from user_objects where object_name='LOCAL_PACK';

STATUS OBJECT_NAME OBJECT_TYPE
------- ------------------------------ ------------------
VALID LOCAL_PACK PACKAGE
VALID LOCAL_PACK PACKAGE BODY


The second execution compiles the package and returns the status to valid.

How can we know beforehand that the local program will be invalidated on the first execution? The only way I can think of is to check the dependencies across all databases involved. By collecting all rows from dba_dependencies from all databases we can see that when the remote program is changed the programs on other databases that use this remote program will be invalidated when they are executed. Then we can compile these programs and see if they compile without errors.

Database links may be very annoying sometimes, this case is just one of them.

Wednesday, August 06, 2008

Index block split bug in 9i

In his famous index internals presentation Richard Foote mentions a bug in 9i about index block splits when rows are inserted in the order of the index columns. Depending on when you commit your inserts the index size changes dramatically.

While I was trying to find out why a 3-column primary key index takes more space than its table I recalled that bug and it turned out that was the reason of the space issue. The related bug is 3196414 and it is fixed in 10G.

Here is the test case Richard presents in his paper.


SQL> create table t(id number,value varchar2(10));

Table created.

SQL> create index t_ind on t(id);

Index created.

SQL> @mystat split

NAME VALUE
------------------------------ ----------
leaf node splits 0
leaf node 90-10 splits 0
branch node splits 0

SQL> ed
Wrote file afiedt.buf

1 begin
2 for i in 1..10000 loop
3 insert into t values(i,'test');
4 commit;
5 end loop;
6* end;
SQL> r
1 begin
2 for i in 1..10000 loop
3 insert into t values(i,'test');
4 commit;
5 end loop;
6* end;

PL/SQL procedure successfully completed.

SQL> @mystat2 split

NAME VALUE DIFF
------------------------------ ---------- ----------
leaf node splits 35 35
leaf node 90-10 splits 0 0
branch node splits 0 0

SQL> analyze index t_ind validate structure;

Index analyzed.

SQL> select lf_blks, pct_used from index_stats;

LF_BLKS PCT_USED
---------- ----------
36 51

SQL> drop table t;

Table dropped.



I am trying to insert the rows in the order of the primary key column, so what I expect to see is that when an index block fills there will be a 90-10 split and the index will grow in size. But as the number of leaf block splits show there are 35 block splits and none of them are 90-10 splits meaning all are 50-50 block splits. I have 36 leaf blocks but half of each one is empty.

If we try the same inserts but commit after the loop the result changes.

SQL> create table t(id number,value varchar2(10));

Table created.

SQL> create index t_ind on t(id);

Index created.

SQL> @mystat split

NAME VALUE
------------------------------ ----------
leaf node splits 35
leaf node 90-10 splits 0
branch node splits 0

SQL> ed
Wrote file afiedt.buf

1 begin
2 for i in 1..10000 loop
3 insert into t values(i,'test');
4 end loop;
5 commit;
6* end;
SQL> r
1 begin
2 for i in 1..10000 loop
3 insert into t values(i,'test');
4 end loop;
5 commit;
6* end;

PL/SQL procedure successfully completed.

SQL> @mystat2 split

NAME VALUE DIFF
------------------------------ ---------- ----------
leaf node splits 53 53
leaf node 90-10 splits 18 18
branch node splits 0 0

SQL> analyze index t_ind validate structure;

Index analyzed.

SQL> select lf_blks, pct_used from index_stats;

LF_BLKS PCT_USED
---------- ----------
19 94


In this case we see that there have been 18 block splits and all were 90-10 splits as expected. We have 19 leaf blocks and all are nearly full. Depending on where the commit is we can get an index twice the size it has to be. When I ran the same test in 10G it did not matter where the commit was. I got 19 leaf blocks in both cases.

I did not test if this problem happens when several sessions insert a single row and commit just like in an OLTP system but I think it is likely because we have indexes showing this behavior in OLTP systems.

Friday, August 01, 2008

OTN members, don't change your e-mail account!

I am regular user of OTN and its forums. Last week I was trying to login to OTN from a public computer and I got the "invalid login" error everytime I tried. I was sure I was typing my password correct but I could not get in anyway. So, I tried to get my password reset and sent to my e-mail address. Then I remembered that the e-mail address I used to register for OTN was from my previous employer meaning I did not have access to it anymore. As OTN does not allow changing the registration e-mail address I was stuck. I send a request from OTN to get my password delivered to my current e-mail address. Here is the reply I got:

Resolution: Oracle's membership management system does not currently support
the editing of the email address or username in your membership profile.
(It will support this capability in a future release.)
Please create a new account with the new email address you wish to use. However,
it is possible to change the email at which you receive
Discussion Forum "watch" emails (see "Your Control Panel" when logged in).
They tell me to create a new user and forget about my history, watch list, everything. What a user centric approach this is.

If you are an OTN member do not lose your password and your e-mail account at the same time, you will not find anybody from OTN who is willing to solve your problem and help you to recover your password.

I am used to bad behavior and unwillingness to solve problems in Metalink, now I get the same behavior in OTN. Whatever, just wanted to let you know about it.

Thursday, April 17, 2008

Materialized view refresh change in 10G and sql tracing

Complete refresh of a single materialized view used to do a truncate and insert on the mview table until 10G. Starting with 10G the refresh does a delete and insert on the mview table. This guarantees that the table is never empty in case of an error, the refresh process became an atomic operation.

There is another difference between 9.2 and 10G in the refresh process, which I have realized when trying to find out why a DDL trigger prevented a refresh operation. In 9.2 the refresh process runs an ALTER SUMMARY statement against the mview while in 10G it does not.

We have a DDL trigger in some of the test environments (all 9.2) for preventing some operations. The trigger checks the type of the DDL and allows or disallows it based on some conditions. After this trigger was put in place some developers started to complain about some periodic mview refresh operations failing a few weeks ago. I knew it was not because of the TRUNCATE because the trigger allowed truncate operations.

So I enabled sql tracing and found out what the refresh process was doing. Here is a simplified test case for it in 10G.


YAS@10G>create table master as select * from all_objects where rownum<=5;

Table created.

YAS@10G>alter table master add primary key (owner,object_name,object_type);

Table altered.

YAS@10G>create materialized view mview as select * from master;

Materialized view created.

YAS@10G>exec dbms_mview.refresh('MVIEW');

PL/SQL procedure successfully completed.

YAS@10G>create or replace trigger ddl_trigger before ddl on schema
2 begin
3 if ora_sysevent<>'TRUNCATE' then
4 raise_application_error(-20001,'DDL NOT ALLOWED');
5 end if;
6 end;
7 /

Trigger created.

YAS@10G>exec dbms_mview.refresh('MVIEW');

PL/SQL procedure successfully completed.

The refresh was successful in 10G even after the DDL trigger was active. In 9.2 it errors out after the DDL trigger is enabled.

SQL> exec dbms_mview.refresh('MVIEW');
BEGIN dbms_mview.refresh('MVIEW'); END;

*
ERROR at line 1:
ORA-04045: errors during recompilation/revalidation of YAS.MVIEW
ORA-20001: DDL NOT ALLOWED
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 820
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 877
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 858
ORA-06512: at line 1

After enabling sql trace and running the refresh again, the trace file shows the problem statement.

SQL> alter session set sql_trace=true;

Session altered.

SQL> exec dbms_mview.refresh('MVIEW');
BEGIN dbms_mview.refresh('MVIEW'); END;

*
ERROR at line 1:
ORA-04045: errors during recompilation/revalidation of YAS.MVIEW
ORA-20001: DDL NOT ALLOWED
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 820
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 877
ORA-06512: at "SYS.DBMS_SNAPSHOT", line 858
ORA-06512: at line 1

The last statement in the trace file is this:

ALTER SUMMARY "YAS"."MVIEW" COMPILE

So, in 9.2 refresh process runs an ALTER SUMMARY command against the mview. After changing the trigger to allow DDL operations on SUMMARY objects I could refresh the mview without errors.

SQL> r
1 create or replace trigger ddl_trigger before ddl on schema
2 begin
3 if ora_sysevent<>'TRUNCATE' and ora_dict_obj_type<>'SUMMARY' then
4 raise_application_error(-20001,'DDL NOT ALLOWED');
5 end if;
6* end;

Trigger created.

SQL> exec dbms_mview.refresh('MVIEW');

PL/SQL procedure successfully completed.
Tonguc Yilmaz had a recent post about using 10046 for purposes other than performance tuning, like finding out what statements a procedure runs. I find uses for sql tracing nearly everyday and the above case is one of them. When you are not able to understand why an error happens, it is sometimes very useful to turn on sql tracing and examine the trace file. Errors related to specific bind values, errors raised from a "when others" block (you know we do not like "when others", right?) are a couple of things you can dig and analyze with sql tracing.

Friday, April 11, 2008

Disqus

Last week I read a post in Andy C's blog about the service called Disqus. It is a service to keep track of comments in blogs. Later he made another post about it.

I have been looking for a solution to keep track of blog comments, both mine and other people's. Not all blogs have the option to subscribe to the comments, when you comment on a post you need to check later if someone commented further. I want a central repository where I can see all my comments on a post, all comments made by others on the same post and all comments made on my own blog.

I tried Cocomment before but I was not satisfied with it. So I decided to give Disqus a try and enabled it on this blog. Laurent Schneider decided to try it too.

Then Andy made another post about the concerns of some people about Disqus (one being Tim Hall). Their concerns make sense. Who owns the comments one makes, the commenter or the blog owner? Is it sensible to store the comments to your blog not in your blog database, but elsewhere? What if Disqus is not there next year, what if Disqus is inaccessible for some time? Is it possible to export the comments from Disqus and import them back to the blog?

Some of these may be irrelevant if you are using a public blogging service, like Blogger, because it means you are already storing your posts and comments somewhere else. The question "What if Blogger is not there next year?" comes to mind for example.

A solution suggested by Tim Hall is to dual post the comments. The comments will be in the blog and on Disqus also, this need the blogging service to provide a way to do it.

Another solution can be a strong export-import utility in Disqus. That way you can export the comments and put them back to the blog whenever you want. Disqus currently has an export utility but as far as I have read it is not reliable for now.

While I agree with these concerns I liked what Disqus provides. The one-stop page for all comments, the threading of comments, being able to follow other users are primary features I like. So, I will stick with it, at least for now.

Collect in 10G

Collections can be a great help in speeding up the PL/SQL programs. By using bulk collect operations it is possible to get great performance improvements.

In 9.2 we needed to use the bulk collect clause to fetch rows into a collection. 10G brings a new function called COLLECT, which takes a column as a parameter and returns a nested table containing the column values. Using this we can get the data into a collection without using bulk collect.

Here is a very simple demo of this new function.


YAS@10G>create table t as select * from all_objects;

Table created.

YAS@10G>create or replace type name_type as table of varchar2(30);
2 /

Type created.

YAS@10G>set serveroutput on

YAS@10G>r
1 declare
2 v_names name_type;
3 begin
4 select cast(collect(object_name) as name_type) into v_names from t;
5 dbms_output.put_line(v_names.count);
6 dbms_output.put_line(v_names(1));
7* end;
42268
ICOL$

PL/SQL procedure successfully completed.


One difference between this and bulk collect is, since this a sql function we need a sql type for this, it cannot be used with local PL/SQL types.

YAS@10G>r
1 declare
2 type name_type is table of varchar2(30);
3 v_names name_type;
4 begin
5 select collect(object_name) into v_names from t;
6* end;
select collect(object_name) into v_names from t;
*
ERROR at line 5:
ORA-06550: line 5, column 35:
PLS-00642: local collection types not allowed in SQL statements
ORA-06550: line 5, column 9:
PL/SQL: ORA-00932: inconsistent datatypes: expected CHAR got -
ORA-06550: line 5, column 2:
PL/SQL: SQL Statement ignored


Another difference, and a more important one, is the performance difference between these two constructs. Using Tom Kyte's runstats package, I did a test to compare the two. I ran the test several times, the results were similar.

YAS@10G>r
1 declare
2 v_names name_type;
3 begin
4 runStats_pkg.rs_start;
5
6 for i in 1..1000 loop
7 select object_name bulk collect into v_names from t;
8 end loop;
9
10 runStats_pkg.rs_middle;
11
12 for i in 1..1000 loop
13 select cast(collect(object_name) as name_type) into v_names from t;
14 end loop;
15
16 runStats_pkg.rs_stop;
17
18* end;
Run1 ran in 1329 hsecs
Run2 ran in 4060 hsecs
run 1 ran in 32.73% of the time

Name Run1 Run2 Diff
LATCH.qmn state object latch 0 1 1
STAT...redo entries 9 10 1
LATCH.JS slv state obj latch 1 0 -1
LATCH.qmn task queue latch 5 6 1
LATCH.transaction branch alloc 0 1 1
LATCH.sort extent pool 0 1 1
LATCH.resmgr:actses change gro 1 0 -1
LATCH.ncodef allocation latch 0 1 1
LATCH.slave class 0 1 1
LATCH.archive control 0 1 1
LATCH.FAL subheap alocation 0 1 1
LATCH.FAL request queue 0 1 1
STAT...heap block compress 6 5 -1
LATCH.session switching 0 1 1
LATCH.ksuosstats global area 1 2 1
LATCH.event group latch 1 0 -1
LATCH.threshold alerts latch 0 1 1
LATCH.list of block allocation 2 0 -2
LATCH.transaction allocation 2 0 -2
LATCH.dummy allocation 3 1 -2
LATCH.user lock 2 0 -2
LATCH.Consistent RBA 4 2 -2
STAT...calls to kcmgcs 4 6 2
STAT...active txn count during 4 6 2
STAT...cleanout - number of kt 4 6 2
STAT...consistent gets 587,009 587,011 2
STAT...consistent gets from ca 587,009 587,011 2
STAT...consistent gets - exami 4 6 2
LATCH.resmgr:free threads list 3 0 -3
LATCH.PL/SQL warning settings 3 0 -3
LATCH.OS process 6 3 -3
LATCH.compile environment latc 3 0 -3
LATCH.slave class create 0 3 3
LATCH.resmgr:actses active lis 3 0 -3
LATCH.cache buffers lru chain 0 3 3
LATCH.OS process allocation 10 14 4
LATCH.session state list latch 4 0 -4
LATCH.redo allocation 50 46 -4
STAT...consistent changes 17 22 5
LATCH.resmgr group change latc 5 0 -5
STAT...db block gets 17 22 5
STAT...db block gets from cach 17 22 5
LATCH.library cache pin alloca 6 0 -6
STAT...db block changes 26 32 6
STAT...session logical reads 587,026 587,033 7
LATCH.In memory undo latch 23 16 -7
LATCH.session idle bit 10 3 -7
LATCH.mostly latch-free SCN 6 14 8
LATCH.KMG MMAN ready and start 5 13 8
LATCH.lgwr LWN SCN 6 14 8
LATCH.simulator hash latch 34,010 34,001 -9
LATCH.simulator lru latch 34,010 34,001 -9
LATCH.session timer 5 14 9
LATCH.dml lock allocation 10 1 -9
LATCH.undo global data 20 10 -10
LATCH.post/wait queue 10 0 -10
LATCH.active checkpoint queue 4 15 11
LATCH.session allocation 13 2 -11
LATCH.archive process latch 4 15 11
LATCH.library cache lock alloc 16 0 -16
LATCH.object queue header oper 8 32 24
LATCH.client/application info 25 0 -25
LATCH.redo writing 24 51 27
LATCH.active service list 37 81 44
STAT...undo change vector size 2,124 2,200 76
LATCH.channel operations paren 60 197 137
LATCH.cache buffers chains 1,174,359 1,174,189 -170
LATCH.JS queue state obj latch 108 288 180
LATCH.messages 108 291 183
STAT...redo size 2,772 2,964 192
LATCH.checkpoint queue latch 80 282 202
LATCH.enqueue hash chains 269 652 383
LATCH.enqueues 248 645 397
LATCH.SQL memory manager worka 276 944 668
LATCH.shared pool 42 1,029 987
LATCH.library cache lock 170 2,018 1,848
LATCH.library cache pin 2,130 4,064 1,934
STAT...Elapsed Time 1,330 4,061 2,731
STAT...CPU used by this sessio 1,334 4,083 2,749
STAT...recursive cpu usage 1,268 4,064 2,796
LATCH.library cache 2,264 5,074 2,810
LATCH.row cache objects 49 9,015 8,966
STAT...session pga memory 2,097,152 1,441,792 -655,360

Run1 latches total versus runs -- difference and pct
Run1 Run2 Diff Pct
1,248,536 1,267,073 18,537 98.54%

PL/SQL procedure successfully completed.


As you see the bulk collect method completes in 1/3rd of the time of the collect function method. Using the collect function hits the library cache harder and uses more latch operations.

The COLLECT function can be used in sql statements to compare collections of columns. In Laurent Schneider's comment on my previous post you can find an example of it.

Wednesday, April 09, 2008

Relational algebra: division in sql

There was a question in one of the Turkish Oracle mailing lists which got me interested. The question simply was:

I have a table holding the parts of a product and another table holding the suppliers of these parts. How can I find the suppliers which supply all the parts?

Someone suggested using the division operation of relational algebra but did not provide how to do it in Oracle. So I started with the Wikipedia link he provided to solve the problem in sql.

Here are the tables:


SQL> create table parts (pid number);

Table created.

SQL> create table catalog (sid number,pid number);

Table created.

SQL> insert into parts select rownum from all_objects where rownum<=5;

5 rows created.

SQL> insert into catalog values (10,1);

1 row created.

SQL> insert into catalog select 1,pid from parts;

5 rows created.

SQL> select * from catalog;

SID PID
---------- ----------
10 1
1 1
1 2
1 3
1 4
1 5


So, the supplier which has all the parts is 1. How do we find that?

The division in relational algebra is done by some steps which are explained in the link. Let's follow those:

The simulation of the division with the basic operations is as follows. We assume that a1,...,an are the attribute names unique to R and b1,...,bm are the attribute names of S. In the first step we project R on its unique attribute names and construct all combinations with tuples in S:

T := Ï€a1,...,an(R) × S

In our case we have the table CATALOG as R, the table PARTS as S. So if we write a sql to find out the above relation we need a cartesian join.

select sid,pid
from (select sid from catalog) ,parts
;


This give us all suppliers combined with all parts.

SID PID
---------- ----------
10 1
1 1
1 1
1 1
1 1
1 1
10 2
1 2
1 2
1 2
1 2

SID PID
---------- ----------
1 2
10 3
1 3
1 3
1 3
1 3
1 3
10 4
1 4
1 4
1 4

SID PID
---------- ----------
1 4
1 4
10 5
1 5
1 5
1 5
1 5
1 5

We now have all the possibilities for the supplier-part relation.

The second step is:

In the next step we subtract R from this relation:

U := T - R

To subtract the table CATALOG we need the MINUS operator.

select sid,pid
from (select sid from catalog) ,parts
minus
select sid,pid from catalog;

SID PID
---------- ----------
10 2
10 3
10 4
10 5





After we had all the possibilities we subtracted the ones which are already in the CATALOG table and we got the ones which are not present in the table.

On to the next step:

Note that in U we have the possible combinations that "could have" been in R, but weren't. So if we now take the projection on the attribute names unique to R then we have the restrictions of the tuples in R for which not all combinations with tuples in S were present in R:

V := πa1,...,an(U)

This step just gets the supplier id's from the previous query.

select sid from (
select sid,pid
from (select sid from catalog) ,parts
minus
select sid,pid from catalog
);

SID
----------
10
10
10
10



Now we have the supplier id which does not supply all of the parts. The next step is obvious, to find the other suppliers (the remaining ones supply all the parts).

So what remains to be done is take the projection of R on its unique attribute names and subtract those in V:

W := πa1,...,an(R) - V

To do this we need to subtract the previous query from the table CATALOG.

select sid from catalog
minus
select sid from (
select sid,pid
from (select sid from catalog) ,parts
minus
select sid,pid from catalog
);

SID
----------
1


In some pages (like this one) the same operation is done using a different sql (obtained by using "not exists" instead of "minus").

select distinct sid from catalog c1
where not exists (
select null from parts p
where not exists (select null from catalog where pid=p.pid and c1.sid=sid));


This one is harder for me to understand in the first look, I am more comfortable following the steps above.

It is great to follow the steps of relational algebra to solve a problem in sql. It helps very much in understanding the solution. Relational algebra rocks!

Tuesday, February 05, 2008

Local vs. remote connection performance

It has been asked several times in several places; is there a performance difference between running a query locally in a client on the server and running the same query in a remote client?

The obvious answer given by the respondents including myself is: "if you do not return thousands of rows through the network, there must not be any difference". This type of response is opposed to what I believe; even if the answer seems obvious test it before you make any suggestions.

Tanel Poder got the same question and did what is needed to be done, he tested it and showed that there was a difference. In this great post of his.

His tests use a database on Solaris, sqlplus clients on Windows and Linux. I have tested the same using a database on Linux and the same behavior is observed there too.

Lesson learned again and again: test your suggestion even if the answer seems obvious.

Monday, February 04, 2008

Database version control

Coding horror is one of the software development blogs I keep a close eye on.

Jeff Atwood posted a nice piece about database version control recently. Database version control is maybe one of the most important and unfortunately most overlooked things in software development. The post is a good read including the links he provides.

Friday, January 11, 2008

The blog tagging thing

During the last few days lots of Oracle bloggers have been busy tagging each other and posting eight unknown things about themselves. I was also tagged by some friends and was asked to post eight things about myself. I have never forwarded any chain e-mails or messages to anyone and in parallel to that I have not written anything about myself after this either.

What I think about this blog tagging thing is very similar to what Howard Rogers thought about it. He shut his site down for some time and you can read what he thinks when you go to his blog. My thoughts on this are here in the comments to an Eddie Awad post. Howard has also posted a comment there to explain further.

Sunday, December 09, 2007

Hiring DBAs based on certification

There was a question in the OTN database forum recently which said:

" Can any one advise me? I have joined a company as new senior dba.

i am not understanding what shall be done at beginning?

Can anybody advice me how to check all the database and what to do in the beginning ?

I have been reading all the stuff,documents fr. a week?"

The same poster also said:

" I have all th knowledge of oracle,I am ocp certified.

But fearing at the beginning.

how to proceede as this is my first job as a dba."

This is what can happen when a company hires someone for a position like "senior DBA" just based on some papers called certifications.

How he claims he has "all the knowledge of oracle" just by getting an OCP certification is another story.

Thursday, December 06, 2007

Consolidating oraInventories

I have always had trouble with dealing with multiple oraInventories and multiple oraInst.loc files in a server.

If there are lots of people installing products in a server or if you are handed an environment which is in such a mess it is a big trouble to get things tidied up (if the installer people used a central inventory instead of each one using their own inventories, you a re lucky).

You need to know which oraInst.loc points to which oraInventory and which products and versions are in that inventory. You need to put the right oraInst.loc in place in order to install a patchset or a patch. Or you need a way to make the central inventory know about all Oracle homes which exist but which are not in the inventory.

In such situations Oracle Universal Installer 10.2 comes to the rescue. There is a Metalink note I have just realized which is 438133.1. The note explains how to consolidate multiple Oracle homes in one central inventory. There is a new option of the installer in 10.2 which is -attachHome. Using a 10.2 installer and this option you can get all your homes included in the central inventory.

./runInstaller -silent -attachHome ORACLE_HOME=<home you want to add> ORACLE_HOME_NAME=<unique home name>

The steps are clearly explained in the note which basically are; using a 10.2 universal installer, setting oraInst.loc to point to the inventory which you want to use to consolidate everything in, setting $ORACLE_HOME to the home you want to include and running the installer with the -attachHome option.

This works for 9i and 10G.

Optimizer blog

As I have learnt from Greg Rahn's blog Structured Data, the people behind the optimizer started blogging. In their first technical post they talk about bind peeking in 11G. I think we will see lots of examples and explanations about the optimizer in there. Their blog is here.

Wednesday, December 05, 2007

Hard Disk Reference

There was a reference to a site about the hard disk a few days ago in the oracle-l list from Ranko Mosic. I have gone through half of it for now, I can say the referenced pages are a great guide for understanding the basics of the hard disk, its parts, inner workings, etc... Trying to tune I/O without knowing about the hard disk basics can lead a DBA to the wrong path easily, this site is a must read in my opinion.

Friday, November 30, 2007

Rotate your logs

If you are using Linux and not rotating your alert logs, listener logs, any log actually, or rotating them with your own scripts, check out logrotate. I did not know this existed. From Dizwell.

Wednesday, November 28, 2007

Bind peeking change in 10g

A new note called "10g Upgrade Companion" has been published in Metalink recently. The note talks about the upgrade from 9i to 10g and lists recommended patches, behavior changes and best practices. While I was going through it I saw something I was not aware of before. In the "behavior changes" section it says: "Bind peeking has been extended to binds buried inside expressions.".

I have talked about bind variable peeking and a behavior change of that feature in 11g before.

In 9i bind variables in expressions were not peeked at, 10g changes this. Now bind variables in expressions are also peeked at.

To see the change in action I have done a simple test. Let's start with the behavior in 9i.


SQL> create table t (col1 number,col2 varchar2(30));

Table created.

SQL> insert into t select 1,object_name from all_objects where rownum<=10000;

10000 rows created.

SQL> update t set col1=0 where rownum=1;

1 row updated.

SQL> create index tind on t(col1);

Index created.

SQL> exec dbms_stats.gather_table_stats(ownname=>user,tabname=>'T',cascade=>true,method_opt=>'for columns col1 size 10');

PL/SQL procedure successfully completed.

Now I have a table with 10,000 rows with skewed data distribution, one row has col1=0, all other rows have col1=1.

SQL> alter session set events '10046 trace name context forever, level 12';

Session altered.

After enabling the trace I execute the following two blocks.


declare
b1 varchar2(1);
begin
b1:=0;
for t in (select * from t where col1=to_number(b1)) loop
null;
end loop;
end;
/

PL/SQL procedure successfully completed.

declare
b1 varchar2(1);
begin
b1:=1;
for t in (select * from t where col1=to_number(b1)) loop
null;
end loop;
end;
/

PL/SQL procedure successfully completed.


What I expect in 9i is; since the bind variable b1 is used in an expression it will not be peeked at, the optimizer will think that b1=0 will return 5000 rows since the number of distinct values for the column is 2 and since there are 10000 rows in the table and it will full scan the table.

When we tkprof the resulting trace file with the "aggregate=no" option what we see is:

SELECT *
FROM
T WHERE COL1=TO_NUMBER(:B1 )


call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 2 0.01 0.01 0 48 0 1
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 4 0.01 0.01 0 48 0 1

Misses in library cache during parse: 1
Optimizer goal: CHOOSE
Parsing user id: SYS (recursive depth: 1)

Rows Row Source Operation
------- ---------------------------------------------------
1 TABLE ACCESS FULL T

SELECT *
FROM
T WHERE COL1=TO_NUMBER(:B1 )


call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 10000 0.35 0.26 0 10003 0 9999
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 10002 0.35 0.26 0 10003 0 9999

Misses in library cache during parse: 0
Optimizer goal: CHOOSE
Parsing user id: SYS (recursive depth: 1)

Rows Row Source Operation
------- ---------------------------------------------------
9999 TABLE ACCESS FULL T

It chose a full table scan for b1=0 and used it for the two queries.

If we run the same test in 10g what we see is:


SELECT *
FROM
T WHERE COL1=TO_NUMBER(:B1 )


call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 1 0.00 0.00 0 3 0 1
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 3 0.00 0.00 0 3 0 1

Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 43 (recursive depth: 1)

Rows Row Source Operation
------- ---------------------------------------------------
1 TABLE ACCESS BY INDEX ROWID T (cr=3 pr=0 pw=0 time=101 us)
1 INDEX RANGE SCAN TIND (cr=2 pr=0 pw=0 time=43 us)(object id 44327)



SELECT *
FROM
T WHERE COL1=TO_NUMBER(:B1 )


call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 100 0.01 0.01 0 256 0 9999
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 102 0.01 0.01 0 256 0 9999

Misses in library cache during parse: 0
Optimizer mode: ALL_ROWS
Parsing user id: 43 (recursive depth: 1)

Rows Row Source Operation
------- ---------------------------------------------------
9999 TABLE ACCESS BY INDEX ROWID T (cr=256 pr=0 pw=0 time=269978 us)
9999 INDEX RANGE SCAN TIND (cr=120 pr=0 pw=0 time=139980 us)(object id 44327)

It peeked at the bind variable in the expression, found out that b1=0 will return one row and decided to use the index. After that since the plan is fixed now it used the same plan for b1=1 also.

This is one of many changes in 10g that can change the execution plans after the upgrade from 9i. Be aware of this if you have bind variables in expressions in your sql statements. Who does not have actually?

Wednesday, October 31, 2007

fast=true for adding columns in 11G

Before 11G adding new columns with default values to tables with millions of rows was a very time consuming and annoying task. Oracle had to update all the rows with the default value when adding the column.

11G introduced a fast=true feature for adding columns with default values. In 11G when you want to add a not null column with a default value, the operation completes in the blink of an eye (if there are no locks on the table of course). Now it does not have to update all the rows, it just keeps the information as meta-data in the dictionary. So adding a new column becomes just a few dictionary operations.

How does this work? How does it return the data if it does not store in the table's blocks?

To answer some questions let's see it in action.


YAS@11G>create table t_default (col1 number);

Table created.

YAS@11G>insert into t_default select 1 from dual;

1 row created.

YAS@11G>commit;

Commit complete.

Now we have a table with one column which has one row in it. Let's add a not null column with a default value.

YAS@11G>alter table t_default add(col_default number default 0 not null);

Table altered.

Now let's have a look at what is in the table's blocks for this one row.

YAS@11G>select dbms_rowid.rowid_block_number(rowid) from t_default where col1=1;

DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)
------------------------------------
172

YAS@11G>alter system dump datafile 4 block 172;

System altered.

The trace file shows the first column but not the one we added after. It did not update the blocks with the new column's default value.

block_row_dump:
tab 0, row 0, @0x1f92
tl: 6 fb: --H-FL-- lb: 0x1 cc: 1
col 0: [ 2] c1 02
end_of_block_dump

If we insert a new row using the default value again.

YAS@11G>insert into t_default values(2,DEFAULT);

1 row created.

YAS@11G>commit;

Commit complete.

YAS@11G>select dbms_rowid.rowid_block_number(rowid) from t_default where col1=2;

DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)
------------------------------------
176

YAS@11G>alter system dump datafile 4 block 176;

System altered.

Now the trace file shows the second column also. So Oracle does not update the rows when adding the column. But for subsequent inserts it inserts the default value for the column if no value is supplied. This means the metadata updates are only used when adding the column, after that updates and inserts modify the block for that column also.

block_row_dump:
tab 0, row 0, @0x1f90
tl: 8 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 03
col 1: [ 1] 80
end_of_block_dump

So, what happens when we query the table after adding a not null column with a default value?

Now we know that the rows at the time of the add column operation are not updated, how does it return the value for that column?

Let's do a simple query and see the trace file.

YAS@11G>exec dbms_stats.gather_table_stats(user,'T_DEFAULT');

PL/SQL procedure successfully completed.

YAS@11G>alter session set events '10046 trace name context forever, level 12';

Session altered.

YAS@11G>select * from t_default where col1=1;

COL1 COL_DEFAULT
---------- -----------
1 0

YAS@11G>select * from t_default where col1=2;

COL1 COL_DEFAULT
---------- -----------
2 0

YAS@11G>exit

I have just queried the row which was inserted before the add column operation (whose new column is not updated but stored in the dictionary as metadata) and after that the second row which is inserted after the add column operation (whose new column is updated in the table block).

The part in the raw trace related to the first row shows that just after my query there are some queries from the data dictionary, like the one from ecol$ just below my query.

PARSING IN CURSOR #3 len=36 dep=0 uid=60 oct=3 lid=60 tim=1193743350604471 hv=3796946483 ad='2b3dbc20'

sqlid='dbjjphgj51mjm'
select * from t_default where col1=1
END OF STMT
PARSE #3:c=7998,e=8401,p=0,cr=2,cu=0,mis=1,r=0,dep=0,og=1,tim=1193743350604449
BINDS #3:
EXEC #3:c=0,e=73,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=1,tim=1193743350604794
WAIT #3: nam='SQL*Net message to client' ela= 6 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743350604856
FETCH #3:c=0,e=107,p=0,cr=3,cu=0,mis=0,r=1,dep=0,og=1,tim=1193743350605014
WAIT #3: nam='SQL*Net message from client' ela= 1304 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743350606363
FETCH #3:c=1000,e=992,p=0,cr=4,cu=0,mis=0,r=0,dep=0,og=1,tim=1193743350607750
STAT #3 id=1 cnt=1 pid=0 pos=1 obj=66986 op='TABLE ACCESS FULL T_DEFAULT (cr=7 pr=0 pw=0 time=0 us cost=3 size=5 card=1)'
WAIT #3: nam='SQL*Net message to client' ela= 4 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743350607916

*** 2007-10-30 13:22:34.800
WAIT #3: nam='SQL*Net message from client' ela= 4192534 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743354800488
=====================
PARSING IN CURSOR #3 len=97 dep=1 uid=0 oct=3 lid=0 tim=1193743354801333 hv=2759248297 ad='2b3983f0' sqlid='aa35g82k7dkd9'
select binaryDefVal, length(binaryDefVal) from ecol$ where tabobj# = :1 and colnum = :2
END OF STMT
PARSE #3:c=0,e=15,p=0,cr=0,cu=0,mis=0,r=0,dep=1,og=4,tim=1193743354801320
BINDS #3:
Bind#0
oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
oacflg=00 fl2=0001 frm=00 csi=00 siz=48 off=0
kxsbbbfp=002fefb0 bln=22 avl=04 flg=05
value=66986
Bind#1
oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
oacflg=00 fl2=0001 frm=00 csi=00 siz=0 off=24
kxsbbbfp=002fefc8 bln=22 avl=02 flg=01
value=2


But the part for the second row does not show those dictionary queries.


PARSING IN CURSOR #4 len=36 dep=0 uid=60 oct=3 lid=60 tim=1193743354802685 hv=704792472 ad='32569228'

sqlid='7dq6h0np04jws'
select * from t_default where col1=2
END OF STMT
PARSE #4:c=1999,e=1898,p=0,cr=2,cu=0,mis=1,r=0,dep=0,og=1,tim=1193743354802668
BINDS #4:
EXEC #4:c=0,e=71,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=1,tim=1193743354802925
WAIT #4: nam='SQL*Net message to client' ela= 6 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743354802987
FETCH #4:c=0,e=119,p=0,cr=7,cu=0,mis=0,r=1,dep=0,og=1,tim=1193743354803159
WAIT #4: nam='SQL*Net message from client' ela= 561 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743354803722
FETCH #4:c=0,e=24,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=1,tim=1193743354803817
STAT #4 id=1 cnt=1 pid=0 pos=1 obj=66986 op='TABLE ACCESS FULL T_DEFAULT (cr=7 pr=0 pw=0 time=0 us cost=3 size=5 card=1)'
WAIT #4: nam='SQL*Net message to client' ela= 4 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743354803971

*** 2007-10-30 13:22:35.872
WAIT #4: nam='SQL*Net message from client' ela= 1068525 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1193743355872534
XCTEND rlbk=0, rd_only=1
=====================
PARSING IN CURSOR #3 len=234 dep=1 uid=0 oct=6 lid=0 tim=1193743355873381 hv=1907731640 ad='2b3d6300'

sqlid='209fr01svbb5s'
update sys.aud$ set action#=:2, returncode=:3, logoff$time=cast(SYS_EXTRACT_UTC(systimestamp) as date), logoff$pread=:4,

logoff$lread=:5, logoff$lwrite=:6, logoff$dead=:7, sessioncpu=:8 where sessionid=:1 and entryid=1 and action#=100
END OF STMT

So this means if we query the table for rows whose related columns are maintained in the dictionary we get those values not from the table, but from the dictionary. This is explained in the documentation as:

"However, subsequent queries that specify the new column are rewritten so that the default value is returned in the result set."

What is this ecol$ table that the database queries to get the default column value?

ecol$ is a new dictionary table in 11G. It is created in $ORACLE_HOME/rdbms/admin/dcore.bsq which is called from $ORACLE_HOME/rdbms/admin/sql.bsq which contains all scripts used for creating basic dictionary tables for database operation.

In dcore.bsq it is explained as:

"This table is an extension to col$ and is used (for now) to store the default value with which a column was added"

How do these additional dictionary queries effect my queries?

To see if these additional dictionary queries effect the original query performance let's create two tables each with two columns, one with a not null column with a default value added after some rows are inserted, and a copy of it with the columns in place when creating the table. And query these tables and see their trace.


YAS@11G>r
1* drop table t_default

Table dropped.

YAS@11G>create table t_default (col1 number);

Table created.

YAS@11G>insert into t_default select level from dual connect by level<=10000;

10000 rows created.

YAS@11G>commit;


Commit complete.

YAS@11G>alter table t_default add(col_default number default 0 not null);

Table altered.

YAS@11G>exec dbms_stats.gather_table_stats(user,'T_DEFAULT');

PL/SQL procedure successfully completed.

YAS@11G>alter session set events '10046 trace name context forever, level 12';

Session altered.

YAS@11G>begin
2 for t in (select * from t_default) loop
3 null;
4 end loop;
5 end;
6 /

PL/SQL procedure successfully completed.

YAS@11G>exit

I have selected all rows after adding the column. Here is the trace file output. I am just including the part with the original sql and the summary part.

SELECT *
FROM
T_DEFAULT


call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 101 0.01 0.01 0 119 0 10000
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 103 0.02 0.02 0 119 0 10000

OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 8 0.00 0.00 0 0 0 0
Execute 8 0.00 0.00 0 3 3 1
Fetch 109 0.01 0.01 0 133 0 10003
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 125 0.02 0.02 0 136 3 10004

Misses in library cache during parse: 1

2 user SQL statements in session.
7 internal SQL statements in session.
9 SQL statements in session.


I have 119 reads for the table and 139-119=20 reads from dictionary tables. I have 7 dictionary sqls each executed one time.

Try the same with a table with the same data which has the columns in place in creation.

YAS@11G>create table t_normal (col1 number,col_default number);

Table created.

YAS@11G>insert into t_normal select level,0 from dual connect by level<=10000;

10000 rows created.

YAS@11G>exec dbms_stats.gather_table_stats(user,'T_NORMAL');


PL/SQL procedure successfully completed.

YAS@11G>commit;

Commit complete.

YAS@11G>alter session set events '10046 trace name context forever, level 12';

Session altered.

YAS@11G>begin
2 for t in (select * from t_normal) loop
null;
end loop;
end;
3 4 5 6
7 /

PL/SQL procedure successfully completed.

YAS@11G>exit

SELECT *
FROM
T_NORMAL


call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 101 0.00 0.01 0 119 0 10000
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 103 0.01 0.01 0 119 0 10000

OVERALL TOTALS FOR ALL RECURSIVE STATEMENTS

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 2 0.00 0.00 0 0 0 0
Execute 2 0.00 0.00 0 3 3 1
Fetch 101 0.00 0.01 0 119 0 10000
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 105 0.01 0.01 0 122 3 10001

Misses in library cache during parse: 1

2 user SQL statements in session.
1 internal SQL statements in session.
3 SQL statements in session.

I have again 119 reads for the table. But now there is only one dictionary sql (which is an update on aud$ which was also there for the former case). I have now 6 dictionary reads instead of 20.

As you can see there is little overhead for querying the dictionary for the default values of columns. No matter how many rows you have, those dictionary sqls are run only once for your query. But the gain you have from this feature is huge, availability is dramatically increased, meaning now you can sleep more instead of waiting for an add column operation.

There are some restrictions for this fast=true switch which are explained here:

"However, the optimized behavior is subject to the following restrictions:
*The table cannot have any LOB columns. It cannot be index-organized, temporary, or part of a cluster. It also cannot be a queue table, an object table, or the container table of a materialized view.
*The column being added cannot be encrypted, and cannot be an object column, nested table column, or a LOB column."

Thursday, September 20, 2007

ORA-04043 in mount mode

There was a question at the OTN Database-General forum today about a problem when trying to describe the view dba_tablespaces.

The poster was getting an ORA-04043 error.


SQL> desc dba_tablespaces
ERROR:
ORA-04043: object dba_tablespaces does not exist

The first thing I thought about this was that the instance might have been in mount mode. I tried it on a database in mount stage and I got the error.

SQL> shutdown abort
ORACLE instance shut down.
SQL> startup mount;
ORACLE instance started.

Total System Global Area 404723928 bytes
Fixed Size 735448 bytes
Variable Size 234881024 bytes
Database Buffers 167772160 bytes
Redo Buffers 1335296 bytes
Database mounted.
SQL> desc dba_tablespaces
ERROR:
ORA-04043: object dba_tablespaces does not exist

I was expecting to be able to see the view after opening the database, but...

SQL> alter database open;
Database altered.

SQL> desc dba_tablespaces
ERROR:
ORA-04043: object dba_tablespaces does not exist

I could not query it either.

SQL> select * from dba_tablespaces;
select * from dba_tablespaces
*
ERROR at line 1:
ORA-00942: table or view does not exist

A quick search in Metalink returned note 296235.1 which refers to the bug 2365821 and says that if you describe any dba_* view in mount mode you cannot describe the same view even after opening the database. The only solution is to restart the database.

SQL> shutdown abort
ORACLE instance shut down.
SQL> startup mount
ORACLE instance started.

Total System Global Area 404723928 bytes
Fixed Size 735448 bytes
Variable Size 234881024 bytes
Database Buffers 167772160 bytes
Redo Buffers 1335296 bytes
Database mounted.
SQL> alter database open;

Database altered.

SQL> desc dba_tablespaces
Name Null? Type
----------------------------------------- -------- ----------------------------
TABLESPACE_NAME NOT NULL VARCHAR2(30)
BLOCK_SIZE NOT NULL NUMBER
INITIAL_EXTENT NUMBER
NEXT_EXTENT NUMBER
MIN_EXTENTS NOT NULL NUMBER
MAX_EXTENTS NUMBER
PCT_INCREASE NUMBER
MIN_EXTLEN NUMBER
STATUS VARCHAR2(9)
CONTENTS VARCHAR2(9)
LOGGING VARCHAR2(9)
FORCE_LOGGING VARCHAR2(3)
EXTENT_MANAGEMENT VARCHAR2(10)
ALLOCATION_TYPE VARCHAR2(9)
PLUGGED_IN VARCHAR2(3)
SEGMENT_SPACE_MANAGEMENT VARCHAR2(6)
DEF_TAB_COMPRESSION VARCHAR2(8)

I was not aware of this behaviour till now. I tested this on 9.2 but the bug seems not fixed in any version.