Improving on EXPLAIN PLAN Results

If you don’t like the results you get from EXPLAIN PLAN, there are some things you can do to change how Oracle executes your query. Generally speaking, these things fall into the following three categories:

  • Restating the query

  • Creating or modifying indexes

  • Using hints

First, though, you have to be sure the default execution path is a bad one. This isn’t as easy as you may think.

Knowing Good Results from Bad

Knowing a good execution plan from a bad one requires some degree of experience and judgment. It also helps to understand your data. In many cases, it may not be enough to look at the plan. You may have to do some benchmarking as well.

Consider the issue of doing a full table scan — reading all the rows in the table — to find rows for a query. On the surface, reading the entire table to find the desired rows seems like a very inefficient approach. Many people avoid it out of hand, thinking that an indexed retrieval is always better. If you have a reasonably large table and are searching for only one or two rows, then a full table scan is not an efficient approach. However, if you are retrieving or summarizing a large percentage of the rows in the table, then a full table scan will likely outperform an indexed retrieval. The problem is that somewhere in between these two extremes lies a large grey area. That’s where you have to do some benchmarking and use some judgment based on your expectations of what the query will be asked to do when it is in production.

Here are some questions to ask yourself as you look at an execution plan:

  • Is a table scan being used to retrieve only a small percentage of rows from a large table? If so, you may want to create an index.

  • Is an index scan being used when you are retrieving, or summarizing, a large percentage of a table’s rows? If so, you may be better of forcing a full table scan.

  • Is Oracle using the most selective index? An index on a YES/NO field would typically be much less selective than an index on last name, for example.

  • Is Oracle joining the largest table last? It’s generally better to eliminate as many rows as possible prior to any joins.

You should be able to make changes based on the above assumptions and be reasonably certain you will gain a performance improvement. It’s always best, however, to test and validate that an improvement has occurred.

Creating Indexes

Creating indexes is an easy way to affect a query. If, for example, you have a large employee table, much larger than the one used in this book, keyed on employee-id, and your query is searching for employees by name, then Oracle will be doing a full table scan for each name lookup. The response time won’t be too good, and your users won’t be too happy. Creating an index on the employee name field would improve your results a great deal.

Don’t overlook the possibility of creating a multicolumn index, even if you don’t use all the index columns in your query’s WHERE clause. Suppose that you frequently execute the following query, which searches for an employee by name and displays that employee’s current billing rate:

SELECT employee_name, employee_billing_rate
  FROM employee
 WHERE employee_name = :emp_name;

If you index the employee table by name, Oracle will look up the name in the index, get the ROWID, read the correct employee record into memory, and then return the billing rate. However, it takes an extra read from disk to fetch the employee record into memory. You can eliminate that extra I/O by creating an index such as this one:

CREATE INDEX employee_by_name ON employee 
  (employee_name, employee_billing_rate);

Since the index now contains the billing rate column, Oracle does not need to read the actual employee record in order to retrieve it. Oracle is smart enough to recognize that all the columns needed to satisfy the query are in the index, and it will take advantage of that fact.

Rewriting the Query

Sometimes you can restate a query, still get the results that you want, and have it run much more efficiently. Consider the following query, which lists all employees who have ever charged time to project 1001 or 1002:

SELECT DISTINCT employee.employee_id, employee.employee_name
  FROM employee, project_hours
 WHERE employee.employee_id = project_hours.employee_id
   AND project_hours.project_id in (1001,1002);

The execution plan for this query looks like this:

   0 SELECT STATEMENT   Cost = 8
   1   SORT UNIQUE
   2     HASH JOIN
   3       TABLE ACCESS FULL EMPLOYEE
   4       TABLE ACCESS FULL PROJECT_HOURS

The query could be restated using an EXISTS predicate instead of joining the employee and project_hours tables together. Here’s how that would look:

SELECT employee_id, employee_name
  FROM employee
 WHERE exists (SELECT * 
                 FROM project_hours
                WHERE project_hours.project_id = 1001
                  AND project_hours.employee_id = employee.employee_id)
    or exists (SELECT * 
                 FROM project_hours
                WHERE project_hours.project_id = 1002
                  AND project_hours.employee_id = employee.employee_id);

The execution plan for this version of the query looks like this:

   0 SELECT STATEMENT   Cost = 1
   1   FILTER
   2     TABLE ACCESS FULL EMPLOYEE
   3     INDEX RANGE SCAN PROJECT_HOURS_PK
   4     INDEX RANGE SCAN PROJECT_HOURS_PK

The second version of the query has a cost of 1, as opposed to a cost of 8 for the first. Thus, the second version of the query is the one to use. The higher cost of the first query probably comes from the SORT, which is necessary to eliminate duplicate rows, and from the full table scan on the PROJECT_HOURS table.

Using Hints

