Showing posts with label Oracle Insight. Show all posts
Showing posts with label Oracle Insight. Show all posts

Sunday, October 3, 2010

Character of DUAL


Character of DUAL:
DUAL is owned by SYS. SYS owns the data dictionary, therefore DUAL is part of the data dictionary. You are not to modify the data dictionary via SQL ever.
Dual table and its purpose:
Dual is just a convenience table. You don't need to use it, you can use anything you want. The advantage to dual is the optimizer understands dual is a special one row, one column table -- when you use it in queries, it uses this knowledge when developing the plan.
The Dual table structure, field and record details are below.
SQL> desc dual;
Name Null? Type
-------------- -------- ----------------------------
DUMMY VARCHAR2(1)
SQL> select * from dual;
D
-
X
SQL> select count(*) from dual;
COUNT(*)
----------
1DUAL is owned by SYS:

The below scripts connect to DB as SYSTEM user and try to modify the DUAL table. The insert statement is not able to modify the table which is owned by SYS user.

SQL> conn system/password@oraprc
Connected.

SQL> INSERT INTO DUAL VALUES ('X');
INSERT INTO DUAL VALUES ('X')
*
ERROR at line 1:
ORA-01031: insufficient privileges

We change the connection to SYS user and try the same and it is able to insert the data.
SQL> conn sys/password@oraprc as sysdba
Connected.

SQL> INSERT INTO DUAL VALUES ('X');

1 row created.

SQL> select count(*) from dual;
COUNT(*)
----------

2
Special one row:
The definition has mentioned that DUAL is special one row, one column table. But we can insert the records in that table. So the count got changed in that.

SQL> INSERT INTO DUAL VALUES ('X');
1 row created.

SQL> select count(*) from dual;
COUNT(*)
---------

3

SQL> select * from dual;
D
-
X

The reason is that, the optimizer understands dual is a magic, special 1 row table. It stopped on the select * because there is to be one row in there. It’s just the way it works.

Delete operation on DUAL:
SQL> select count(*) from dual;
COUNT(*)
----------

3
SQL> delete from dual;
1 row deleted.


SQL> select count(*) from dual;
COUNT(*)
----------

2

SQL> delete from dual;
1 row deleted.

SQL> select count(*) from dual;
COUNT(*)
----------

1
The delete statement is deleting single record each time. The reason is that, the DUAL is special one row table.

Checking with FUCTION:

We create the function which return the value “1” and check the same function with DUAL and user created table.

SQL> select count(*) from dual;

COUNT(*)
----------
2


SQL> create or replace function foo return number
2 as
3 x number;
4 begin
5 x:=1;
6 return 1;
7 end;
8 /
Function created.

SQL> create table emp
2 ( num int);

Table created.

SQL> insert into emp values(1);

1 row created.

SQL> insert into emp values(2);

1 row created.

SQL> insert into emp values(3);

1 row created.

SQL> insert into emp values(4);

1 row created.

SQL> commit;

Commit complete.

SQL> select * from emp;

NUM
----------
1
2
3
4
SQL> select foo from dual;

FOO
----------
1
SQL> select foo from emp;
FOO
----------
1
1
1
1

The EMP table is having the 4 records and it is returning the 4 records when do the select on that table. We have checked the same thing with the DUAL table. The count of the DUAL is 2 and it is returning one record.

Saturday, September 4, 2010

CHAR Vs VARCHAR



CHAR Vs VARCHAR:


We see the information about the CHAR and VARCHAR in this Blog.

Create table t ( X varchar2(30) , Y char(30));
Insert into t values ('a','a');

The above table is having the two fields X & Y and corresponding data types are varchar2 and char. The CHAR is nothing more than a VARCHAR2 that is blank padded out to the maximum length. That is difference between the column X and Y.
The field X consumes 3 bytes – ( NULL indicator, leading byte length , 1 byte for ‘a’).

The field Y consumes 32 bytes–(NULL indicator, leading byte length, 30 byte for ‘a ’).

Need to consider the below points when we use the CHAR data type:

1. The a CHAR type always blank pads the resulting string out to a fixed width, we discover rapidly that it consumes maximum storage both in the table segment and any index segments.

2. Another important reason to avoid CHAR types: they create confusion in applications that need to retrieve this information (many cannot “find” their data after storing it). The reason for this relates to the rules of character string comparison and the strictness with which they are performed. The Below scripts proves that.

SQL@ORA9i> create table t
2 ( char_column char(20),
3 varchar2_column varchar2(20)
4 );

Table created.

SQL@ORA9i> insert into t values ( 'Hello World', 'Hello World' );

1 row created.

SQL@ORA9i> select * from t;

CHAR_COLUMN VARCHAR2_COLUMN
-------------------- --------------------
Hello World Hello World

SQL@ORA9i> select * from t where char_column = 'Hello World';

CHAR_COLUMN VARCHAR2_COLUMN
------------ --------------------
Hello World Hello World

SQL@ORA9i> select * from t where varchar2_column = 'Hello World';

CHAR_COLUMN VARCHAR2_COLUMN
-------------------- --------------------
Hello World Hello World


The above result looks like identical but, in fact, some implicit conversion has taken place and the CHAR(11) literal ‘Hello World’ has been promoted to a CHAR(20) and blank padded when compared to the CHAR column. The reason is, ‘Hello World ’ is not the same as ‘Hello World’ without the trailing spaces. We can confirm that these two strings are different.

SQL@ORA9i> select * from t where char_column = varchar2_column;
no rows selected


They are not equal to each other. We would have to either blank pad out the VARCHAR2_COLUMN to be 20 bytes in length or trim the trailing blanks from the CHAR_COLUMN, as follows:

SQL@ORA9i> select * from t where trim(char_column) = varchar2_column;

CHAR_COLUMN VARCHAR2_COLUMN
-------------------- --------------------
Hello World Hello World

SQL@ORA9i> select * from t where char_column = rpad( varchar2_column, 20 );

CHAR_COLUMN VARCHAR2_COLUMN
-------------------- --------------------
Hello World Hello World
The problem arises with applications that use variable length strings when they bind inputs, with the resulting “no data found”


