SQL performances : when several finely tuned statements sum up to bad programming

When performance issues arise and when, after rounding up the usual suspects, you begin tracing your programs, you usually end up pouring over tkprof output, looking for full scans of large tables and trying to individually tune each SQL statement. This usually yields good results, but it is a far cry from the complete picture.

The typical example to illustrate the point, is the often encountered update or insert-if-not-found case, applied to a table which has a suitable primary key defined.

The PL/SQL beginner will write it the following way :

select count(*)

into v_count

from my_table

where id = v_id;

if (v_count = 0)

then

insert into my_table(id, ...)

values (v_id, ...);

else

update my_table

set ...

where id = v_id;

end if;

Taken individually, each one of the three statements is perfect. However, a seasoned Oracle user will probably wonder why, in the case of the update, we are scanning the primary key index twice, once to count occurrences, and a second time to find the data block address in order to be able to do the update.

Therefore, our experienced user will rather write the following :

begin

-- Get the address of the block to update

select rowid

into v_rowid

from my_table

where id = v_id

for update of ..;

-- Do the update

update my_table

set ...

where rowid = v_rowid;

exception

when no_data_found then

insert into my_table(id, ...)

values (v_id, ...);

end;

All this is fine, but how can we actually prove that one method is better than the other? A smart way to measure statement efficiency is to check how many buffers are accessed to process it; much more than CPU time or any such ‘classical’ value it gives you a fairly everything-else-independent quantity (although physical disorganization, such as row-chaining, would blur the picture and somehow show up as the number of block buffers would be higher than normal.). These values are given in columns ‘query’ and ‘current’ in the tkprof output, but one can also get them easily by running a simple script such as bufgets.sql :

select sum(s.value) buffers_accessed

from v$mystat s,

v$statname n

where n.name in ('db block gets', 'consistent gets')

and n.statistic# = s.statistic#

/

before the statement to test, run the statement, run bufgets.sql once again and compute the difference.

The two competing PL/SQL codes have been benched on a 50,000 rows table (454 Oracle blocks), with a single index (primary key).

In both cases, the processing of a new key ‘costs’ 8 blocks. By measuring independently the INSERT statement, we can determine that the (in fact similar) unsuccessful SELECTs cost 4 in both cases, and the INSERT 4 as well. However, the beginner’s code will cost 13 when processing an already existing key, and the old Oracle hand’s one only 7. Assuming that 10% of values are new ones, the scores for 100 lines will be :

Beginner : 90 x 13 + 10 x 8 = 1250

Old timer : 90 x 7 + 10 x 8 = 710, or almost 2 times as efficient, although the beginner has done no obvious mistake...

As exceptions begin to pop-up, one can wonder why we bother to SELECT first, and whether it would not be more efficient to let Oracle do most of the job itself. We would then have two cursors instead of three, which should be a minor gain both in terms of SGA memory consumption and parsing. But here again, we can write it in two different ways :

update my_table

set ...

where id = v_id;

if SQL%ROWCOUNT = 0

then

insert into my_table(id, ...)

values (v_id, ...);

end if;

or

begin

insert into my_table(id, ...)

values (v_id, ...);

exception

when dup_val_on_index then

update my_table

set ...

where id = v_id;

end;

Intuitively, one may think that if we have more rows to update, the first case will be better, and if we have more rows to insert, the second will execute faster. In fact, this is more than half wrong.

Let’s use our ‘buffer access’ cost :

If we try insert first :

New key : cost = 4 blocks

Already existing key : cost = 22 blocks

If we try update first :

New key : cost = 8 blocks

Already existing key : cost = 6 blocks

In fact, the most efficient way is to update, and insert if not found, even if

odds are 50/50. The reason is that the 'duplicate value' event is

detected only after having inserted the index(es), which is done AFTER

having inserted the row (and updated the rollback segments), whereas

before updating you must search the index and immediately see if the value is

here or not - in other words, the insert generates a ‘post’ exception, whereas the update generates a ‘pre’ exception (kind of).

Here is what happens in both cases :

Try insert first :

a) Update the data block, write the rollback segment, update

index(es), write the rollback segment.

If the data already exists, it is likely that all the indexes

will not be updated, although we have no certainty that unique indexes

are inserted first. We can be pretty sure that some internal rollback will

take place somewhere. Then you will have to switch to the update, which

means :

b) searching the index once again (not very likely that Oracle

will keep track of the rowid of the existing line), update the

datablock, write the rollback segment, and possibly update some indexes

as well.

Try update first :

Search the index. If found, same processing as b) above. If not

found, same processing as a). Much more straightforward.

If we once again compute the cost for 100 rows of which 10 are new :

Insert first :

90 x 22 + 10 x 4 = 2020,

or

almost twice as much as the unsuspecting beginner...

Update first :

90 x 6 + 10 x 8 = 620,

or

more than 10% better than our previous best case...

It could be slightly different of course in a real-life case with more indexes.

However if you wish to find out how effective "insert first" is the number of new lines is the key. So how many new lines equates to the cost factor.

We can compute the resultant cost factor by:

100 - new_lines) x 22 + 4 x new_lines < (100 - new_lines) x 6 + 8 x new_lines

1600 < 20 x new_lines

As a result we now have our rule of thumb:

The insert first becomes less costly only if

you have 80% of rows to insert

against 20% of rows to update.

So much for intuition...

A conclusion ? Watch not only your statements, but your algorithms too!