Rather than allowing Oracle to have total control over how a query is executed, you can provide specific directions to the optimizer through the use of hints. A hint, in Oracle, is an optimizer directive embedded in a SQL statement in the form of a comment. Here is a query with an optimizer hint telling Oracle to do a full table scan:

SELECT /*+ full(employee) */
       employee_id, employee_name, employee_billing_rate
  FROM employee
 WHERE employee_name = 'Jenny Gennick';

The hint in this case is “full(employee)”, which tells Oracle to do a full table scan of the employee table. Oracle will honor this hint and perform a full table scan, even if there happens to be an index on the employee name field.

Syntax for a hint

A hint applies to a single SQL statement, and hints may be specified only for SELECT, INSERT, UPDATE, and DELETE statements. The hint takes the form of a specially formatted comment, and must appear immediately following the keyword that begins the statement. The hint takes the form:

                  keyword /*+ [hint|comment...] */

where:

keyword

Is the keyword that begins the statement. This will be one of the keywords SELECT, UPDATE, or DELETE. Hints do not apply to any other type of statement. The hint must immediately follow the keyword that begins the statement.

hint

The hint itself, sometimes with one or more arguments enclosed in parenthesis. Tables 8-2 through 8-7 provide a complete list of possible hints. Hints are not case-sensitive. A single comment may contain more than one hint, as long as the hints are separated by at least one space.

comment

A user-specified comment. Oracle allows you to intersperse comments with your hints.

Here are some examples of how hints may be specified:

SELECT /*+ full(employee) */ employee_id, employee_name
  FROM employee
 WHERE employee_billing_rate > 100;

SELECT /*+ full(e) do a full table scan on the employee table, because
                          most employees do have billing rates > 100. */ 
       employee_id, employee_name
  FROM employee e
 WHERE employee_billing_rate > 100;

Note the second example. The table name is “employee”, but an alias of “e” has been specified. The hint for the table uses the same alias, and is specified as “full(e)”. Whenever an alias is used, you must also use the alias name in any hints for the table.

If you want to supply multiple hints for a statement, they must all appear in the same comment, for example:

SELECT /*+ full(employee) first_rows */ 
       employee_id, employee_name
  FROM employee
 WHERE employee_billing_rate > 100;

When subqueries are used, they are allowed to have their own hints. The hint for a subquery follows immediately after the keyword that starts the query, for example:

SELECT /*+ first_rows */ employee_id, employee_name
  FROM employee
 WHERE exists (SELECT /*+ full(project_hours) */* 
                 FROM project_hours
                WHERE project_hours.project_id = 1001
                  AND project_hours.employee_id = employee.employee_id);

When using hints, be very careful to get the syntax exactly right. Because hints are embedded in the statements as comments, Oracle can’t do any syntax checking. Oracle treats any incorrectly specified hint as a comment. In addition, you should always do an EXPLAIN PLAN after you code your hints, just to be sure that the optimizer is really doing what you think you told it to do.

Oracle hints can loosely be divided into the following categories:

  • Optimizer goal hints

  • Access method hints

  • Join order hints

  • Join operation hints

  • Parallel execution hints

  • Other hints

The next few sections describe the hints available in each category.

Optimizer goal hints

Optimization goal hints allow you to influence the optimizer’s overall goal when formulating an execution plan. You may, for example, specify that you want the plan optimized to return the first record as quickly as possible. Table 8.2 gives a list of these hints.

Table 8-2. Optimization Goal Hints

Hint

Description

ALL_ROWS

Tells the optimizer to produce an execution plan that minimizes resource consumption.

FIRST_ROWS

Tells the optimizer to produce an execution plan with the goal of getting to the first row as quickly as possible.

CHOOSE

Allows the optimizer to choose between the rule-based mode and the cost-based mode. If statistics are present for any tables in the query, the cost-based approach will be taken.

RULE

Forces the optimizer to use a rule-based approach for the statement.

You should avoid the RULE hint if at all possible. That hint causes the rule-based optimizer to be used. The rule-based optimizer uses a fixed set of rules when determining the execution plan for a statement, and does not attempt to factor in the ultimate cost of executing that plan. The cost-based optimizer, on the other hand, will base its decision on the estimated I/O and CPU overhead required by various alternative plans. While Oracle still supports the rule-based optimizer, it hasn’t been enhanced in years, won’t be enhanced in the future, and may be de-supported at some point. Oracle is putting their development effort into the cost-based optimizer.

Access method hints

Access method hints allow you to control the way data is accessed. For example, you can tell Oracle to do a full table scan, or to use an index when accessing a table. You can name the specific index to be used. Table 8.3 provides a list of these hints.

Table 8-3. Access Method Hints

Hint

Description

FULL(table_name)

Requests a full table scan of the specified table, regardless of any indexes that may exist.

ROWID(table_name)