SQL@ORA9i> variable varchar2_bv varchar2(20)
SQL@ORA9i> exec :varchar2_bv := 'Hello World';

PL/SQL procedure successfully completed.

SQL@ORA9i> select * from t where char_column = :varchar2_bv;

no rows selected

SQL@ORA9i> select * from t where varchar2_column = :varchar2_bv;

CHAR_COLUMN VARCHAR2_COLUMN
-------------------- --------------------
Hello World Hello World
The above search for VARCHAR2 string worked but not for CHAR. The VARCHAR2 bind variable will not be promoted to a CHAR(20) in the same way as a character string literal. At this point, many programmers form the opinion that “bind variables don’t work; we have to use literals.” That would be a very bad decision indeed.

The solution is to bind using a CHAR type:

SQL@ORA9i> variable char_bv char(20)
SQL@ORA9i> exec :char_bv := 'Hello World';

PL/SQL procedure successfully completed.

SQL@ORA9i> select * from t where char_column = :char_bv;

CHAR_COLUMN VARCHAR2_COLUMN
-------------------- -------------------
Hello World Hello World

SQL@ORA9i> select * from t where varchar2_column = :char_bv;

no rows selected

We will be running into this issue constantly if we mix and match CHAR and VARCHAR.

Thursday, August 5, 2010

Performance Improvement - Part 3: Correlated Update tuning

Performance Improvement - Part 3: Correlated Update tuning:

We have seen the small difference in correlated update statement execution in the previous Blog. Please refer below link if need to check it.


http://karthikeyanbaskaran.blogspot.com/2010/06/difference-in-correlated-update.html

The scenario is, need to update the “Address” field in table”Main” from the “Sub” table using the “id” field. The -1 needs to update in all other columns. The below update method solve it in single statement.

update main mset address = nvl((select address from sub s where m.id=s.id),-1);
Correlated update statement process for all the rows in the table “MAIN”. We see how it works when we load the lot records in the table “MAIN” and performance improvement method.


SQL> insert into main
2 select * from main;
............
.......
....
SQL> insert into main
2 select * from main;

Now the table “Main” is having the 1048576 records and table “Sub” is having the 3 records.

SQL> select count(*) from main;

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

1048576

SQL> select count(*) from sub;

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

3
The number of distinct address data is same now and we start our analysis process.

SQL> select address,count(*) from main
2 group by address;

ADDRESS COUNT(*)
---------- ----------
NNNNNNN 1048576

The below update statement works very well for our scenario. But every time it updates for the 1048576 records.

All the time it updates the 1048576 records, even if there are less difference needs to update it in the table “Main” from “Sub”.

SQL> set timing on
SQL> set autotrace traceonly
SQL> update main m
2 set address = nvl((select address from sub s where m.id=s.id),-1);

1048576 rows updated.

Elapsed: 00:01:44.04
Execution Plan
----------------------------------------------------------
0 UPDATE STATEMENT Optimizer=CHOOSE
1 0 UPDATE OF 'MAIN'
2 1 TABLE ACCESS (FULL) OF 'MAIN'
3 1 TABLE ACCESS (FULL) OF 'SUB'

Statistics
----------------------------------------------------------
805 recursive calls
1073721 db block gets
2659 consistent gets
2124 physical reads
252412780 redo size
618 bytes sent via SQL*Net to client
582 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
6 sorts (memory)
0 sorts (disk)
1048576 rows processed


The statement took 01:44 minutes to complete and it generated the 252412780 bytes. We roll back the results.


SQL> rollback;

Rollback complete.

Elapsed: 00:02:26.09

Performance improved script:

The WHERE condition has added to update statement and it updates the records which is difference comparing with Table “Main” and “Sub”.

SQL> update main m
2 set address = nvl((select address from sub s where m.id=s.id),-1)
3 where address is null
4 or address <> nvl((select address from sub s where m.id=s.id),-1);

1048576 rows updated.

Elapsed: 00:01:42.05

Execution Plan
----------------------------------------------------------
0 UPDATE STATEMENT Optimizer=CHOOSE
1 0 UPDATE OF 'MAIN'
2 1 FILTER
3 2 TABLE ACCESS (FULL) OF 'MAIN'
4 2 TABLE ACCESS (FULL) OF 'SUB'
5 1 TABLE ACCESS (FULL) OF 'SUB'

Statistics
----------------------------------------------------------
536 recursive calls
1073630 db block gets
2611 consistent gets
1963 physical reads
252411468 redo size
627 bytes sent via SQL*Net to client
672 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
1048576 rows processed


The statement took 01:42 minutes to complete and it generated the 252411468 bytes. There is no big difference when it updates all the records.

SQL> select address,count(*) from main
2 group by address;

ADDRESS COUNT(*)
---------- ----------
-1 524288
A 262144
C 262144

Elapsed: 00:00:01.03
SQL> commit;

Commit complete.

Elapsed: 00:00:00.00

We change the some set of records to check the execution time.

SQL> update sub set address='M' where id =4;

1 row updated.

Elapsed: 00:00:00.02
SQL> commit;

Commit complete.

Elapsed: 00:00:00.00
SQL> set autotrace traceonly
SQL>
SQL> update main m
2 set address = nvl((select address from sub s where m.id=s.id),-1);
1048576 rows updated.

Elapsed: 00:01:51.00

Execution Plan
----------------------------------------------------------
0 UPDATE STATEMENT Optimizer=CHOOSE
1 0 UPDATE OF 'MAIN'
2 1 TABLE ACCESS (FULL) OF 'MAIN'
3 1 TABLE ACCESS (FULL) OF 'SUB'




Statistics
----------------------------------------------------------
624 recursive calls
1072662 db block gets
4953 consistent gets
2313 physical reads
248380252 redo size
629 bytes sent via SQL*Net to client
584 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
1048576 rows processed



The statement took 01:51minutes to complete and it generated the 248380252 bytes.


SQL> set autotrace off;
SQL> select address,count(*) from main
2 group by address;

ADDRESS COUNT(*)
---------- ----------
-1 524288
A 262144
M 262144

