Oracle slow query performance with PARALLEL optimization plan - oracle

I have very simple query
SELECT
A
FROM table
where B = 'X'
explain plan for it looks like
|
0 | SELECT STATEMENT | | 2 | 16 | 4 (0)| 00:00:01 | | | |
| 1 | PX COORDINATOR | | | | | | | | |
| 2 | PX SEND QC (RANDOM) | :TQ10000 | 2 | 16 | 4 (0)| 00:00:01 | Q1,00 | P->S | QC (RAND) |
| 3 | PX BLOCK ITERATOR | | 2 | 16 | 4 (0)| 00:00:01 | Q1,00 | PCWC | |
|* 4 | INDEX FAST FULL SCAN| TABLE_UNIQUE_ROLES_KEY1 | 2 | 16 | 4 (0)| 00:00:01 | Q1,00 | PCWP | |
It appears to me that Oracle tries to run PARALLEL execution plan.
But I do not have any understanding why it would do it. It significantly slows down query
and if I do
SELECT /*+ NO_PARALLEL */
A
FROM table
where B = 'X'
it works fast, and plan is:
----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 16 | 4 (0)| 00:00:01 |
|* 1 | INDEX FAST FULL SCAN| TABLE_UNIQUE_ROLES_KEY1 | 2 | 16 | 4 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------
What causes parallelism in first scenario?
The degree on the table is set to 1 but the degree on the TABLE_UNIQUE_ROLES_KEY1 (and the other indexes on the table) are all set to 4. I don't have privileges to query v$parameter so I can't see how parallelism is configured for the database.
TABLE_UNIQUE_ROLES_KEY1 is a covering index for the query-- it is defined on the columns (a, b, c, d) where a is the column I'm selecting, b is the column that I'm filtering on and c and d are not involved in the query.

The immediate cause is that someone has told Oracle that it should use parallel query (the degree for your indexes has all been set to 4). That tends to make the optimizer think that full scanning the index in parallel will be relatively cheap which is why the optimizer is picking that plan.
You can change the parallel setting on your index
ALTER INDEX TABLE_UNIQUE_ROLES_KEY1 NOPARALLEL
which should stop the optimizer from choosing this plan (you may have to set other indexes to noparallel as well to prevent the optimizer from picking a different index to full scan in parallel). But I'd hesitate to do that until I understood what person or process set the degree on your indexes to 4-- if you don't understand the root cause, it's likely that you'll end up either breaking something else or in an endless battle where that person/ process sets your indexes to use parallelism and you set them back.
The two most likely candidates for what caused the indexes to have a degree of 4 are that someone (either a developer or a DBA) was trying to get parallel query to kick in for some other query or that the DBA is running an (almost certainly unnecessary) script that periodically rebuilds indexes that does so in parallel without realizing that this changes the degree setting on the index and makes it likely that parallel query kicks in. So you probably need to have a chat with the other developers and/or the other DBAs to figure out whether setting the index to noparallel will negatively affect them and whether there are other processes that will be changing the setting on you.

Related

How can we use use parallel (10) hint in Oracle Merge statement