Tells Oracle to perform a scan of the specified table based on ROWIDs.

CLUSTER(table_name)

Tells Oracle to do a cluster scan of the specified table. This hint is ignored if the table is not clustered.

HASH(table_name)

Tells Oracle to do a hash scan of the specified table. This hint is ignored if the table is not clustered.

HASH_AJ(table_name)

Tells Oracle to do a hash anti-join of the specified table.

INDEX(table_name

[index_name...])

Tells Oracle to access the specified table via an index scan. Optionally, you may specify the index to use; otherwise, Oracle chooses the index. You may also specify a list of indexes to choose from, and Oracle will choose from that list.

INDEX_ASC(table_name [index_name...])

Similar to the INDEX hint, but tells Oracle to scan the index in ascending order.

INDEX_COMBINE(table_name [index_name...])

Tells Oracle to use some combination of two indexes. You may specify the indexes to choose from, or let Oracle make the choice.

INDEX_DESC(table_name [index_name...])

Similar to INDEX_ASC, but forces Oracle to scan the index in descending order.

INDEX_FFS(table_name [index_name...])

Tells Oracle to do a fast full index scan.

MERGE_AJ(table_name)

Turns a NOT IN subquery into a merge anti-join.

AND_EQUAL(table_name index_name index name...)

Tells Oracle to scan two or more indexes and merge the results. You must specify at least two index names.

USE_CONCAT

Turns a query with OR conditions into two or more queries unioned together with a UNION ALL.

All access method hints take at least a table name as an argument. That’s because you may want to specify different access methods for different tables in your query. The FULL hint, for example, takes one table name as an argument.

/*+ FULL(employee) */

Some of the access method hints are index-related, and allow you to specify one or more indexes to be used. In many cases, as with the INDEX hint, you have the choice of specifying an index name or not. The following hint, for example, tells Oracle you want to do an index scan on the employee table, but it’s up to Oracle to pick the index:

/*+ INDEX(employee) */

This is useful if you think Oracle will make the correct choice, or if you don’t want to hardcode an index name into the hint. You have the option, however, of specifying the exact index to use. Here’s an example:

/*+ INDEX(employee employee_by_name) */

You may even specify a list of indexes, and Oracle will choose from the indexes in that list. If, for example, you had seven indexes on the employee table, but you believed that only two would be at all useful for the query in question, you could specify a hint like this:

/*+ INDEX(employee employee_by_name, employee_by_billing_rate) */

This tells Oracle that you want to use an index scan to access the employee table, and that you want to use either the name index or the billing rate index.

The AND_EQUAL hint is special in that it requires at least two indexes to be specified. That’s because this hint causes Oracle to merge the results of two index scans together. You can’t do that unless you have two indexes to scan.

If your choice conflicts with other hints, if it cannot be implemented, or if the indexes you specify do not exist, Oracle will simply ignore the hint altogether. Take a look at the following query:

SELECT /*+ USE_CONCAT */
       employee_id, employee_name
  FROM employee
 WHERE employee_name = 'Jeff Gennick';

The USE_CONCAT hint makes no sense here because the query does not contain an OR condition. You can’t break this up into two queries and then UNION the results together, so Oracle will ignore the hint. A bad hint will be honored, however, whenever it is possible to implement. The following query contains a hint to do an index scan on the primary key index for the employee table:

SELECT /*+ INDEX(employee employee_pk) */
       employee_name
  FROM employee
 WHERE employee_name = 'Jeff Gennick';

The primary key for employee is the employee_id field. An index on employee name does exist. The query seeks one record based on the employee name. Even though it makes perfect sense to look up the name in the name index, Oracle will honor the request to use the primary key index. Here is the execution plan for this statement:

   0 SELECT STATEMENT   Cost = 826
   1   TABLE ACCESS BY INDEX ROWID EMPLOYEE
   2     INDEX FULL SCAN EMPLOYEE_PK

Oracle is going to read every entry in the primary key index, retrieve the associated row from the employee table, and check the name to see if it has a match. This is worse than a full table scan! Oracle does this because the hint requested it, and because it physically can be done, so be careful what you ask for, and check the results. Change the index name used in the query to employee_by_name, and the execution plan looks like this:

0 SELECT STATEMENT   Cost = 1
1   INDEX RANGE SCAN EMPLOYEE_BY_NAME

You can see that the relative cost of using the name index is much, much less than that of using the primary key index. Be careful with these hints.

Join order hints

Join order hints allow you to exercise some control over the order in which Oracle joins tables. There are only three of them, and they are listed in Table 8.4.

Table 8-4. Join Order Hints

Hint

Description

ORDERED

Tells Oracle to join tables left to right, in the same order in which they are listed in the FROM clause.

STAR