Elapsed: 00:00:00.09

Rollback the result.

SQL> rollback;

Rollback complete.

Elapsed: 00:04:40.08
SQL> select address,count(*) from main
2 group by address;

ADDRESS COUNT(*)
---------- ----------
-1 524288
A 262144
C 262144


Performance improved script:

SQL> set autotrace traceonly

SQL> update main m
2 set address = nvl((select address from sub s where m.id=s.id),-1)
3 where address is null
4 or address <> nvl((select address from sub s where m.id=s.id),-1);

262144 rows updated.

Elapsed: 00:00:45.04

Execution Plan
----------------------------------------------------------
0 UPDATE STATEMENT Optimizer=CHOOSE
1 0 UPDATE OF 'MAIN'
2 1 FILTER
3 2 TABLE ACCESS (FULL) OF 'MAIN'
4 2 TABLE ACCESS (FULL) OF 'SUB'
5 1 TABLE ACCESS (FULL) OF 'SUB'




Statistics
----------------------------------------------------------
176 recursive calls
268198 db block gets
2421 consistent gets
2054 physical reads
62104336 redo size
628 bytes sent via SQL*Net to client
672 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
262144 rows processed


The statement took 00:45.04 seconds to complete and it generated the 62104336 bytes.

SQL> set autotrace off;
SQL> select 248380252-62104336 redo_diff from dual;

REDO_DIFF
----------
186275916

The second update statement is avoiding the 186275916 bytes of the redo. So it is saving space in the UNDO and space in archive folders.

This method gives the good performance if it updates some part records in table.


Monday, July 19, 2010

Performance Improvement - Part 2: Hard parse is not bad

Performance Improvement - Part 2: Hard parse is not bad

In previous post, we have seen the soft parsing is doing less operation comparing with hard parsing.

Please have a look on below post before read this one.

http://karthikeyanbaskaran.blogspot.com/2010/07/performance-improvement-part-1-parsing.html

The Hard parse includes the parse, optimize, generation of the plan for the query and soft parse skips optimize and generation of the plan. So we decided that hard parse is not good for application performance.

Some times soft parsing is having performance issue. The below scripts proves that.
SQL@ORA10G> create table t
2 as
3 select case when rownum = 1
4 then 1 else 99 end id, a.*
5 from all_objects a;

Table created.

SQL@ORA10G> create index t_idx on t(id);

Index created.


SQL@ORA10G> analyze table t compute statistics;

Table analyzed.



The WHERE ID=1 condition will return one record and WHERE ID=99 will return all of the rest (about 40,000 records). Also, the optimizer is very aware of this fact, we can definitely see different plans for different inputs, as we are passing the literals in the WHERE clause.


SQL@ORA10G> set autotrace traceonly explain
SQL@ORA10G> select * from t where id = 1;

Execution Plan
----------------------------------------------------------


0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=2 Card=1 Bytes=141)
1 0 TABLE ACCESS (BY INDEX ROWID) OF 'T' (TABLE) (Cost=2 Card=1 Bytes=141)
2 1 INDEX (RANGE SCAN) OF 'T_IDX' (INDEX) (Cost=1 Card=1)

SQL@ORA10G> select * from t where id = 99;

Execution Plan
----------------------------------------------------------


0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=137 Card=34323 Bytes=4839543)
1 0 TABLE ACCESS (FULL) OF 'T' (TABLE) (Cost=137 Card=34323 Bytes=4839543)






From Oracle9i Database Release 1 through Oracle Database 10g Release 2, Oracle Database will wait until the cursor is opened to do the actual optimization of the query—it will wait for the bind variable value to be supplied by the application before figuring out the right way to optimize the query. This is called bind variable peeking, when the optimizer first looks at the bind values and then optimizes the query.



In this case, however, depending on which inputs are used to first run the query, the database will either choose a full scan or an index range scan plus table access by index rowid. And in Oracle9i Database Release 1 through Oracle Database 10g Release 2, that is the plan that will be used to execute the SELECT * FROM t WHERE ID = :ID query, regardless of the subsequent bind values, until the query is hard-parsed and optimized again.

SQL@ORA10G> exec :id := 99

PL/SQL procedure successfully completed.

SQL@ORA10G> select * from t where id = :id;

Execution Plan
----------------------------------------------------------

0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=137 Card=20391 Bytes=1814799)

1 0 TABLE ACCESS (FULL) OF 'T' (TABLE) (Cost=137 Card=20391 Bytes=1814799)





So we started off with ID=99 as the bind, and the optimizer chose a full scan. Therefore, regardless of the bind value, the database will execute a full scan from now on. For example:
SQL@ORA10G> exec :id :=1

PL/SQL procedure successfully completed.

SQL@ORA10G> select * from t where id = :id;

Execution Plan
----------------------------------------------------------

0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=137 Card=20391 Bytes=1814799)

1 0 TABLE ACCESS (FULL) OF 'T' (TABLE) (Cost=137 Card=20391 Bytes=1814799)




The above result demonstrates that it is not going for the index range scan or table access by index rowid. In this case, the logical I/Os (Consistent gets) are more because of the FULL table scan instead of the index scan.

The result proves the “poorly performing query”.

Thursday, June 10, 2010

Difference in Correlated Update

Difference in Correlated Update :



I came across the Correlated update statement with below format. It updates the address filed in the “Main” table and takes it from the “sub” table. The NVL function has given outside the sub query.

Outer NVL Query:
update main m
set address = nvl((select address from sub s where m.id=s.id),-1);

We can give the NVL function with the sub query (specifically for that column). Then what is the difference between “Outer NVL Query” and “Inner NVL Query”.
Inner NVL Query:
update main m
set address = (select nvl(address,-1) from sub s where m.id=s.id)

Initial setup:
This step creates the tables and populates it.

create table main ( id number, Address varchar2(10));

insert into main values (1,'NNNNNNN');
insert into main values (2,'NNNNNNN');
insert into main values (3,'NNNNNNN');
insert into main values (4,'NNNNNNN');

create table sub ( id number, Address varchar2(10));

insert into sub values (1,'A');
insert into sub values (2,null);
insert into sub values (4,'C');

