So, you've assembled a large staff of ORACLE Forms and Reports developers to complete
the physical model development of a large scale system. The functional requirements
require you to meet certain benchmarks of performance or process large amounts of
data.
It's not surprising at all to find that most ORACLE developers know very little about
applications level performance tuning. There is quite a bit that the developer can do
to increase the performance of the code he writes
This paper discusses in basic terms the main tools available to the ORACLE
applications developer and how they can be utilized to tune applications for increased
performance. It outlines steps to take when a bottleneck is found and how these tools
can help identify potential problems before they eat up valuable development
resources. The main intent of this paper is not to provide an exhaustive description
of each of these tools, but rather to provide a practical step by step approach to
applications performance tuning which can be easily adopted by the average ORACLE
applications developer with as little distraction from his or her main objective
(developing the software) as possible. This paper explains, in general terms, how the
ORACLE Optimizer works; it explains how tools such as Explain Plan and ORACLE Hints
can be used to manipulate the Optimizer; it provides guidelines to follow when setting
up indexes; and it identifies and explains the different approaches to query
optimization.
The Optimizer
Any attempt to solve performance problems within your applications must start with an
understanding of the ORACLE Optimizer and its role in increasing performance. When a
SQL SELECT, UPDATE, INSERT or DELETE statement is processed, ORACLE must access the
data referenced by the statement. The Optimizer portion of ORACLE is used to determine
the most efficient path to reference the data. The optimizer formulates execution
plans and attempts to choose the best plan for executing a statement.
In ORACLE version 7.xx and up, the optimizer uses one of two techniques to formulate
execution plans for SQL statements: a cost-based approach or a rule-based approach.
The cost-based approach generally chooses an execution plan which is as good as, or
better than, the plans chosen by the rule-based approach, especially for large queries
with multiple joins or multiple indexes. The cost-based approach also improves
productivity by eliminating the need for you to manually tune your SQL statements
yourself; meaning that you need not be concerned with the order of the tables in the
WHERE clause or turning on or off indexes by modifying the columns in the WHERE
clause. Why then have a rule-based approach? The rule-based approach exists mainly to
accommodate code written in versions of ORACLE earlier than version 7. In addition,
problems with the cost-based optimizer in version 7.1 and 7.2 have made it necessary
to use the rule-based approach as a work-around in some cases. It is important though
to keep in mind that the rule-based approach will be eliminated in future versions of
ORACLE.
If your version of ORACLE supports cost-based optimization and you have no specific
reason to use the rule-base approach, I recommend that you do not use it. However, the
way the rule-based optimizer works is as follows. The optimizer uses a set of rules to
determine the best execution plan for a SQL statement. The execution plan is derived
by the way the tables are arranged in the WHERE clause, the availability of indexes to
be used along with a few general rules applied across the board. These rules will
cause the optimizer to choose the same query execution plan regardless of the
underlying data distribution. For example, if the optimizer finds an index on a table,
it will use it even if using the index will not eliminate a majority of the rows, thus
slowing the performance of the statement. When using the rule-based approach, the
developer of the query would need to explicitly turn off the index by modifying one of
its columns in the WHERE clause of the statement (or by some other means), or suffer
slower than necessary performance. And if the distribution of the underlying data in
the tables changed such that the index was needed, the developer would have to go back
and change the statement accordingly.
No matter which optimization approach is used, the optimizer�s objective is the same;
to determine the best path for accessing the data. This is very important because the
performance implications are virtually unlimited. It may, for instance take several
minutes to do a full table scan on a table with hundreds of thousands of rows. If the
optimizer knows to use a unique index on the table, a single row can instead be
retrieved almost instantly. The savings in retrieval time would be several minutes.
That may not sound like much, but if you�re the one sitting behind the keyboard
waiting for a response, it could be very significant. That is however the very tip of
the iceberg. Let�s say this hundred thousand row table is now being joined to another
table which has one hundred thousand rows in it as well. If the optimizer knows to use
the unique index on this table the result is still retrieved almost instantly. If it
does not know to use the index and instead chooses to do a full table scan on this
table for every row in the second table, the result could take hours if not days. Add
joins to a few more similar tables and without a good query optimization path, the
results may never come back.
How does the cost-base Optimizer know what path to choose? The cost-based optimizer
uses statistics such as the number of records in the table and the distribution and
selectivity (discussed later) of the data in the tables to help it determine the
optimal query execution plan. Statistics must therefore be generated for all tables
and indexes accessed by your SQL statements before using the cost-based approach
(generating statistics for tables automatically gathers statistics for the indexes on
that table). If the size and data distribution of these tables changes frequently, you
should generate these statistics on a regular basis to ensure they accurately
represent the data in the tables. ORACLE can generate statistics using the following
techniques.
* Estimation based on random data sampling.
* Exact computation.
Since collecting statistics on a table prevents SELECT, INSERT, UPDATE, and DELETE
statements from accessing the table during the time the statistics are being
calculated, estimation (which is much faster) is especially useful. It minimizes the
time the tables are unavailable. Exact computation should be used when estimation
produces skewed results.
The following example statement generates statistics for the PERSON table and its
indexes:
ANALYZE TABLE PERSON ESTIMATE STATISTICS SAMPLE 20 PERCENT;
The ORACLE Optimizer will use the cost-based approach to optimization only if
statistics exist for the tables accessed by the statement and if the OPTIMIZER_GOAL
session parameter is set to a cost-based value such as CHOOSE. (This is generally the
default setting.) If these conditions are not met, the optimizer will revert to
optimizing the statement by a set of rules which do not take the actual number of rows
in the tables or the distribution of the data into consideration (rule-based approach).
You can also enable cost-based optimization at the session and statement level. To
enable cost-based optimization for your session only, issue the ALTER SESSION
statement with the OPTIMIZER_GOAL parameter value of CHOOSE, ALL_ROWS or FIRST_ROWS.
To enable cost-based optimization for an individual SQL statement, use the ALL_ROWS or
FIRST_ROWS hint (discussed later).
So, the first step to performance tuning the application is to make sure you have
enabled the cost-based approach to query optimization and verify that statistics on
the target tables are up-to-date. If for some reason, the cost-base approach is not
available to you, then you need to write the SELECT, UPDATE, INSERT or DELETE
statement in such a way that the best optimization path is available to the rule-based
optimizer (techniques for doing this are discussed later).
The next step is to choose the appropriate goal for the cost-based optimizer�s
execution plan. The execution plan produced by the optimizer can very greatly
depending on the optimizer�s goal. The types of joins that the optimizer chooses can
have a great impact on the way the results are returned. For example, optimizing for
best throughput, (or the minimum time to return all rows accessed by the statement) is
likely to result in a full table scan rather than an indexed scan. It would also more
likely result in a sort-merge join rather than a nested loops join. Optimizing for the
best response time, (or the minimum time to return the first row accessed by the
statement) is more likely to result in an indexed scan or a nested loops join. This is
because sort-merge join operations may return the entire query result faster, while
the nested loops operation may return the first row faster. If the goal is best
throughput, the optimizer is more likely to choose the sort-merge join. If the goal is
best response time, the optimizer is more likely to choose a nested-loops join. It is
therefore desirable to choose a goal for the optimizer based on the needs of the
application. In general, if the statement is to return rows to the screen, response
time would be most important because the user can proceed once the first row has been
returned to the screen. However, if the statement returns rows to a report, throughput
would be generally be more important because the report would be unavailable until all
rows were retrieved anyway. By default, the cost-based approach optimizes for best
throughput.
The goal of the cost-based approach can be set at the session or statement level. To
change the goal of the cost-based approach for all SQL statements in your session,
issue the ALTER SESSION statement with the OPTIMIZER_GOAL parameter. To specify the
goal of the cost-based approach for an individual SQL statement, use the ALL_ROWS or
FIRST_ROWS hint (discussed later).
Indexing
It has been my experience that the solution to the vast majority of performance
problems boils down to creating indexes or making existing indexes visible or
accessible to the optimizer. Without a doubt, a few well placed indexes can produce
the greatest improvement in application performance, especially when the volume of
data is high. More than a few applications programmers have spent hours and hours
reengineering database structure or their code, or messing with system parameters in
an attempt to improve application performance only to find out that the problem was
that the optimizer was not using the indexes which they thought they were. It is
important to understand when to use indexes and when not to use indexes.
Indexes should generally be used when the statement will result in a small percentage
of the total number of rows in the table being returned. In this case, the statement
is said to have good selectivity. For example, if I am selecting all of the employees
in the EMPLOYEE table with a particular social security number or name, since the
number of rows in the table with a particular social security number or name is
expected to be low, processing the statement using an index on the social security or
name field would undoubtedly improve performance. If, on the other hand, I was
selecting all of the EMPLOYEES who are female, I would probably not want to use an
index since the number of rows which meet the criteria would be pretty high (probably
around 50 percent). It is important to understand that processing a statement through
an index takes overhead. The more records processed through the index, the more
overhead incurred. The point at which the overhead outweighs the benefit of the index
varies, but is generally accepted to be between 15 and 25 percent. That is, if the
statement retrieves more than 15 to 25 percent of the rows in the table, it will
probably be cheaper to do a full table scan rather than using the index. In this case,
the statement is said to be not very selective (bad selectivity).
The next step in the process of resolving your performance problem should then be to
make sure that indexes are placed on the appropriate tables and that the statement is
actually using the indexes. The ORACLE optimizer will decide whether or not to use an
index which is available to it, but your job is to make sure that the index is indeed
available to the optimizer for the given statement. In addition to creating the
indexes and making them visible to the optimizer, you must also make sure that you
don�t inadvertently make them unavailable to the optimizer. For example, the optimizer
cannot use the index if the columns in the index are modified in the WHERE clause.
Remember, to avoid the following when using indexes, because they can make indexes
unavailable to the optimizer.
* Avoid modifying the indexed columns in the WHERE clause.
* Avoid using NOT, !=. <> or || in the WHERE clause.
* Avoid using calculations on indexed columns in the WHERE clause.
* Avoid using columns which contain NULLs in your indexes.
In addition, do the following whenever possible.
* Remember to use the leading portion of composite indexes (e.g. if your index is made
of more than one column, and you are only using part of the index, make sure the part
you use is in the leading portion of the index).
* Use UNION instead of OR.
* Remember to consider internal conversions. (For example, if a numeric column is
compared to an alphanumeric column, the alphanumeric column will automatically be
converted to numeric by ORACLE, thus preventing the optimizer from being able to use
the column in an index.)
Explain Plan Tool
Once you have verified that indexes are in place and that to the best of your
knowledge, they are available to the optimizer to use, the next step is to see what
the optimizer is really doing. This can be done by using the Explain Plan tool. The
Explain Plan tool will allow you to see the exact execution plan which the optimizer
will choose when executing your statement. This lets you see if the indexes you expect
to be used are indeed being used. It can also give you some idea of how long the
statement will take to execute.
The sample file below contains the statements necessary to use the Explain Plan to
evaluate ORACLE statements. To use it, replace �USER_NAME� with something unique which
identifies you. Next, paste the SQL statement which you are evaluating in the file
where indicated. Then execute the file from the SQL prompt and observe the output.
EXPLTEST.sql
delete from plan_table
where statement_id = 'USER_NAME'
/
explain plan set statement_id = 'USER_NAME'
into plan_table for
(Place your SQL statement here)
/
select lpad(' ',1*level)||operation||'('||options||')'||object_name||' '||object_type
results
from plan_table
connect by prior id = parent_id
and statement_id = 'USER_NAME'
start with id = 1
and statement_id = 'USER_NAME'
/
Below is a sample SQL statement which selects backorder quantities for stock in
inventory.
SELECT I.STOCK_NBR, B.BACKORD_QTY
FROM INVENTORY I, BACKORDER B
WHERE I.INVENTORY_ID = B.INVENTORY_ID
AND I.INVENTORY_ID = 123
/
Consider the following output on the above SQL statement which came from the Explain
Plan tool. It tells me that a nested loops join is being done to link the two tables.
It tells me that a full table scan is being done on the INVENTORY table and the
records returned form that table are being joined via a range scan to the BACKORDER
table�s BACKORDERS_FOR_FRGN index. The BACKORDERS_FOR_FRGN index then links by ROWID
to the actual BACKORDER table (this is necessary because the column being returned
from the BACKORDER table is not in the index).
NESTED LOOPS()
TABLE ACCESS(FULL)INVENTORY
TABLE ACCESS(BY ROWID)BACKORDER
INDEX(RANGE SCAN)BACKORDERS_FOR_FRGN NON-UNIQUE
It would be a good idea to spend some time learning to understand the output from the
Explain Plan tool. The value in doing this obviously increases the more bottlenecks
you have to fix, however, even if you are not sure at first how much you will need it,
I still strongly recommend that you take the time. Since explaining all of the ins and
outs of the Explain Plan tool�s output could easily be the subject of an entire paper,
and since it was not my intent to get to that level, I will just touch on a few of the
basics.
In general, full table scans are your most glaring place for improvement. Replacing
them with indexed searches will give you the most �bang for the buck�. Any time you
see that the table access is by a full table scan, it should stick out as a possible
place for improvement.
In the example statement above, it shows us that no index is being used on the
INVENTORY table. There is clearly room for improvement here because we are giving it a
distinct INVENTORY identifier. In this case a quick check might reveal that no index
existed on the INVENTORY_ID field of the INVENTORY table. Adding the index would
change the Explain Plan output as follows.
NESTED LOOPS()
INDEX(UNIQUE SCAN) INVENTORY_PK UNIQUE
TABLE ACCESS(BY ROWID) BACKORDER
INDEX(RANGE SCAN) BACKORDERS_FOR_FRGN NON-UNIQUE
Note that the FULL access has been replaced by a UNIQUE SCAN. This output is much
better and now, the statement will return the results instantly rather than taking
several minutes.
The type of join operation used by the optimizer can significantly impact performance.
The ORACLE server uses the following three basic types of join operations.
* Nested loops returns the first records to the next operation quickly. It is the most
common way ORACLE server performs joins, and in most cases, it indicates that an index
is available for use during the join.
* Merge Joins does not return records to the next operation until all of the rows have
been processed. It is usually used when indexes are either unavailable or disabled by
the statement�s syntax.
* Hash Join is similar to nested loops in that it loops through rows coming from each
step of the plan, however instead of going to an index or table to retrieve the row,
it builds an internal cache structure in memory from which to work.
Understanding the distribution of data within the tables which are being joined and
the amount of data (e.g. number of rows) in each table is also important. As an
example, the nested loops join is a directional operation: if you join two tables
together, you will get different performance depending on which table the optimizer
chooses as the driving table. In the example above, the optimizer choose the INVENTORY
table as the driving table and joined each row from that table their corresponding
rows in the BACKORDER table. This was desirable since we expect only one row with
INVENTORY_ID = 123. Thus, only one pass through the BACKORDER table�s index is
necessary. The optimizer will try to choose the table which returns the fewest rows as
the driving table. The cost-based optimizer determines this by the available
statistics on the tables (among other things). The rule-based optimizer determines
this by the placement of the table in the FROM clause (among other things).
Oracle Hints
At times you may know more about the distribution of the data, indexes or other
factors which the optimizer uses to determine the best execution plan than ORACLE
does. You may want to tell the optimizer to use or not to use a particular index. Or,
it may be helpful to have the optimizer use the rule-based approach to optimization
even though your default approach is cost-based. Through the use of ORACLE hints, the
optimizer can be instructed to do these kinds of things. ORACLE hints can be applied
to simple select, update or delete statements, to the parent or subquery of a complex
statement or even to a part of a compound query, in order to specify one or more of
the following.
* The optimization approach for a SQL statement.
* The goal of cost based optimization (e.g. best throughput or best response time).
* The join order for a SQL statement.
* The join operation for a statement (e.g. sort merge or nested loops).
In short, ORACLE hints can be used to give you control over the choices that the
optimizer makes when establishing the execution plan for your SQL statement. The
syntax for using hints is as follows:
SYNTAX : select | delete | update /*+ hint */
from �;
select | delete | update --+ hint
from �;
As you can see, the hint is buried inside a comment on the select line of the WHERE
clause. This is the only place in the statement where hints can be used. Since it is
buried inside a comment, it is important to understand that if the syntax is incorrect
or the hint is inappropriate, it will be treated as a comment and ignored. This means
that particular attention should be paid to detail when using hints.
Consider the same query we used in the previous example. If for some reason you knew
that a full table scan on the BACKORDER table would be faster than using the index,
you could use an ORACLE hint to tell the optimizer to ignore the index on the
BACKORDER table and instead do a full table scan. This might happen if, for instance,
you knew that the vast majority of the records currently in the BACKORDER table were
related to the INVENTORY record with INVENTORY_ID = 123. The new SQL statement with
the ORACLE hint would look like this.
SELECT /*+FULL(B) */ I.STOCK_NBR, B.BACKORD_QTY
FROM INVENTORY I, BACKORDER B
WHERE I.INVENTORY_ID = B.INVENTORY_ID
AND I.INVENTORY_ID = 123
/
Note that the hint references the BACKORDER table by its alias rather than by the
table name itself. Beware, this is a necessary thing, and is a matter of syntax, but
it can cause confusion. The new Explain Plan output for this statement is as follows.
NESTED LOOPS()
INDEX(UNIQUE SCAN)INVENTORY_PK UNIQUE
TABLE ACCESS(FULL)BACKORDER
As you can see, the optimizer took the hint and did a full table scan on the BACKORDER
table rather than using the available index. Hints such as USE_NL and USE_MERGE can be
used to choose the join type. Hints such as FULL, ROWID and INDEX can be used to
choose the access methods for the join.
The ORDERED hint will cause the tables to be joined in the order they are written in
the from clause, from left to right (useful when you need to change the driving
table). In addition, the ALL_ROWS, FIRST_ROWS and RULE hints can be used to choose the
optimization goal and approach for the SQL statement. Again, the intent was not to
discuss these in detail, but rather to provide some exposure to them and show how they
can be used to effect performance.
Conclusion
The join operation is one of the most important aspects of the relational database and
it has a profound impact on performance and specifically response-time. Understanding
the way the ORACLE optimizer determines execution plans to accomplish the join, and
understanding the tools which allow you to manipulate the optimizer in its choice of
these plans, will allow you to more easily increase productivity and the overall user
satisfaction of your system. To that end, all of the tools discussed in this paper can
and should be employed by your developers to help solve performance problems in their
applications. If developers use these techniques, in the order in which they are
presented, the resolution to the vast majority of performance problems encountered
will become quickly clear. It is my sincere hope that this paper�s introduction to the
tuning concepts and tools will act as a roadmap to the art of solving performance
problems and that the steps presented here will serve as a starting point for
developers who are beginning to explore the vastly misunderstood world of performance
tuning large applications