How can we use parallel (10) hints in the oracle merge statement? , I am a bit new to hints and would like to know if this can be done for merge statements?
In very-very short:
First of all, parallel execution must be enabled on the server.
You need to check the following 3 parameters:
select name,value
from v$parameter
where name in (
'parallel_degree_policy'
,'parallel_servers_target'
,'parallel_max_servers'
);
PARALLEL_MAX_SERVERS specifies the maximum number of parallel execution processes, so it must be >0
PARALLEL_SERVERS_TARGET specifies the number of parallel server processes allowed to run parallel statements before statement queuing will be used. When the parameter PARALLEL_DEGREE_POLICY is set to AUTO, Oracle will queue SQL statements that require parallel execution, if the necessary parallel server processes are not available.
PARALLEL_DEGREE_POLICY specifies whether or not automatic degree of Parallelism, statement queuing, and in-memory parallel execution will be enabled.
Values:
MANUAL: Disables automatic degree of parallelism, statement queuing, and in-memory parallel execution. This reverts the behavior of parallel execution to what it was prior to Oracle Database 11g Release 2 (11.2). This is the default.
LIMITED: Enables automatic degree of parallelism for some statements but statement queuing and in-memory Parallel Execution are disabled. Automatic degree of parallelism is only applied to those statements that access tables or indexes decorated explicitly with the PARALLEL clause. Tables and indexes that have a degree of parallelism specified will use that degree of parallelism.
AUTO: Enables automatic degree of parallelism, statement queuing, and in-memory parallel execution.
Then you can check your session parameters:
select id,name,sql_feature,isdefault,value
from v$ses_optimizer_env e
where name like '%parallel%'
and sid=userenv('sid');
ID NAME SQL_FEATURE ISDEFAUL VALUE
---------- ---------------------------------------- ------------ -------- -------------------------
2 parallel_execution_enabled QKSFM_CBO YES true
13 parallel_threads_per_cpu QKSFM_CBO YES 2
35 parallel_query_mode QKSFM_ALL YES enabled
36 parallel_dml_mode QKSFM_ALL YES disabled
37 parallel_ddl_mode QKSFM_ALL YES enabled
245 parallel_degree_policy QKSFM_PQ YES manual
246 parallel_degree QKSFM_PQ YES 0
247 parallel_min_time_threshold QKSFM_PQ YES 10
256 parallel_query_default_dop QKSFM_PQ YES 0
272 parallel_degree_limit QKSFM_PQ YES 65535
273 parallel_force_local QKSFM_PQ YES false
274 parallel_max_degree QKSFM_PQ YES 16
289 parallel_autodop QKSFM_PQ YES 0
290 parallel_ddldml QKSFM_PQ YES 0
331 parallel_dblink QKSFM_PQ YES 0
Here you can notice three xxx_mode parameters: parallel_ddl_mode, parallel_dml_mode and parallel_query_mode, which allows related parallel operations on session level and can be enabled or disable using alter session enable/disable/force, for example alter session force parallel dml parallel 16 - enable parallel dml with DOP=16:
SQL> alter session force parallel dml parallel 16;
Session altered.
SQL> select id,name,sql_feature,isdefault,value
2 from v$ses_optimizer_env e
3 where name like 'parallel_dml%'
4 and sid=userenv('sid');
ID NAME SQL_FEATURE ISDEFAULT VALUE
---------- ------------------------- ----------- ---------- -------------------------
4 parallel_dml_forced_dop QKSFM_CBO NO 16
36 parallel_dml_mode QKSFM_ALL NO forced
Hint parallel has 2 forms:
statement level parallel(DoP)
and object level parallel(alias DoP)
In fact, this hint
enables parallel execution in case of parallel_degree_policy parameter=MANUAL
reduces a cost of parallel operations to make parallel plans more preferable (it doesn't force parallel execution - your plan still will be serial if its' cost cheaper then parallel one), and
specifies DoP (Degree of Parallelism).
Next, different plan steps can have different DOP, or even run serially: for example, different objects (indexes or tables or their partitions) can have different parallel options (alter table xxx parallel 8) or "read" operations (row-sources) can be parallelized, but "change" (like UPDATE or MERGE) operations are serial.
For example, with parallel_degree_policy=manual and disabled parallel dml:
SQL> explain plan for update/*+ parallel(4) */ t set b=a+1 where a >1;
Plan hash value: 1378397380
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | TQ |IN-OUT| PQ Distrib |
---------------------------------------------------------------------------------------------------------------
| 0 | UPDATE STATEMENT | | 9999 | 79992 | 2 (0)| 00:00:01 | | | |
| 1 | UPDATE | T | | | | | | | |
| 2 | PX COORDINATOR | | | | | | | | |
| 3 | PX SEND QC (RANDOM)| :TQ10000 | 9999 | 79992 | 2 (0)| 00:00:01 | Q1,00 | P->S | QC (RAND) |
| 4 | PX BLOCK ITERATOR | | 9999 | 79992 | 2 (0)| 00:00:01 | Q1,00 | PCWC | |
|* 5 | TABLE ACCESS FULL| T | 9999 | 79992 | 2 (0)| 00:00:01 | Q1,00 | PCWP | |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - filter("A">1)
Note
-----
- Degree of Parallelism is 4 because of hint
You can see than FTS (Full table scan) is parallel, but UPDATE is serial. On newer actually supported Oracle versions and you can see one more note - PDML is disabled in current session.
And if you enable parallel dml using alter session enable parallel dml or alter session force parallel dml parallel N (on actual oracle version you can use also enable_parallel_dml hint, it was backported to 11.2.0.4, but documented only since 12.1, so I wouldn't suggest to use it until at least 12.1):
SQL> alter session enable parallel dml;
Session altered.
SQL> explain plan for update/*+ parallel(4) */ t set b=a+1 where a >1;
Plan hash value: 2037160838
---------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | TQ |IN-OUT| PQ Distrib |
---------------------------------------------------------------------------------------------------------------
| 0 | UPDATE STATEMENT | | 9999 | 79992 | 2 (0)| 00:00:01 | | | |
| 1 | PX COORDINATOR | | | | | | | | |
| 2 | PX SEND QC (RANDOM) | :TQ10000 | 9999 | 79992 | 2 (0)| 00:00:01 | Q1,00 | P->S | QC (RAND) |
| 3 | UPDATE | T | | | | | Q1,00 | PCWP | |
| 4 | PX BLOCK ITERATOR | | 9999 | 79992 | 2 (0)| 00:00:01 | Q1,00 | PCWC | |
|* 5 | TABLE ACCESS FULL| T | 9999 | 79992 | 2 (0)| 00:00:01 | Q1,00 | PCWP | |
---------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - filter("A">1)
Note
-----
- Degree of Parallelism is 4 because of hint
As you can see, now UPDATE is parallel too.
So, it depends on your current database, table and sessions parameters and what exactly you want to get and: enable parallel operations in case of parallel_degree_policy=manual, or enable parallel dml, or limit/force required DOP, etc
A bit more details for start:
19.1.2.7 Hints for Parallel Execution
Using Parallel Execution
How Parallel Execution Works
How? Like this:
SQL> alter session enable parallel dml;
Session altered.
SQL> merge /*+ parallel(10) */ into emp e
2 using dept d
3 on (d.deptno = e.deptno)
4 when matched then update set
5 e.comm = e.comm * 1;
14 rows merged.
SQL>
The PARALLEL hint will enable read-parallelism, but to also enable write-parallelism you will need to either run the above ALTER SESSION command or use the hint /*+ ENABLE_PARALLEL_DML */.

how to avoid TABLE ACCESS FULL when fetching rows in Oracle

I'm using Oracle 18c but I guess my question would not be bound to the specific version.
I want to fetch rows from a table but I found a complex, ugly solution.
I would like to know if there is better, simple query that can return the same result as following.
First of all, I have a simple table like this.
Note that col is going to store large text.
CREATE TABLE simpletable
(record_id NUMBER,
col CLOB,
PRIMARY KEY (record_id));
I want to retrieve single row from the above table and whichever row is acceptable.
First query came to my mind is as following.
SELECT * FROM (SELECT * FROM simpletable) WHERE rownum <= 1;
Another is as following.
SELECT * FROM (SELECT * FROM simpletableORDER BY record_id) WHERE rownum <= 1;
Unfortunately, neither of above two does not use primary-key index and uses TABLE ACCESS FULL which can take long time when the table grows enough large.
(I'm guessing that oracle preferred the simpler plan because my table is not enough large yet to use index scan.
Oracle might choose different plan if the table grows up further.)
My final solution that uses primary-key index to narrow down the table access is following.
SELECT simpletable.* FROM
(SELECT * FROM
(SELECT record_id, ROWID as id FROM simpletable ORDER BY record_id)
WHERE rownum<=1) a
JOIN simpletable ON a.id = simpletable.ROWID;
If you have a better solution, please let me know.
It would be very appreciated.
P.S.
The first two queries produced the following plan.
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2015 | 4 (25)| 00:00:01 |
|* 1 | COUNT STOPKEY | | | | | |
| 2 | VIEW | | 1 | 2015 | 4 (25)| 00:00:01 |
|* 3 | SORT ORDER BY STOPKEY| | 1 | 2015 | 4 (25)| 00:00:01 |
| 4 | TABLE ACCESS FULL | SIMPLETABLE | 1 | 2015 | 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------------
the final one is:
----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2039 | 3 (0)| 00:00:01 |
| 1 | NESTED LOOPS | | 1 | 2039 | 3 (0)| 00:00:01 |
| 2 | VIEW | | 1 | 25 | 2 (0)| 00:00:01 |
|* 3 | COUNT STOPKEY | | | | | |
| 4 | VIEW | | 1 | 25 | 2 (0)| 00:00:01 |
| 5 | INDEX FULL SCAN | SYS_C007561 | 1 | 25 | 2 (0)| 00:00:01 |
| 6 | TABLE ACCESS BY USER ROWID| SIMPLETABLE | 1 | 2014 | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------
I think using OFFSET..FETCH method might helps you here -
SELECT *
FROM simpletable
ORDER BY record_id
OFFSET 0 ROWS
FETCH FIRST ROW ONLY;
If the Cost Based Optimizer code has access to reliable statistics, from its dictionary, regarding all the objects available for this query, then it will very likely produce an optimal execution plan. Of course, there are exceptions and you would argue with their support people as to whether or not choosing a suboptimal plan is a bug.
In this specific case, if you are querying a single table and the CBO could choose between a full table scan and some other scan and then chose a full table scan, then chances were good that the CBO determined that the number of blocks scanned (buffer gets) would have been smaller using a full table scan.
You can expose the truth of the matter by tracing the execution of multiple versions of the statement, each one using a different set of hints to force a particular execution plan. You should consider the execution with the fewest buffer gets to be the winner. Alternatively, if the execution plan is of a serial nature, then you can use response time as measure. If the winner is not automatically chosen by the CBO, then it's probably because the statistics it used were not accurate and you should make them accurate. If the statistics are indeed accurate then Oracle support will probably give you a very long homework assignment.
Similar to the Horror Vacui, some database developers suffer under the Horror FULL TABLE SCAN by simply assuming index access good, full scan bad.
But this is not true, FULL TABLE SCAN is a normal access method, that is preferred in some situation.
Let's illustrate it on a simple example with 10K rows in your table
insert into simpletable (record_id, col)
select rownum, rpad('x',3998,'y')
from dual connect by level <= 10000
To get one arbitrary row from the table you simple use the following query
select * from simpletable where rownum = 1;
Here is the output (edited for brevity) you get from SQL*Plus with setting set autotrace traceonly to see the execution plan and the statistics.
Execution Plan
----------------------------------------------------------
Plan hash value: 1007892724
--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time
--------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2015 | 2 (0)| 00:00:01
|* 1 | COUNT STOPKEY | | | | |
| 2 | TABLE ACCESS FULL| SIMPLETABLE | 10188 | 19M| 2 (0)| 00:00:01
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(ROWNUM=1)
Statistics
----------------------------------------------------------
5 consistent gets
2 physical reads
1 rows processed
The most important information is the statistics consistent gets - there were only 5 blocks accessed - the table is much larger.
What is the explanation? See the operation COUNT STOPKEY above the TABLE ACCESS FULL this ensures that the scan is terminated after the first row is found.
If you want to get a specific row, e.g. the one with the highest ID, the prefered approach is using the row_limiting_clause
SELECT *
FROM simpletable
ORDER BY record_id DESC
OFFSET 0 ROWS FETCH NEXT 1 ROW ONLY;
You will see the execution plan below, that performs first the INDEX FULL SCAN DESCENDING. The complete (full) index will be red in the descending order, but again due to STOPKEY you break after reading the highest key (which is the first entry due to the descending order).
---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10188 | 19M| 613 (0)| 00:00:01 |
|* 1 | VIEW | | 10188 | 19M| 613 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY | | 10188 | 19M| 613 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| SIMPLETABLE | 10188 | 19M| 613 (0)| 00:00:01 |
| 4 | INDEX FULL SCAN DESCENDING| SYS_C008793 | 10188 | | 29 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=CASE WHEN (0>=0) THEN
0 ELSE 0 END +1 AND "from$_subquery$_002"."rowlimit_$$_rownumber">0)
2 - filter(ROW_NUMBER() OVER ( ORDER BY
INTERNAL_FUNCTION("SIMPLETABLE"."RECORD_ID") DESC )<=CASE WHEN (0>=0) THEN 0 ELSE 0
END +1)
Note if the table is empty or contains very few rows, you will se even here a TABLE ACCESS FULL because the optimizer recognises that it is more effective that to first go to the index and that access the table.