select * from main;

select * from sub

Inner NVL Query:

update main m
set address = (select nvl(address,-1) from sub s where m.id=s.id);

It is applying the NVL function if NULL value comes from the “SUB” table. It is putting the NULL value in the “Main” table if it does not find the match in the “Sub” table.

We think that the outer NVL query works in the same way. But there is some difference.
Outer NVL Query:
update main m
set address = nvl((select address from sub s where m.id=s.id),-1)

We can see the difference in below result. It is applying the NVL function in the both the situation.

This is small Difference in the Correlated Update.

Wednesday, April 28, 2010

Get rows just I want



The Oracle latest version optimizers are working too smart. The comparison with oracle 9i and higher viersions.
I have a Query with an ORDER BY clause that returns 10,000 rows, but I'm restricting it to 500 rows with ROWNUM. Does the Oracle database store 10,000 rows in memory or open with 500 rows? For example:
SELECT empno, empnmFROM ( SELECT empno, empnm FROM employee ORDER BY empno )WHERE ROWNUM < 500;

Generally, the Oracle database does not store an entire result "in memory." Instead, the database tries to answer the query and return the first row before it gets the last row.
If the database attempted to store the result set in memory, we would never be able to ask a query like give more that one billion rows. Oracle generally answers the query on the fly whenever possible. In the above case—if EMPNO is indexed and EMPNO is NOT NULL—the optimizer will read the index to process the ORDER BY and stop after reading 500 rows.

Oracle 11g Result:

SQL_ORA11G> create table emp as select object_id empno, object_name ename from all_objects;
Table created.

SQL_ORA11G> select count(*) from emp;
COUNT(*)

----------
68503

SQL_ORA11G> create index emp_idx on emp(empno);
Index created.


SQL_ORA11G> analyze table emp compute statistics;
Table analyzed.


SQL_ORA11G> analyze index emp_idx compute statistics;
Index analyzed.


SQL_ORA11G> set autotrace traceonly
SQL_ORA11G> select empno, ename from ( select empno, ename from emp order by empno ) where rownum < 500;
Execution Plan
----------------------------------------------------------
Plan hash value: 2549860052
----------------------------------------------------------
Id Operation Name Rows Bytes Cost (%CPU) Time
----------------------------------------------------------
0 SELECT STATEMENT 499 14970 6 (0) 00:00:01

* 1 COUNT STOPKEY
2 VIEW 499 14970 6 (0) 00:00:01
3 TABLE ACCESS BY INDEX ROWID EMP 68503 1873K 6 (0) 00:00:01
4 INDEX FULL SCAN EMP_IDX 499 3 (0) 00:00:01

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

Oracle 9i Result:

The same script is working differently in the Oracle 9i. it is taking all the records and it is not stopping after it reaches 500. Oracle 9i reads all the data and filtering the 500 records.
SQL> create table emp as select object_id empno, object_name ename from all_objects;
Table created.


SQL> select count(*) from emp;
COUNT(*)

----------
29125

SQL> create index emp_idx on emp(empno);
Index created.


SQL> analyze table emp compute statistics;
Table analyzed.


SQL> analyze index emp_idx compute statistics;
Index analyzed.


SQL> set autotrace traceonly
SQL> select empno, ename from ( select empno, ename from emp order by empno ) where rownum < 500

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=194 Card=499 Bytes=873750) 1 0 COUNT (STOPKEY)
2 1 VIEW (Cost=194 Card=29125 Bytes=873750)
3 2 SORT (ORDER BY STOPKEY) (Cost=194 Card=29125 Bytes=786375)
4 3 TABLE ACCESS (FULL) OF 'EMP' (Cost=15 Card=29125 Bytes=786375)

We try to retrieve the data from table with out order by. So it will not use any index and it goes for the FULL table scan.

Oracle 9i:
In Oracle 9i, it reads the 29125 records and giving the 499 records to outside.

SQL> select empno, ename from emp where rownum < 500;
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=15 Card=499 Bytes=786375)
1 0 COUNT (STOPKEY) 2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=15 Card=29125 Bytes=786375)

Oracle 11g:
In Oracle 11g, it is going for the FULL table scan and it reads only 499 records but the actual count of the table is 68503. it is giving rows just I want.

SQL_ORA11G> select empno, ename from emp where rownum < 500;

Execution Plan
----------------------------------------------------
Plan hash value: 1973284518
----------------------------------------------------
Id Operation Name Rows Bytes Cost (%CPU) Time
----------------------------------------------------
0 SELECT STATEMENT 499 13972 3 (0) 00:00:01
* 1 COUNT STOPKEY
2 TABLE ACCESS FULL EMP 499 13972 3 (0) 00:00:01
----------------------------------------------------


Tuesday, February 23, 2010

Array Size Effects


The array size is the number of rows fetched (or sent, in the case of inserts, updates, and deletes) by the server at a time. It can have a dramatic effect on performance.
SQL> drop table t;

Table dropped.

SQL> create table t as select * from all_objects;

Table created.

SQL> select count(*) from t;
COUNT (*)

----------
29120
SQL> set autotrace traceonly statistics;
SQL> set arraysize 2
SQL> select * from t;

29120 rows selected.

Statistics
----------------------------------------------------------

14779 consistent gets

Note how one half of 29120 (rows fetched) is very close to 14779 , the number of consistent gets. Every row we fetched from the server actually caused it to send two rows back. So, for every two rows of data, we needed to do a logical I/O to get the data. Oracle got a block, took two rows from it, and sent it to SQL*Plus. Then SQL*Plus asked for the next two rows, and Oracle got thatblock again or got the next block, if we had already fetched the data, and returned the next two rows, and so on.
Next, let’s increase the array size:
SQL> set arraysize 5
SQL> select * from t;

29120 rows selected.

Statistics
----------------------

6152 consistent gets

Now, 29120 divided by 5 is about 5824, and that would be the least amount of consistent gets we would be able to achieve (the actual observed number of consistent gets is slightly higher).

All that means is sometimes in order to get two rows, we needed to get two blocks: we got the last row from one block and the first row from the next block.
Let’s increase the array size again:

SQL> set arraysize 10
SQL> select * from t;

29120 rows selected.

Statistics
----------------------

3271 consistent gets

SQL> set arraysize 25
SQL> select * from t;

29120 rows selected.

Statistics
----------------------

1551 consistent gets

SQL> set arraysize 100
SQL> select * from t;

29120 rows selected.


Statistics
----------------------

688 consistent gets

SQL> set arraysize 500
SQL> select * from t;

29120 rows selected.


Statistics
------------------

460 consistent gets
............
As you can see, as the array size goes up, the number of consistent gets goes down. So, does that mean you should set your array size to 5,000, as in this last test? Absolutely not. If you notice, the overall number of consistent gets has not dropped dramatically between array sizes of 100 and 5,000.

It would be better to have more of a stream of information flowing: Ask for 100 rows, get 100 rows, ask for 100, process 100, and so on. That way, both the client and server are more or less continuously processing data, rather than the processing occurring in small bursts.

Monday, February 15, 2010

Can we create table name and column name with case sensitive in oracle?

The table name and Column names CAN BE case sensitive in oracle, but are not by default. Bydefault, any way you create a table, upper case, lower case, mixed case, doesn't matter; everything will be forced to UPPER CASE. If you want it not to be UPPER CASE, you need to create the column with quotes around the name of the table or column and the case for the column that you specify will be kept.

The below script explains that very well.

The first one we see the table name with the case sensitive.

SQL> create table "Test" ( num number);

Table created.

We select the records from the table test with out any quotes. It is telling us that the table is not available.
SQL> select * from test;
select * from test
*
ERROR at line 1:
ORA-00942: table or view does not exist


SQL> select * from "Test";
no rows selected

The above query with the quotes is showing result. It proves that we can create a table with case sensitive.

Now we see the column name with the case sensitive.

SQL> create table t
2 as
3 select decode(mod(rownum,2),0,'M','F') as "gender",
4 all_objects.*
5 from all_objects
6 /

Table created.


SQL> create index t_idx on t (Gender,object_id);
create index t_idx on t (Gender,object_id)
*
ERROR at line 1:
ORA-00904: "GENDER": invalid identifier


SQL> desc t;
Name Null? Type
-------------- -------- ----------------------------
gender VARCHAR2(1)
OWNER NOT NULL VARCHAR2(30)
OBJECT_NAME NOT NULL VARCHAR2(30)

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

Index skip scan


We are using the B-Tree index and our predicate does not use the leading edge of index, in this case we might have a table T with an index on T(x,y). we query SELECT * from T WHERE x= 8.

The optimizer will allow to use the index since our predicate did not involve the column X. the optimizer would notice that it did not have to go to the table to get either X or Y. they are in the index. So it may very well option for the fast full index scan.

See below link for more information:
http://karthikeyanbaskaran.blogspot.com/2010/02/index-is-not-getting-used.html

The is another case whereby the index T(x,y) could be used by the CBO is during an index skip scan. The skip scan works well if and only if the leading edge of the index has very few distinct values and the optimizer understand that. Example, consider an idex on (GENDER, EMPNO) where GENDER has the values M and F.


SQL> create table t
2 as
3 select decode(mod(rownum,2),0,'M','F') as gender,
4 all_objects.*
5 from all_objects;

Table created.

SQL> create index t_idx on t(gender,object_id);

Index created.

SQL> begin
dbms_stats.gather_table_stats('SYSTEM','T');
end;
/

PL/SQL procedure successfully completed.

SQL> set autotrace traceonly explain
SQL> select * from t where object_id = 41;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=1 Bytes=94)
1 0 TABLE ACCESS (BY INDEX ROWID) OF 'T' (Cost=4 Card=1 Bytes= 94)
2 1 INDEX (SKIP SCAN) OF 'T_IDX' (NON-UNIQUE) (Cost=3 Card=1 )

The INDEX SKIP scan step tells us that Oracle is going to skip throughout the index, looking for the points where GENDER changes values and read down the tree from there.

Now we increase the count of the distinct value.

SQL> set autotrace off;
SQL> update t set gender = chr(mod(rownum,256));

29119 rows updated.

SQL> begin
2 dbms_stats.gather_table_stats('SYSTEM','T');
3 end;
4 /

PL/SQL procedure successfully completed.

SQL> set autotrace traceonly explain
SQL> select * from t where object_id = 41;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=41 Card=1 Bytes=94)
1 0 TABLE ACCESS (FULL) OF 'T' (Cost=41 Card=1 Bytes=94)

Thursday, February 11, 2010

Index is not getting used


We are using the B-Tree index and our predicate does not use the leading edge of index, in this case we might have a table T with an index on T(x,y). we query SELECT * from T WHERE x= 8 and see how the optimizer executes.

We see this in the below example.

create table test (x number, y number);
insert into test values ( 4,5);
insert into test values (1,3);
insert into test values (7,5);
insert into test values (3,6);

SQL> select * from test;

X Y
----- ----

4 5 1 3
7 5
3 6

SQL> create index test_idx on test(x,y);

Index created.

SQL> begin
2 dbms_stats.gather_table_stats('SYSTEM','TEST');
3 end;
4 /

PL/SQL procedure successfully completed.

SQL> set autotrace traceonly explain
SQL> select * from test where y=5;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1 Bytes=6)
1 0 TABLE ACCESS (FULL) OF 'TEST' (Cost=2 Card=1 Bytes=6)

The above query is going for the FULL table access since we have an index but the Y is not a leading edge of the index.


SQL> select * from test where x=3;

Execution Plan
----------------------------------------------------------

0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1 Bytes=6)
1 0 TABLE ACCESS (FULL) OF 'TEST' (Cost=2 Card=1 Bytes=6)


Now we try with both the conditions. The below example is having both X and Y in the where clause.

SQL> select * from test where x=3 and y=5;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1 Bytes=6)
1 0 TABLE ACCESS (FULL) OF 'TEST' (Cost=2 Card=1 Bytes=6)