Tells Oracle to use a star query execution plan, if at all possible. This can only work if there are at least three tables being joined, and the largest table has a concatenated index on columns that reference the two smaller tables. The two smaller tables are joined first, and then a nested-loop join is used to retrieve the required rows from the largest table.

STAR_TRANSFORMATION

Tells Oracle to transform the query into a star query, if possible, and then use the best plan for that query.

Join operation hints

Join operation hints allow you to control the manner in which two tables are joined. Oracle uses three basic methods whenever two tables are joined: the merge join, the nested loops join, and the hash join.

A merge join is done by sorting the rows from each table by the join columns. Once the two rowsets have been sorted, Oracle reads through both and joins any matching rows together. A merge join often uses fewer resources than the other options, but you have to wait for all the records to be sorted before you get the first one back. You also have to have enough memory and temporary disk space to handle the sort.

The method used for a nested loops join corresponds to the mental image most people have in mind when they think of joining tables. Oracle picks one table as the driving table, and reads through that table row by row. For each row read from the driving table, Oracle looks up the corresponding rows in the secondary table and joins them together. Because no sort is involved, a nested loops join will usually get you the first record back more quickly than a merge join. For the same reason, a nested loops join also does not require large amounts of disk space and memory. However a nested loops join may result in a considerably greater number of disk reads than a merge join.

A hash join is similar to a merge join, but a sort is not required. A hash table is built in memory to allow quick access to the rows from one of the tables to be joined. Then rows are read from the other table. As each row is read from the second table, the hash function is applied to the join columns, and the result is used to find the corresponding rows from the first table.

Aside from the hints used to specify the join method, there are a few other hints lumped into the join operation category. Table 8.5 lists all the join operation hints.

Table 8-5. Join Operation Hints

Hint

Description

USE_NL(table_name)

Tells Oracle to use a nested loop when joining this table. The table specified by this hint will be the one accessed by the innermost loop. The other table will be the driving table.

USE_MERGE(table_name)

Tells Oracle to use the sort merge method when joining this table.

USE_HASH(table_name)

Tells Oracle to use a hash join for the specified table.

NO_MERGE

This is not the opposite of USE_MERGE. The NO_MERGE hint applies to queries that contain joins on one or more views. It prevents Oracle from merging the query from a view into the main query.

DRIVING_SITE(table_name)

This hint applies when you are executing a distributed join, one that joins tables from two or more databases. Without a hint, Oracle will choose which database actually collects the tables and does the join. By using the hint, you are telling Oracle that you want the join performed by the database containing the specified table.

Parallel execution hints

The hints shown in Table 8.6 allow you to influence the way Oracle executes a query in a parallel processing environment. In an environment with a single CPU, parallel processing is not possible, and these hints will be ignored.

Table 8-6. Parallel Execution Hints

Hint

Description

PARALLEL(table_name

[, degree [, num_instances]])

Tells Oracle to access data from the indicated table in a parallel processing mode. You can optionally specify both the degree of parallelism to use and the number of instances that will be involved. The keyword DEFAULT may be used for both arguments, in which case Oracle decides the values based on parameters in the INIT.ORA file and the table definition. Using the PARALLEL hint in an INSERT statement automatically turns APPEND mode on. See the APPEND and NO_APPEND hints.

NO_PARALLEL(table_name)

Tells Oracle not to access the specified table in parallel.

APPEND

Applies only to INSERT statements. It tells Oracle not to attempt to reuse any freespace that may be available in any extents currently allocated to the table.

NOAPPEND

This is the opposite of APPEND, and tells Oracle to use any freespace in extents currently allocated to the table. This hint exists because APPEND becomes the default behavior whenever a PARALLEL hint is used in an INSERT statement.

PARALLEL_INDEX(table_name, index_name [,degree [,num_instances]])

Tells Oracle to access data from the indicated table by scanning the specified index in a parallel processing mode. The index must be a partitioned index. You can optionally specify both the degree of parallelism to use and the number of instances that will be involved. The keyword DEFAULT may be used for both arguments, in which case Oracle decides the values based on parameters in the INIT.ORA file and the table definition.

Other hints

There are a few hints that don’t fit neatly into one of the other categories. These are listed in Table 8.7.

Table 8-7. Other Hints

Hint

Description

CACHE(table_name)

Applies only when a full table scan is being performed on the specified table. It tells Oracle to place blocks for that table at the most recently used end of the buffer cache, so they will remain in memory as long as possible. This can be useful for small lookup tables that you expect to access repeatedly.

NOCACHE(table_name)

This is the opposite of CACHE, and tells Oracle to place blocks at the least recently used end of the buffer cache, where they will be cleared out as soon as possible.

PUSH_SUBQ

Tells Oracle to evaluate nonmerged subqueries as soon as possible during query execution. If you expect the subquery to eliminate a large number of rows, this can result in a performance improvement.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.184.90