How to imporve performace of an insert when a table has so many indexes in Oracle?

We have a table having about 100 indexes on it. So when I try to insert a high number of rows into this table, it takes too much time to perform the insert. I tried the PARALLEL and APPEND hints but did not help much.
Is there any other ways to improve the insert performance in such situations? (I don't want to disable and then enable the triggers)
Use the explain plan to ensure that you are using the append and parallel hints correctly - there are many ways for those hints can go wrong.
Here's an example of a good explain plan for large data warehouse statements:
create table test1(a number);
explain plan for
insert /*+ append parallel enable_parallel_dml */ into test1
select 1 from test1;
select * from table(dbms_xplan.display);
Plan hash value: 1209398148
--------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time | TQ |IN-OUT| PQ Distrib |
--------------------------------------------------------------------------------------------------------------------
| 0 | INSERT STATEMENT | | 1 | 2 (0)| 00:00:01 | | | |
| 1 | PX COORDINATOR | | | | | | | |
| 2 | PX SEND QC (RANDOM) | :TQ10000 | 1 | 2 (0)| 00:00:01 | Q1,00 | P->S | QC (RAND) |
| 3 | LOAD AS SELECT (HYBRID TSM/HWMB)| TEST1 | | | | Q1,00 | PCWP | |
| 4 | OPTIMIZER STATISTICS GATHERING | | 1 | 2 (0)| 00:00:01 | Q1,00 | PCWP | |
| 5 | PX BLOCK ITERATOR | | 1 | 2 (0)| 00:00:01 | Q1,00 | PCWC | |
| 6 | TABLE ACCESS FULL | TEST1 | 1 | 2 (0)| 00:00:01 | Q1,00 | PCWP | |
--------------------------------------------------------------------------------------------------------------------
Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (U - Unused (1))
---------------------------------------------------------------------------
0 - STATEMENT
U - parallel
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
- automatic DOP: Computed Degree of Parallelism is 2
For good parallel direct-path performance, these are the main things to look for in the explain plan:
Ensure direct path writes are used. "LOAD AS SELECT" means the append hint worked, "INSERT CONVENTIONAL" means the hint was not used. There are many possible reasons why direct path cannot be used, the most common of which is that parallel DML is not enabled, which is what the third hint does. (Before 12c, you had to run alter session enable parallel dml.)
Ensure parallelism is used for both reads and writes. There should be a "PX" operation both before and after the "LOAD AS SELECT". If there's not an operation before, then the writing is not done in parallel.
Ensure the degree of parallelism is correct. The explain plan will tell you the requested degree of parallelism. The DOP is hard to get right and is affected by many factors. If your DOP seems wrong, use this checklist to look for possible problems.
If you're still having problems, use a SQL monitoring report to find information about the actual execution plan, rows, times, and wait events. Generating the report is usually as easy as select dbms_sqltune.report_sql_monitor('your SQL_ID') from dual. If you post the results here someone can probably find a way to improve the performance.

Why is Oracle's query planner adding a filter predicate that replicates a constraint?

I have a simple Oracle query with a plan that doesn't make sense.
SELECT
u.custid AS "custid",
l.listid AS "listid"
FROM
users u
INNER JOIN lists l ON u.custid = l.custid
And here’s what the autotrace explain tells me it has for a plan
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1468K| 29M| | 11548 (1)| 00:00:01 |
|* 1 | HASH JOIN | | 1468K| 29M| 7104K| 11548 (1)| 00:00:01 |
| 2 | INDEX FAST FULL SCAN| USERS_PK | 404K| 2367K| | 266 (2)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | LISTS | 1416K| 20M| | 9110 (1)| 00:00:01 |
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("U"."CUSTID"="L"."CUSTID")
3 - filter(TRUNC("SORT_TYPE")>=1 AND TRUNC("SORT_TYPE")<=16)
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
- this is an adaptive plan
- 1 Sql Plan Directive used for this statement
What concerns me is predicate 3. sort_type does not appear in the query, and is not indexed at all. It seems to me that sort_type should not be involved in this query at all.
There is a constraint on lists.sort_type: (Yes, I know we could have sort_type be an INTEGER not a NUMBER)
sort_type NUMBER DEFAULT 2 NOT NULL,
CONSTRAINT lists_sort_type CHECK ( sort_type BETWEEN 1 AND 16 AND TRUNC(sort_type) = sort_type )
It looks to me that that filter is on sort_type is basically a tautology. Every row in lists must pass that filter because of that constraint.
If I drop the constraint, the filter no longer shows up in the plan, and the estimated cost goes down a little bit. If I add the constraint back, the plan uses the filter again. There's no significant difference in execution speed one way or the other.
I'm concerned because I discovered this filter in a much larger, more complex query that I was trying to optimize down from a couple of minutes of runtime.
Why is Oracle adding that filter, and is it a problem and/or pointing to another problem?
EDIT: If I change the constraint on sort_type to not have the TRUNC part, the filter disappears. If I split the constraint into two different constraints, the filter comes back.
Generally speaking, Oracle generates predicates based on your CHECK constraints whenever doing so will give the optimizer more information to generate a good plan. It is not always smart enough to recognize when those are redundant. Here is a short example in Oracle 12c using your table names:
-- Create the CUSTS table
CREATE TABLE custs ( custid number not null );
CREATE INDEX custs_u1 on custs (custid);
-- Create the LISTS table
CREATE TABLE lists
( listid number not null,
sort_type number not null,
custid number,
constraint lists_c1 check ( sort_type between 1 and 16 and
trunc(sort_type) = sort_Type )
);
-- Explain a join
EXPLAIN PLAN FOR
SELECT /*+ USE_HASH(u) */
u.custid AS "custid",
l.listid AS "listid"
FROM custs u
INNER JOIN lists l ON u.custid = l.custid;
-- Show the plan
select * from table(dbms_xplan.display);
Plan hash value: 2443150416
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 39 | 3 (0)| 00:00:01 |
|* 1 | HASH JOIN | | 1 | 39 | 3 (0)| 00:00:01 |
| 2 | INDEX FULL SCAN | CUSTS_U1 | 1 | 13 | 1 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| LISTS | 1 | 26 | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("U"."CUSTID"="L"."CUSTID")
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
So far, nothing weird. No questionable predicates added.
Now, let's tell the Oracle optimizer that the distribution of data on TRUNC(sort_type) might matter...
declare
x varchar2(30);
begin
x := dbms_stats.create_extended_stats ( user, 'LISTS', '(TRUNC(SORT_TYPE))');
dbms_output.put_line('Extension name = ' || x);
end;
... and, now, let's explain that same query again...
-- Re-explain the same join as before
EXPLAIN PLAN FOR
SELECT /*+ USE_HASH(u) */
u.custid AS "custid",
l.listid AS "listid"
FROM custs u
INNER JOIN lists l ON u.custid = l.custid;
-- Show the new plan
select * from table(dbms_xplan.display);
Plan hash value: 2443150416
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 52 | 3 (0)| 00:00:01 |
|* 1 | HASH JOIN | | 1 | 52 | 3 (0)| 00:00:01 |
| 2 | INDEX FULL SCAN | CUSTS_U1 | 1 | 13 | 1 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| LISTS | 1 | 39 | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("U"."CUSTID"="L"."CUSTID")
3 - filter(TRUNC("SORT_TYPE")>=1 AND TRUNC("SORT_TYPE")<=16)
Note
-----
- dynamic statistics used: dynamic sampling (level=2)
Now, Oracle has added the predicate, because the CBO might benefit from it. Does it really benefit from it? No, but Oracle is only smart enough to know that it might and that it doesn't(*) hurt anything.
(*) there have been numerous bugs in previous versions where this _has_ hurt things by messing up the selectivity estimated by the CBO.
The presence of extended statistics is only one example reason of why Oracle might think it could benefit from this predicate. To find out if that is the reason in your case, you can look for extended statistics in your database like this:
SELECT * FROM dba_stat_extensions where table_name = 'LISTS';
Keep in mind, the Oracle CBO can create stat extensions on its own. So there could be extended stats that you didn't realize were there.

How to utilize TABLE ACCESS BY INDEX ROWID

I have a problem with a query that uses a FULL TABLE SCAN.
When this query runs on our UAT env, it uses a TABLE ACCESS BY INDEX ROWID, but in prod
it uses FULL TABLE SCAN. UAT runs much better than PROD.
We have the same tables and indexes structure in prod and uat.
I already tried rebuilding and recreating the indexes but the same explain plan is used.
Table and indexes statics were also updated.
Can you help me to make this query use INDEX access instead of FUll table access?
Please see below the explain plan of our prod and uat.
EXPLAIN PLAN PROD
=====================================================================
SQL> explain plan for
SELECT ASV_ODC_BRANCH.CODE, ASV_ODC_BRANCH.DESCRIPTION, ASV_ODC_BRANCH.BRSTN, DEB.VIEW_DORMANT.ACCTNO AS DORMANT_ACCT,
DEB.VIEW_DORMANT.SHORTNAME AS DORMANT_NAME, DEB.VIEW_DORMANT.OPID_ENTRY, DEB.CUSTOMER.CUSTOMERNO,
DEB.CUSTOMER.TIME_STAMP_ENTRY
FROM ASV_ODC_BRANCH, DEB.VIEW_DORMANT, DEB.CUSTOMER
WHERE trim(ASV_ODC_BRANCH.CODE) = decode(SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 3, 1) || SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 7, 1), ’29’,
SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 4, 3), SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 3, 3)) AND
DEB.VIEW_DORMANT.ACCTNO = DEB.CUSTOMER.CUSTOMERNO AND (DEB.VIEW_DORMANT.ACCTNO = :Xacct)
ORDER BY ASV_ODC_BRANCH.CODE, DORMANT_ACCT;
Explained.
PLAN_TABLE_OUTPUT | Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
| 0 | SELECT STATEMENT | | 3 | 489 | 3601 (2)|
| 1 | SORT ORDER BY | | 3 | 489 | 3601 (2)|
| 2 | HASH JOIN | | 3 | 489 | 3600 (2)|
| 3 | MERGE JOIN CARTESIAN | | 1 | 90 | 3595 (2)|
| 4 | NESTED LOOPS | | 1 | 66 | 3592 (2)|
| 5 | **TABLE ACCESS FULL** | ACCOUNT | 1 | 56 | 3590 (2)|
| 6 | TABLE ACCESS BY INDEX ROWID| EXTENSION1 | 1 | 10 | 2 (0)|
| 7 | INDEX UNIQUE SCAN | PKEXT10 | 1 | | 1 (0)|
| 8 | BUFFER SORT | | 1 | 24 | 3593 (2)|
| 9 | TABLE ACCESS BY INDEX ROWID| CUSTOMER | 1 | 24 | 3 (0)|
| 10 | INDEX RANGE SCAN | UXCUST1 | 1 | | 2 (0)|
| 11 | TABLE ACCESS FULL | ASV_ODC_BRANCH | 334 | 24382 | 5 (0)|
**EXPLAIN PLAN UAT**
======================================================================================
SQL> explain plan for
SELECT ASV_ODC_BRANCH.CODE, ASV_ODC_BRANCH.DESCRIPTION, ASV_ODC_BRANCH.BRSTN, DEB.VIEW_DORMANT.ACCTNO AS DORMANT_ACCT,
DEB.VIEW_DORMANT.SHORTNAME AS DORMANT_NAME, DEB.VIEW_DORMANT.OPID_ENTRY, DEB.CUSTOMER.CUSTOMERNO,
DEB.CUSTOMER.TIME_STAMP_ENTRY
FROM ASV_ODC_BRANCH, DEB.VIEW_DORMANT, DEB.CUSTOMER
WHERE trim(ASV_ODC_BRANCH.CODE) = decode(SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 3, 1) || SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 7, 1), ’29’,
SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 4, 3), SUBSTR(DEB.VIEW_DORMANT.ACCTNO, 3, 3)) AND
DEB.VIEW_DORMANT.ACCTNO = DEB.CUSTOMER.CUSTOMERNO AND (DEB.VIEW_DORMANT.ACCTNO = :Xacct)
ORDER BY ASV_ODC_BRANCH.CODE, DORMANT_ACCT;
Explained.
SQL> /
PLAN_TABLE_OUTPUT
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
| 0 | SELECT STATEMENT | | 5 | 5930 | 19 (11)|
| 1 | SORT ORDER BY | | 5 | 5930 | 19 (11)|
| 2 | HASH JOIN | | 5 | 5930 | 18 (6)|
| 3 | MERGE JOIN CARTESIAN | | 2 | 2220 | 12 (0)|
| 4 | NESTED LOOPS | | 1 | 1085 | 9 (0)|
| 5 | **TABLE ACCESS BY INDEX ROWID**| ACCOUNT | 1 | 57 | 7 (0)|
| 6 | INDEX SKIP SCAN | UXACCT2 | 1 | | 6 (0)|
| 7 | TABLE ACCESS BY INDEX ROWID| EXTENSION1 | 1 | 1028 | 2 (0)|
| 8 | INDEX UNIQUE SCAN | PKEXT10 | 1 | | 1 (0)|
| 9 | BUFFER SORT | | 1 | 25 | 10 (0)|
| 10 | TABLE ACCESS BY INDEX ROWID| CUSTOMER | 1 | 25 | 3 (0)|
| 11 | INDEX RANGE SCAN | UXCUST1 | 1 | | 2 (0)|
| 12 | TABLE ACCESS FULL | ASV_ODC_BRANCH | 336 | 25536 | 5 (0)|
The difference is in
EXPLAIN PLAN PROD
| 5 | **TABLE ACCESS FULL** | ACCOUNT | 1 | 56 | 3590 (2)|
EXPLAIN PLAN UAT
| 5 | **TABLE ACCESS BY INDEX ROWID**| ACCOUNT | 1 | 57 | 7 (0)|
| 6 | INDEX SKIP SCAN | UXACCT2 | 1 | | 6 (0)|
How does it work?
Rather than restricting the search path using a predicate from the statement, Skip Scans are initiated by probing the index for distinct values of the prefix column. Each of these distinct values is then used as a starting point for a regular index search. The result is several separate searches of a single index that, when combined, eliminate the affect of the prefix column. Essentially, the index has been searched from the second level down.
The optimizer uses statistics to decide if a skip scan would be more efficient than a full table scan.
Optimizer considers his as an advantage over a FTS because
It reduces the number of indexes needed to support a range of queries. This increases performance by reducing index maintenance and decreases wasted space associated with multiple indexes.
The prefix column should be the most discriminating and the most widely used in queries. These two conditions do not always go hand in hand which makes the decision difficult. In these situations skip scanning reduces the impact of makeing the "wrong" decision.
You can consider the following
Check the optimizer mode across the environments.
Gather stats on all the tables used in the query. For example, if a table has not been analyzed since it was created, and if it has less than DB_FILE_MULTIBLOCK_READ_COUNT blocks under the high water mark, then the optimizer thinks that the table is small and uses a full table scan. Review the LAST_ANALYZED and BLOCKS columns in the ALL_TABLES table to examine the statistics.
Though your environment is similar and code is same, optimizer is going to check on the fly and choose the best available method. So do your UAT with same data setup. Since it is a UAT (almost a preproduction in most o the companies), it should be the closest to production in terms of size.
From what I can see, table DEB.VIEW_DORMANT is a view on tables ACCOUNT and EXTENSION1, and you'd like to use index UXACCT2 from the former. I guess a hint inside this request should allow you to do what you want, something like:
SELECT /*+ INDEX(D UXACCT2) */ ASV_ODC_BRANCH.CODE,
...
FROM ASV_ODC_BRANCH, DEB.VIEW_DORMANT D, DEB.CUSTOMER
...
PS: if this is a query you manage (not generated by any high-level software), I suggest you use aliases for your table as I did, that makes queries so much more readable...
Can I help you to make it a INDEX-ACCESS instead of a FTS? Probably...
Do you really want that? - Probably NOT
Why is the database behaving differently? Because you operate on different data - As your sample shows, the query returns a different number of rows, so I don't even have to ask if your production and test-data is the same. If you have different data (different amounts or different values in indexed columns) you Database-Stats will be different, your indexes will look different and so the optimizer will come to a different query-plan!
What should you do? Make sure all you indexes are up to date, your partitions are sanely arranged, all you database-stats are up-to-date and no strange tuning-settings are in place ( query-plans, environment settings...) Then the optimizer will in most cases find the best plan - and in many cases a full-table-scan is the FASTER alternative.
But if you measure the times and the optimizer clearly takes the wrong path, although it has accurate table-stats, you should file a bug-report with oracle.
If you have other reasons for wanting the optimizer to do indexed-access:
If you cannot enter an Optimizer-HINT like Emmanuel offered, you can try Profiles or Baselines, which offer nice tuning possibilities. You can write your own statement, with different WHERE-Predicates until you get a plan with index-access and use this as an SQL-Profile and link this profile to the original statement which will then use the same query-plan.

Resources