Still it is not going for the index scan. The reason is that, the table is having the small number of records (4 records). The optimizer checks the best plan and goes for the FULL table access. This is proving us the FULL table access is not always bad.

“FULL table access is not always bad and indexes are not always good.”


We increase the records in the table.
SQL> set autotrace off;
SQL> insert into test select * from test;
8 rows created.

SQL> /
16 rows created.
....
...

..
.

SQL> /
16384 rows created.

SQL> commit;
Commit complete.


SQL> begin
2 dbms_stats.gather_table_stats('SYSTEM','TEST');
3end;
/
PL/SQL procedure successfully completed.

SQL> set autotrace traceonly explain
SQL> select * from test where x=3;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=8192 Bytes=49152)

1 0 INDEX (FAST FULL SCAN) OF 'TEST_IDX' (NON-UNIQUE) (Cost=4 Card=8192 Bytes=49152)

The optimizer will allow to use the index since our predicate did not involve the column X. the optimizer would notice that it did not have to go to the table to get either X or Y. they are in the index. So it may very well option for the fast full index scan. The index is smaller typically smaller than the underlying table. (This access path is always available with CBO only).
SQL> select * from test where y=6;

Execution Plan
----------------------------------------------------------

0 SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=10923 Bytes=65538)

1 0 INDEX (FAST FULL SCAN) OF 'TEST_IDX' (NON-UNIQUE) (Cost=4 Card=10923 Bytes=65538)


Now we try with both the conditions. The below example is having both X and Y in the where clause.
SQL> select * from test where x=3 and y=6;

Execution Plan
----------------------------------------------------------

0 SELECT STATEMENT Optimizer=CHOOSE (Cost=3 Card=2731 Bytes=16386)
1 0 INDEX (RANGE SCAN) OF 'TEST_IDX' (NON-UNIQUE) (Cost=3 Card=2731 Bytes=16386)

The explain plans shows that, it is going for the index scan.

Wednesday, February 3, 2010

Index scan gives the sorted record ?


Index scan gives the sorted record.
SQL> column plan_plus_exp format a100
SQL> set trimspool on
SQL> create table t nologging as select * from dba_objects;

Table created.

SQL> set time on
SQL> set timing on
SQL> create index t_idx on t(owner,object_id) nologging compute statistics;

Index created.

Elapsed: 00:00:00.03
SQL> select count(*) from t;
COUNT(*)
----------
29515
Elapsed: 00:00:00.00

SQL Query 1:
SQL> set autotrace on
18:07:31 SQL> select rownum' 'owner' 'object_id from t where owner='SYS' and rownum < 11;

optimizer="CHOOSE" cost="5" card="10" bytes="18581)" cost="5" card="1093" bytes="18581)"


SQL Query 2:
SQL> select * from (select rownum' 'owner' 'object_id from t
2 where owner='SYS'
3 order by owner,object_id ) where rownum < 11;


optimizer="CHOOSE" cost="5" card="10" bytes="63394)" cost="5" card="1093" bytes="63394)" cost="5" card="1093" bytes="18581)"
The first SQL and second SQL are also giving the same result with same statistics. First SQL may not work as expected in future if I drop index obviously it will not.
But RULES OF THE GAME:
Rule #1: if you want, expect, or need sorted data, there is exactly one way to achieve that in a relational database.

"You must use order by"

Rule #2: please re-read #1 until you believe it.

The first query says "give me a random set of 10 rows for the SYS owner"

The second says "give me 10 rows for the sys owner starting at the smallest object_id in sorted order"


Monday, August 24, 2009

TRUNCATE Vs Index Status

Comments: The Redo size of the current session

SQL> select name,a.value
from v$sesstat a, v$sysstat b
where b.statistic#=a.statistic#
and b.name = 'redo size' and sid = 16;
NAME VALUE
-------------- ----------
redo size 32404

Comments: create the CTAS table with NOLOGGING.

SQL> create table t nologging as select * from all_objects where 1=0;

Table created.

SQL> select name,a.value
from v$sesstat a, v$sysstat b
where b.statistic#=a.statistic#
and b.name = 'redo size' and sid = 16;

NAME VALUE

-------------- ----------
redo size 69912


Comments: create the index with NOLOGGING option.

SQL> create index object_name_idx on t(object_name) nologging;

Index created.

SQL> select name,a.value
from v$sesstat a, v$sysstat b
where b.statistic#=a.statistic#
and b.name = 'redo size' and sid = 16;

NAME VALUE

-------------- ----------
redo size 87920


Comments: Insert the records into table with the append hint. It will not create any Redo for the table data but it will create a Redo for the index. Check the Redo size and It is high.

SQL> insert /*+ append */ into t select * from all_objects;

27196 rows created.

SQL> select name,a.value
from v$sesstat a, v$sysstat b
where b.statistic#=a.statistic#
and b.name = 'redo size' and sid = 16;


NAME VALUE

-------------- ----------
redo size 4302432


Comments: Make the index as UNUSABLE state.

SQL> alter index object_name_idx unusable;

Index altered.

SQL> select name,a.value
from v$sesstat a, v$sysstat b
where b.statistic#=a.statistic#
and b.name = 'redo size' and sid = 16;

NAME VALUE

-------------- ----------
redo size 4305028

Comments: Check the status of the index and truncate the table data.

SQL> select index_name, status from user_indexes where table_name = 'T';INDEX_NAME STATUS
------------------------------ --------
OBJECT_NAME_IDX UNUSABLE


SQL> truncate table t;

Table truncated.

Comments: Insert the records into table with the append hint.

SQL> insert /*+ append */ into t select * from all_objects;

27196 rows created.


Comments: Now we check the Redo generation. Still it is high and it has generated the Redo for the index because the TRUNCATE operation has made that index as VALID.


SQL> select name,a.value
from v$sesstat a, v$sysstat b
where b.statistic#=a.statistic#
and b.name = 'redo size' and sid = 16;


NAME VALUE

-------------- ----------
redo size 8529448


Comments: check the current status of the index.

SQL> select index_name, status from user_indexes where table_name = 'T';
INDEX_NAME STATUS
------------------------------ --------
OBJECT_NAME_IDX VALID

Saturday, August 22, 2009

NOLOGGING Vs Redo entries.


Title: NOLOGGING Vs Redo entries.
Comments: This document is related to the redo log generation of the scripts in LOGGING mode and the NOLOGGING mode.

Checking the redo generation for table creation:
Ø Checking the record count and the current Redo log size of the session.

SQL> select count(*) from all_objects;



COUNT(*)
----------
27189

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 0
1 row selected.
Ø Creating the table CTAS (Create Table As Select) table with the NOLOGGING mode. It is not creating the Redo logs for the data.

SQL> create table t NOLOGGING as select * from all_objects;

Table created.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 56732

Ø Creating the table CTAS table with the LOGGING mode. It is creating the Redo logs for the data.

SQL> create table tt as select * from all_objects;
Table created.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 3176828
1 row selected.
Ø Creating the table CTAS table with the NOLOGGING mode (only table structure) and INSERT the records. It is creating the Redo logs for the data.

SQL> create table ttt nologging as select * from all_objects where 1=2;
Table created.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;
NAME VALUE
------------ ----------
redo size 3212880
1 row selected.


SQL> insert into ttt
2 select * from all_objects;

27192 rows created.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 6257108

1 row selected.

Ø Creating the table CTAS table with the LOGGING mode (only table structure) and INSERT records with /*+ APPEND */ Hint. The Redo logs are not generated for the data.

SQL> create table tttt nologging as select * from all_objects where 1=2;
Table created.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 6293312

1 row selected.
SQL> INSERT /*+ APPEND */ INTO tttt select * from all_objects;
27193 rows created.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 6311384
1 row selected.


Redo Log generation table:
The Below table shows when the log generates.




----------- ------------- --------------- ---------------
Checking the redo generation for indexes:
Ø Drop the Redo_check table.

SQL> drop table redo_check;
Table dropped.


Ø Checks the current redo size of the session.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 25016

Ø Create a table and insert the records.

SQL> create table redo_check (
2 num number,
3 data1 varchar2(20)
4 );

Table created.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 32460

SQL> declare
2 i int;
3 begin
4 for i in 1 .. 200000
5 loop
6 insert into redo_check values(i,'Redo check ' i);
7 end loop;
8 end;
9 /

PL/SQL procedure successfully completed.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 49555748

Ø Create the index with NOLOGGING mode.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 49555860
1 row selected.
SQL> create index dat_idx on redo_check (data1) nologging;
Index created.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 49633080

1 row selected
.

Ø We try to update the index column and check the Redo generation.




SQL> update redo_check set data1 = ' redo check' where num <100001 em="">
Ø Yes. The updates are generating the Redo logs for the table entry and index if we create a table in the NOLOGGING mode also.

SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 127536500

1 row selected.SQL> commit;Commit complete.SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 127536592

1 row selected.

Ø We drop the index and try to update the records.

SQL> drop index dat_idx;
Index dropped.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 127548252

1 row selected.
SQL> update redo_check set data1 = ' redo check' where num < 100001;
Ø Now it has created the redo only for the table entry.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 152859364

1 row selected.
Ø After that creating the index with NOLOGGING. This is one of the best ways to increase the performance of the scripts.
SQL> create index dat_idx on redo_check (data1) nologging;
Index created.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 152906760

1 row selected.SQL> commit;
Ø Now we check the redo generation with unusable the index and rebuilding the index way.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 0
Ø Make the index as an Unusable and update the records in the table.
SQL> alter index dat_idx unusable;
Index altered.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 2224

SQL> update redo_check set data1 ='reupdate' where num < data1 ="'reupdate'">
Ø The update statement is giving the error because the index is in the unusable state. To void this error message alter the current session with below statement.
SQL> alter session set skip_unusable_indexes=true;
Session altered.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 2876

Ø Execute the update statement now and check the redo log. It has created the redo generation only for the table entries.
SQL> update redo_check set data1 ='reupdate' where num < 100001;>SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;

NAME VALUE
------------ ----------
redo size 24896420

Ø Rebuild the index and if we check the redo generation. It did not create any redo for the index entries.

SQL> alter index data1_idx rebuild;
Index altered.
SQL> select name,a.value
2 from v$sesstat a, v$sysstat b
3 where b.statistic#=a.statistic#
4 and b.name like 'redo size' and sid = 9;


NAME VALUE
------------ ----------
redo size 24934104




Indexes on two different table space( Associated to different schema)


Question: Can we create indexes on two different table spaces. ( Associated to different schema)

Comments: The answer is “Yes”. We can create a table in one table space and index in another table space.




Now the question is some thinking different. Can we create an index on some other user’s table space (Demo2_TS) which is not associated to the current user (Demo2_TS)? (Where table has created)

Scripts:
sql> conn demo1/demo1 ;
sql> create table table_in_demo1 ( col1 number, col2 number );
sql> insert into table_in_demo1 values (10,11);
sql> insert into table_in_demo1 values (20,21);
sql> insert into table_in_demo1 values (30,31);
sql> select * from table_in_demo1;

COL1 COL2
----- ------
10 11
20 21
30 31

sql> grant all on table_in_demo1 to demo2
sql> conn demo2/demo2
sql> select * from demo1.table_in_demo1
COL1 COL2
------ -----
10 11
20 21
30 31

Comments: Now the Demo2 is having the access to “table_in_demo1”. Can we create a index on table_in_demo1 table from DEMO2?

The table is available in the Demo1_TS table space. Will it allow to create a index in the
Demo2_TS. (The demo2 is associated with the Demo2_TS).

Let’s check in the Demo2 schema

Scripts:
sql> conn demo2/demo2
sql> create index tab_idx on demo1.table_in_demo1(col2);

sql> select owner,index_name,table_owner from all_indexes
where table_name = 'TABLE_IN_DEMO1';


Solution:
It is allowing programmer to create an index in Demo2 schema.

sql> conn demo1/demo1
sql> select * from table_in_demo1 where col2 = 21;

Explain plan from Demo1 schema:



So,
We can create an index on some other user’s table space (Demo2_TS) which is not associated to the current user (Demo1_TS)
Comments:
One more question. What will happen if we revoke the permission from Demo1 schema?

No. Two more question because of the above question.

Question 1: will it allow programmer to revoke the permission?
Question 2: if it allows programmer to revote, will it use the query use that index?

Answer for Question 1:sql> conn demo1/demo1
sql> revoke all on table_in_demo1 from demo2;

Yes. It is allowing programmer to revoke.

Answer for Question 2:
sql> select * from table_in_demo1 where col2 = 21;
Explain plan from Demo1 schema:
Wow! What a surprise! Still it is using Demo2 index.

We try to insert one record.

sql> conn demo1/demo1
sql> insert into table_in_demo1 values (40,41);
sql> select * from table_in_demo1;

COL1 COL2
----- ------
10 11
20 21
30 31
40 41

Yes. It is allowing programmer to insert.

Friday, August 21, 2009

UNION = OR?

Question: Are UNION and OR doing to same or different?

Comments: We check with some data.

Scripts:

create table test_tab ( num1 number, num2 number );
insert into test_tab values(10,15);
insert into test_tab values(20,23);
insert into test_tab values(17,20);
commit;

select * from test_tab;
NUM1 NUM2
---------- ----------
10 15
20 23
17 20

Result of OR:
select * from test_tab where num1 = 10 or num2 = 20;

NUM1 NUM2
---------- ----------
10 15
17 20

Result of UNION:
select * from test_tab where num1 = 10
union
select * from test_tab where num2 = 20;

NUM1 NUM2
---------- ----------
10 15
17 20

Conclusion:
Wow! The result of Union and OR clause is same.
So
UNION=OR
Do not come to conclusion with above result. Check with some other scenario.
Question: UNION and OR will give same result ?

Comments: check with some other data.

Scripts:

drop table test_tab;create table test_tab
(
num1 number,
num2 number
);
insert into test_tab values ( 1,1);
insert into test_tab values ( 1,1);

Result of OR:

select * from test_tab where num1 = 1 or num2 = 1;
NUM1 NUM2
---------- ----------
1 1
1 1

Result of UNION:

select * from test_tab where num1 = 1
union
select * from test_tab where num2 = 1;
NUM1 NUM2
---------- ----------
1 1

Result of UNION ALL:
select * from test_tab where num1 = 1
union all
select * from test_tab where num2 = 1;

NUM1 NUM2
---------- ----------
1 1
1 1
1 1
1 1

Conclusion:
Yes! The result of Union and OR clause is not same.

So
UNION<>OR

Index scan on Null able column

Issue: Tune the below SQL script.

Comments: The statement is having the two conditions in the WHERE clause. One it is checking the number and another one is checking the NULL value.
Scripts:
select * FROM CUSTOMER_ORDER_TUNE WHERE NAME = '0000003836' or NAME IS NULL
Explain plan: It is going for the TABLE ACCESS FULL.
PlanSELECT STATEMENT CHOOSE Cost: 156 Bytes: 81 Cardinality: 1
1 TABLE ACCESS FULL SLSFORCE.CUSTOMER_ORDER_TUNE Cost: 156 Bytes: 81 Cardinality: 1

Comments: We know that, we have to create index on the “NAME” field.
create index NAME_IDX on CUSTOMER_ORDER_TUNE(NAME)

Comments: Now we check the explain plan for the script.
select * FROM CUSTOMER_ORDER_TUNE WHERE NAME = '0000003836' or NAME IS NULL
Explain plan: Still it is going for the TABLE ACCESS FULL because the script is checking the NULL.

Plan
SELECT STATEMENT CHOOSE Cost: 156 Bytes: 81 Cardinality: 1
1 TABLE ACCESS FULL SLSFORCE.CUSTOMER_ORDER_TUNE Cost: 156 Bytes: 81 Cardinality: 1


Comments: Let we divide the SQL statement as a two part and we check the explain plan.

select * FROM CUSTOMER_ORDER_TUNE WHERE NAME = '0000003836'
union all
select * FROM CUSTOMER_ORDER_TUNE WHERE NAME IS NULL

Explain plan: Good! Now partially it is going for the index scan. The second part of the query is going for the FULL TABLE access. The reason is, because the NAME is null able, the index on only on name and entirely null keys are NOT entered into b*tree indexes.

Plan
SELECT STATEMENT CHOOSE Cost: 158 Bytes: 162 Cardinality: 2
4 UNION-ALL
2 TABLE ACCESS BY INDEX ROWID SLSFORCE.CUSTOMER_ORDER_TUNE Cost: 2 Bytes: 81 Cardinality: 1
1 INDEX RANGE SCAN NON-UNIQUE SLSFORCE.NAME_IDX Cost: 1 Cardinality: 1
3 TABLE ACCESS FULL SLSFORCE.CUSTOMER_ORDER_TUNE Cost: 156 Bytes: 81 Cardinality: 1



Comments: Still it is not going for the index scan. So we drop the index and create it on different way.
drop index NAME_IDX
create index NAME_IDX on CUSTOMER_ORDER_TUNE(NAME,0)

Comments: Now we check the explain plan for the script.

select * FROM CUSTOMER_ORDER_TUNE WHERE NAME = '0000003836'
union all
select * FROM CUSTOMER_ORDER_TUNE WHERE NAME IS NULL

Explain plan: Wow! Now it is going for the index scan and cost also very less.

Plan
SELECT STATEMENT CHOOSE Cost: 4 Bytes: 162 Cardinality: 2
5 UNION-ALL
2 TABLE ACCESS BY INDEX ROWID SLSFORCE.CUSTOMER_ORDER_TUNE Cost: 3 Bytes: 81 Cardinality: 1
1 INDEX RANGE SCAN NON-UNIQUE SLSFORCE.NAME_IDX Cost: 2 Cardinality: 1
4 TABLE ACCESS BY INDEX ROWID SLSFORCE.CUSTOMER_ORDER_TUNE Cost: 1 Bytes: 81 Cardinality: 1
3 INDEX RANGE SCAN NON-UNIQUE SLSFORCE.NAME_IDX Cost: 1 Cardinality: 1