ORACLE Performance: (Hash) Join vs Subselect With Order By's in Huge Tables - oracle

I've got the following (abstract) problem in ORACLE 11g:
There are two tables called STRONG and WEAK.
The primary key of WEAK is a compound consisting of the primary key of STRONG (called pk) plus an additional column (called fk).
Thus we have a generic 1:N relationship.
However, in the real world application, there is always exactly one row within the N WEAK rows related to an entry of STRONG that has a special relationship.
This is why there is an additional 1:1 relationship realised by adding the column fk to STRONG as well.
Furthermore, it might be worth noting that both tables are huge but well indexed.
The overall picture looks like this:
Now, I have to define a view showing rows of STRONG along with some additional columns linked by that 1:1 relationship. I tried two basic approaches:
Subselects
SELECT
(SELECT some_data1 FROM weak WHERE weak.pk = strong.pk AND weak.fk = strong.fk) some_data1,
(SELECT some_data2 FROM weak WHERE weak.pk = strong.pk AND weak.fk = strong.fk) some_data2
FROM strong
Left Outer Join
SELECT
weak.some_data1,
weak.some_data2
FROM strong
LEFT OUTER JOIN weak ON weak.pk = strong.pk AND weak.fk = strong.fk
I first thought that the "Left Outer Join"-way has to be better and I still think that this is true as long as there is no WHERE/ORDER_BY-clause. However, in the real world application, user query dialog inputs are dynamically
translated into extensions of the above statements. Typically, the user knows the primary key of STRONG resulting in queries like this:
SELECT *
FROM the_view
WHERE the_view.pk LIKE '123%' --Or even the exact key
ORDER BY the_view.pk
Using the "Left Outer Join"-way, we encountered some very serious performance problems, even though most of these SELECTs only return a few rows. I think what happened is that the hash table did not fit into the
memory resulting in way too many I/O-events. Thus, we went back to the Subselects.
Now, i have a few questions:
Q1
Does Oracle have to compute the entire hash table for every SELECT (with ORDER_BY)?
Q2
Why is the "Subselect"-way faster? Here, it might be worth noting that these columns can appear in the WHERE-clause as well.
Q3
Does it somehow matter that joining the two tables might potentially increase the number of selcted rows? If so: Can we somehow tell Oracle that this can never happen from a logical perspective?
Q4
In case that the "Left Outer Join"-Way is not a well-performing option: The "Subselect"-way does seem somewhat redundant. Is there a better way?
Thanks a lot!
EDIT
Due to request, I will add an explanation plan of the actual case. However, there are a few important things here:
In the above description, I tried to simplify the actual problem. In the real world, the view is a lot more complex.
One thing I left out due to simplification is that the performance issues mainly occur when using the STRONG => WEAK-Join in a nested join (see below). The actual situation looks like this:
ZV is the name of our target view - the explanation plan below refers to that view.
Z (~3M rows) join T (~1M rows)
T joins CCPP (~1M rows)
TV is a view based on T. Come to think of it... this might be critical. The front end application sort of restricts us in the way we define these views: In ZV, we have to join TV instead of T and we can not implement that T => CCPP-join in TV, forcing us to define the join TV => CCPP as a nested join.
We only encountered the performance problems in our productive environment with lots of user. Obviously, we had to get rid of these problems. Thus, it can not be reproduced right now. The response time of the statements below are totally fine.
The Execution Plan
---------------------------------------------------------------------------- ----------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)|
---------------------------------------------------------------------------- ----------------------------------
| 0 | SELECT STATEMENT | | 717K| 73M| | 13340 (2)|
| 1 | HASH JOIN OUTER | | 717K| 73M| 66M| 13340 (2)|
| 2 | VIEW | | 687K| 59M| | 5 (0)|
| 3 | NESTED LOOPS OUTER | | 687K| 94M| | 5 (0)|
| 4 | NESTED LOOPS OUTER | | 1 | 118 | | 4 (0)|
| 5 | TABLE ACCESS BY INDEX ROWID | Z | 1 | 103 | | 3 (0)|
| 6 | INDEX UNIQUE SCAN | SYS_C00245876 | 1 | | | 2 (0)|
| 7 | INDEX UNIQUE SCAN | SYS_C00245876 | 1798K| 25M| | 1 (0)|
| 8 | VIEW PUSHED PREDICATE | TV | 687K| 17M| | 1 (0)|
| 9 | NESTED LOOPS OUTER | | 1 | 67 | | 2 (0)|
| 10 | TABLE ACCESS BY INDEX ROWID| T | 1 | 48 | | 2 (0)|
| 11 | INDEX UNIQUE SCAN | SYS_C00245609 | 1 | | | 1 (0)|
| 12 | INDEX UNIQUE SCAN | SYS_C00254613 | 1 | 19 | | 0 (0)|
| 13 | TABLE ACCESS FULL | CCPP | 5165K| 88M| | 4105 (3)|
--------------------------------------------------------------------------------------------------------------

The real question is - how many records does your query return?
10 records only or 10.000 (or 10M) and you expect to see the first 10 rows quickly?
For the letter case the subquery solution works indeed better as you need no sort and you lookup the WEAK table only small number of times.
For the former case (i.e. the number of selected rows in both table is small) I'd expect execution plan as follows:
--------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 4 | 336 | 100 (1)| 00:00:02 |
| 1 | SORT ORDER BY | | 4 | 336 | 100 (1)| 00:00:02 |
| 2 | NESTED LOOPS OUTER | | 4 | 336 | 99 (0)| 00:00:02 |
| 3 | TABLE ACCESS BY INDEX ROWID| STRONG | 4 | 168 | 94 (0)| 00:00:02 |
|* 4 | INDEX RANGE SCAN | STRONG_IDX | 997 | | 4 (0)| 00:00:01 |
| 5 | TABLE ACCESS BY INDEX ROWID| WEAK | 1 | 42 | 2 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | WEAK_IDX | 1 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("STRONG"."PK" LIKE '1234%')
filter("STRONG"."PK" LIKE '1234%')
6 - access("WEAK"."PK"(+)="STRONG"."PK" AND "WEAK"."FK"(+)="STRONG"."FK")
filter("WEAK"."PK"(+) LIKE '1234%')
If you see FULL TABLE SCAN on one or other table - the optimizes impression could be that the predicate pk LIKE '123%' will return too much records and the index access will be slower.
This could be a good or bad guess, so you may need to check your table statistics and cardinality estimation.
Some additional information follows
Q1
If Oracle performs a HASH JOIN the whole data source (typically the smaller one) must be read in memory
in the hash table. This is the whole table or the part of it as filtered by the WHERE/ON clause.
(In your case only records with pk LIKE '123%' )
Q2
This may be only an impression, as you see quickly first records. The subquery is performed
only for the first few fetched rows.
To know the exact answer you must examine (or post) the execution plans.
Q3
No, sorry, joining of the two tables NEVER potentially increase the number of selcted rows but returns exact the number of rows
as defined in the SQL standard.
It is your responsibility to define the join on a unique / primary key to avoid duplication.
Q4
You may of course select something like some_data1 ||'#'||some_data2 in the subquery, but it is in your responsibility
to decide if it is safe..

Related

Ignore the date range parameter in the where clause when parameter is not entered

Ignore the date range parameter in the where clause when parameter is not entered. For my date range im using Between.
These parameter are being entered from jasper report
SELECT *
from customer
where client_id = $P{CLIENT_ID}
AND (Account_id = CASE WHEN $P{Account_ID}>0
THEN $P{Account_ID}
ELSE Account_ID END
OR Account_ID IS NULL )
AND datetrx BETWEEN $P{DATE_START} AND $P{DATE_END}
if date is not entered the report should bring records of any dates, since date range is not entered
You have two possibilities to approach the optional input paramaters.
The simpler way is to use static SQL and providing default value for the missing parameters, so that you get all matches.
Here you simple sets the boundaries to the minimum and maximum possible DATE.
select *
from customer
where customer_id = $P{CLIENT_ID}
and datetrx between nvl($P{DATE_START},date'1900-01-01')
and nvl($P{DATE_END},date'2200-01-01')
The more advanced way was popularized by Tom Kyte and is based on using dynamic SQL.
If the paramaters are provided, you generate normal SQL with the BETWEEN predicate:
select *
from customer
where customer_id = $P{CLIENT_ID}
and datetrx between $P{DATE_START} and $P{DATE_END}
In case the parameter are missing (i.e. NULL is passed) you generate a different SQL as shown below.
select *
from customer
where customer_id = $P{CLIENT_ID}
and (1=1 or datetrx between $P{DATE_START} and $P{DATE_END})
Note, that
1) the number of the bind variables is the same in both variants of the query, which is important as you can use identical setXXXX statements
2) due to the shortcut 1 = 1 or is the between predicate ignored, i.e. all dates are considered.
Which option should be used?
Well for simple queries there will be small difefrence, but for complex queries with several options of missing parameters and large data, the dynamic SQL approach is preferred.
The reason is, that using static SQL you use the same statement for more different queries - here one for access with data range and one for access without data range.
The dynamic option produces different SQL for each access.
You may see it on the execution plans:
Access with date range
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 22 | 1 (0)| 00:00:01 |
|* 1 | FILTER | | | | | |
|* 2 | INDEX RANGE SCAN| CUST_IDX1 | 1 | 22 | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(TO_DATE(:1)<=TO_DATE(:2))
2 - access("CUSTOMER_ID"=1 AND "DATETRX">=:1 AND "DATETRX"<=:2)
Access without data range
------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 22 | 1 (0)| 00:00:01 |
|* 1 | INDEX RANGE SCAN| CUST_IDX1 | 1 | 22 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("CUSTOMER_ID"=1)
Both statements produce different execution plan, that is optimised for the input parameter. In the static option use must share the same execution plan for all input which may cause problems.

Oracle Function based index returning slowly

I have a table setup that contains some 640m records and I'm trying to create an index.
The manner in which I want to select records involves something like this:
index_i9(ORA_HASH(placard_bcd,128),event);
However that still returns about 3-4 million records, and from my testing, it takes a length amount of time (~12 minutes or so).
Is this a bad idea as an index? I don't think getting 3-4m records should take that long.
Any ideas?
Edit (adding more info):
The table has a bunch of columns but I don't know if I need to list all of them:
table_a
container NOT NULL NUMBER(19),
placard_bcd NOT NULL VARCHAR2(30),
event NOT NULL VARCHAR(5),
bin_number NUMBER(3),
...
...
It takes about 12 minutes to return all of the records that would return based on the index above. So to provide me all 3-4 million records.
The query used looks something like this:
select barcode, event, bin_number
from table_a
where ora_hash(barcode,128) = 105;
and event in ('CLOS','PASG','BUILD');
The explain plan provided is this:
Plan hash value: 4185630329
-----------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 35074 | 4212K| 338 (0)| 00:00:01 |
| 1 | INLIST ITERATOR | | | | | |
| 2 | TABLE ACCESS BY INDEX ROWID BATCHED| TABLE_A | 35074 | 4212K| 338 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | TABLE_A_I9 | 14030 | | 14 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access(("EVENT_TYPE"='BUILD' OR "EVENT_TYPE"='CLOS' OR "EVENT_TYPE"='PASG') AND
ORA_HASH("PLACARD_BCD",128)=105)
Everything seems correct, but it still is taking a while to provide me with the records.

Execution Plan of Inner Query different when run as part of a larger query

I faced a puzzling situation. A query had a good execution plan. But when that query was used as an inner query inside a larger query, that plan changed. I am trying to understand why it might be so.
This was on Oracle 11g. My query was:
SELECT * FROM YFS_SHIPMENT_H
WHERE SHIPMENT_KEY IN
(
SELECT DISTINCT SHIPMENT_KEY
FROM YFS_SHIPMENT_LINE_H
WHERE ORDER_HEADER_KEY = '20150113083918815889858'
OR ( ORDER_LINE_KEY IN ( '20150113084438815896336') )
);
As you can see, there is an inner query here, which is:
SELECT DISTINCT SHIPMENT_KEY
FROM YFS_SHIPMENT_LINE_H
WHERE ORDER_HEADER_KEY = '20150113083918815889858'
OR ( ORDER_LINE_KEY IN ( '20150113084438815896336') )
When I run just the inner query, I get the execution plan as:
PLAN_TABLE_OUTPUT
========================================================================================================
SQL_ID 3v82m4j5tv1k3, child number 0
=====================================
SELECT DISTINCT SHIPMENT_KEY FROM YFS_SHIPMENT_LINE_H WHERE
ORDER_HEADER_KEY = '20150113083918815889858' OR ( ORDER_LINE_KEY IN (
'20150113084438815896336') )
Plan hash value: 3691773903
========================================================================================================
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
========================================================================================================
| 0 | SELECT STATEMENT | | | | 10 (100)| |
| 1 | HASH UNIQUE | | 7 | 525 | 10 (10)| 00:00:01 |
| 2 | CONCATENATION | | | | | |
| 3 | TABLE ACCESS BY INDEX ROWID| YFS_SHIPMENT_LINE_H | 1 | 75 | 4 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | YFS_SHIPMENT_LINE_H_I4 | 1 | | 3 (0)| 00:00:01 |
|* 5 | TABLE ACCESS BY INDEX ROWID| YFS_SHIPMENT_LINE_H | 6 | 450 | 5 (0)| 00:00:01 |
|* 6 | INDEX RANGE SCAN | YFS_SHIPMENT_LINE_H_I6 | 6 | | 3 (0)| 00:00:01 |
========================================================================================================
Predicate Information (identified by operation id):
===================================================
4 = access("ORDER_LINE_KEY"='20150113084438815896336')
5 = filter(LNNVL("ORDER_LINE_KEY"='20150113084438815896336'))
6 = access("ORDER_HEADER_KEY"='20150113083918815889858')
The execution plan shows that the table YFS_SHIPMENT_LINE_H is accessed with two indexes YFS_SHIPMENT_LINE_H_I4 and YFS_SHIPMENT_LINE_H_I6; and then the results are concatenated. This plan seems fine and the query response time is great.
But when I run the complete query, the access path of the inner query changes as given below:
PLAN_TABLE_OUTPUT
=======================================================================================================
SQL_ID dk1bp8p9g3vzx, child number 0
=====================================
SELECT * FROM YFS_SHIPMENT_H WHERE SHIPMENT_KEY IN ( SELECT DISTINCT
SHIPMENT_KEY FROM YFS_SHIPMENT_LINE_H WHERE ORDER_HEADER_KEY =
'20150113083918815889858' OR ( ORDER_LINE_KEY IN (
'20150113084438815896336') ) )
Plan hash value: 3651083773
=======================================================================================================
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
=======================================================================================================
| 0 | SELECT STATEMENT | | | | 12593 (100)| |
| 1 | NESTED LOOPS | | | | | |
| 2 | NESTED LOOPS | | 7 | 6384 | 12593 (1)| 00:02:32 |
| 3 | SORT UNIQUE | | 7 | 525 | 12587 (1)| 00:02:32 |
|* 4 | INDEX FAST FULL SCAN | YFS_SHIPMENT_LINE_H_I2 | 7 | 525 | 12587 (1)| 00:02:32 |
|* 5 | INDEX UNIQUE SCAN | YFS_SHIPMENT_H_PK | 1 | | 1 (0)| 00:00:01 |
| 6 | TABLE ACCESS BY INDEX ROWID| YFS_SHIPMENT_H | 1 | 837 | 2 (0)| 00:00:01 |
=======================================================================================================
Predicate Information (identified by operation id):
===================================================
4 = filter(("ORDER_HEADER_KEY"='20150113083918815889858' OR
"ORDER_LINE_KEY"='20150113084438815896336'))
5 = access("SHIPMENT_KEY"="SHIPMENT_KEY")
Please note that the YFS_SHIPMENT_LINE_H is now being accessed with a different index (YFS_SHIPMENT_LINE_H_I2). As it turns out, this is not a very good index and the query response time suffers.
My question is: Why would the inner query execution plan change when it is run as part of the larger query? Once the optimizer has figured out the best way to access YFS_SHIPMENT_LINE_H, why wouldn't it continue to use the same execution plan even when it is part of the larger query?
Note: I am not too concerned about what would be the correct access path or the index to use; and hence not giving all the indexes on the table here; and the cardinality of the data. My concern is about the change when executed separately versus as part of another query.
Thanks.
-- Parag
I'm not sure why the Oracle optimizer decides to change the execution path. But, I think this is a better way to write the query:
SELECT s.*
FROM YFS_SHIPMENT_H s
WHERE s.SHIPMENT_KEY IN (SELECT sl.SHIPMENT_KEY
FROM YFS_SHIPMENT_LINE_H sl
WHERE sl.ORDER_HEADER_KEY = '20150113083918815889858'
) OR
s.SHIPMENT_KEY IN (SELECT sl.SHIPMENT_KEY
FROM YFS_SHIPMENT_LINE_H sl
WHERE sl.ORDER_LINE_KEY IN ('20150113084438815896336')
);
Notes:
There is no need to have SELECT DISTINCT in a subquery for IN. I'm pretty sure that Oracle ignores it, but it could add overhead.
Splitting the logic into two queries makes it more likely that Oracle can use indexes for the query (the best ones are on YFS_SHIPMENT_LINE_H(ORDER_HEADER_KEY, SHIPMENT_KEY) and YFS_SHIPMENT_LINE_H(ORDER_LINE_KEY, SHIPMENT_KEY)).
In the first query (not used as a subquery), the base table is accessed based on the conditions in the where clause. The indexes on the two columns involved are used for accessing the rows.
In the complex query, you are doing a semi-join. The optimizer, rightly or wrongly, has decided that it is more efficient to read the rows from the shipment table first, read the shipment_key, and use the index on shipment_key in the shipment_line table to retrieve rows to see if they are a match. The where clause conditions on the shipment_line table are now just filter predicates, they are not used to decide which rows to be retrieved from the table.
If you feel the optimizer got it wrong (which is possible, although not often with relatively simple queries like this one), make sure statistics are up-to-date. What would be relevant here is the size of each table, how many rows on average have the same shipment_key in shipment_line, and the selectiveness of the conditions in the where clause in the subquery. Keep in mind that for the outer query, it is not necessary to compute the subquery in full (and very likely Oracle does not compute it in full); for each row from the shipment table, as soon as a matching row in the shipment_line table is found that satisfies the where clause, the search for that shipment_key in shipment_line stops.
One thing you can do, if you really think the optimizer got it wrong, is to see what happens if you use hints. For example, you can tell the optimizer not to use the I2 index on shipment_line (pretend it doesn't exist) - see what plan it will come up with.
The join on shipment_key forces the optimizer to use the most selective index, in this case, the YFS_SHIPMENT_LINE_H_I2 index. Sterling created this index for this query and it is WRONG. Drop it (or make invisible) and watch your query pick up the correct plan. If you are hesitant to drop the index since it is part of the Sterling product, use SQL Plan Management baselines.
YFS_SHIPMENT_LINE_H_I2 SHIPMENT_KEY 1
YFS_SHIPMENT_LINE_H_I2 ORDER_HEADER_KEY 2
YFS_SHIPMENT_LINE_H_I2 ORDER_RELEASE_KEY 3
YFS_SHIPMENT_LINE_H_I2 ORDER_LINE_KEY 4
YFS_SHIPMENT_LINE_H_I2 REQUESTED_TAG_NUMBER 5

Improving SQL Exists scalability

Say we have two tables, TEST and TEST_CHILDS in the following way:
creat TABLE TEST(id1 number PRIMARY KEY, word VARCHAR(50),numero number);
creat TABLE TEST_CHILD (id2 number references test(id), word2 VARCHAR(50));
CREATE INDEX TEST_IDX ON TEST_CHILD(word2);
CREATE INDEX TEST_JOIN_IDX ON TEST_CHILD(id);
insert into TEST SELECT ROWNUM,U1.USERNAME||U2.TABLE_NAME, LENGTH(U1.USERNAME) FROM ALL_USERS U1,ALL_TABLES U2;
INSERT INTO TEST_CHILD SELECT MOD(ROWNUM,15000)+1,U1.USER_ID||U2.TABLE_NAME FROM ALL_USERS U1,ALL_TABLES U2;
We would like to query to get rows from TEST table that satisfy some criteria in the child table, so we go for:
SELECT /*+FIRST_ROWS(10)*/* FROM TEST T WHERE EXISTS (SELECT NULL FROM TEST_CHILD TC WHERE word2 like 'string%' AND TC.id = T.id ) AND ROWNUM < 10;
We always want just the first 10 results, not any more at all. Therefore, we would like to get the same response time to read 10 results whether table has 10 matching values or 1,000,000; since it could get 10 distinct results from the child table and get the values on the parent table (or at least that is the plan that we would like). But when checking the actual execution plan we see:
-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 54 | 5 (20)| 00:00:01 |
|* 1 | COUNT STOPKEY | | | | | |
| 2 | NESTED LOOPS | | | | | |
| 3 | NESTED LOOPS | | 1 | 54 | 5 (20)| 00:00:01 |
| 4 | SORT UNIQUE | | 1 | 23 | 3 (0)| 00:00:01 |
| 5 | TABLE ACCESS BY INDEX ROWID| TEST_CHILD | 1 | 23 | 3 (0)| 00:00:01 |
|* 6 | INDEX RANGE SCAN | TEST_IDX | 1 | | 2 (0)| 00:00:01 |
|* 7 | INDEX UNIQUE SCAN | SYS_C005145 | 1 | | 0 (0)| 00:00:01 |
| 8 | TABLE ACCESS BY INDEX ROWID | TEST | 1 | 31 | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(ROWNUM<10)
6 - access("WORD2" LIKE 'string%')
filter("WORD2" LIKE 'string%')
7 - access("TC"."ID"="T"."ID")
SORT UNIQUE under the STOPKEY, what afaik means that it is reading all results from the child table, making the distinct to finally select only the first 10, making the query not as scalable as we would like it to be.
Is there any mistake in my example?
Is it possible to improve this execution plan so it scales better?
The SORT UNIQUE is going to find and sort all of the records from TEST_CHILD that matched 'string%' - it is NOT going to read all results from child table. Your logic requires this. IF you only picked the first 10 rows from TEST_CHILD that matched 'string%', and those 10 rows all had the same ID, then your final results from TEST would only have 1 row.
Anyway, your performance should be fine as long as 'string%' matches a relatively low number of rows in TEST_CHILD. IF your situation is such that 'string%' often matches a HUGE record count on TEST_CHILD, there's not much you can do to make the SQL more performant given the current tables. In such a case, if this is a mission-critical SQL, with performance tied to your annual bonus, there's probably some fancy footwork you could do with MATERIALIZED VIEWs to, e.g. pre-compute 10 TEST rows for high-cardinality WORD2 values in TEST_CHILD.
One final thought - a "risky" solution, but one which should work if you don't have thousands of TEST_CHILD rows matching the same TEST row, would be the following:
SELECT *
FROM TEST
WHERE ID1 IN
(SELECT ID2
FROM TEST_CHILD
WHERE word2 like 'string%'
AND ROWNUM < 1000)
AND ROWNUM <10;
You can adjust 1000 up or down, of course, but if it's too low, you risk finding less than 10 distinct ID values, which would give you final results with less than 10 rows.

Make Oracle 9i use indexes

I'm having a performance issue when deploying an app developed on 10g XE in a client's 9i server. The same query produces completely different query plans depending on the server:
SELECT DISTINCT FOO.FOO_ID AS C0,
GEE.GEE_CODE AS C1,
TO_CHAR(FOO.SOME_DATE, 'DD/MM/YYYY') AS C2,
TMP_FOO.SORT_ORDER AS SORT_ORDER_
FROM TMP_FOO
INNER JOIN FOO ON TMP_FOO.FOO_ID=FOO.FOO_ID
LEFT JOIN BAR ON FOO.FOO_ID=BAR.FOO_ID
LEFT JOIN GEE ON FOO.GEE_ID=GEE.GEE_ID
ORDER BY SORT_ORDER_;
Oracle Database 10g Express Edition Release 10.2.0.1.0 - Production:
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 67 | 10 (30)| 00:00:01 |
| 1 | SORT UNIQUE | | 1 | 67 | 9 (23)| 00:00:01 |
| 2 | NESTED LOOPS OUTER | | 1 | 67 | 8 (13)| 00:00:01 |
|* 3 | HASH JOIN OUTER | | 1 | 48 | 7 (15)| 00:00:01 |
| 4 | NESTED LOOPS | | 1 | 44 | 3 (0)| 00:00:01 |
| 5 | TABLE ACCESS FULL | TMP_FOO | 1 | 26 | 2 (0)| 00:00:01 |
| 6 | TABLE ACCESS BY INDEX ROWID| FOO | 1 | 18 | 1 (0)| 00:00:01 |
|* 7 | INDEX UNIQUE SCAN | FOO_PK | 1 | | 0 (0)| 00:00:01 |
| 8 | TABLE ACCESS FULL | BAR | 1 | 4 | 3 (0)| 00:00:01 |
| 9 | TABLE ACCESS BY INDEX ROWID | GEE | 1 | 19 | 1 (0)| 00:00:01 |
|* 10 | INDEX UNIQUE SCAN | GEE_PK | 1 | | 0 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------
Oracle9i Release 9.2.0.1.0 - 64bit Production:
----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 98M| 6546M| | 3382K|
| 1 | SORT UNIQUE | | 98M| 6546M| 14G| 1692K|
|* 2 | HASH JOIN OUTER | | 98M| 6546M| 137M| 2874 |
| 3 | VIEW | | 2401K| 109M| | 677 |
|* 4 | HASH JOIN OUTER | | 2401K| 169M| 40M| 677 |
| 5 | VIEW | | 587K| 34M| | 24 |
|* 6 | HASH JOIN | | 587K| 34M| | 24 |
| 7 | TABLE ACCESS FULL| TMP_FOO | 8168 | 207K| | 10 |
| 8 | TABLE ACCESS FULL| FOO | 7188 | 245K| | 9 |
| 9 | TABLE ACCESS FULL | BAR | 409 | 5317 | | 1 |
| 10 | TABLE ACCESS FULL | GEE | 4084 | 89848 | | 5 |
----------------------------------------------------------------------------
As far as I can tell, indexes exist and are correct. What are my options to make Oracle 9i use them?
Update #1: TMP_FOO is a temporary table and it has no rows in this test. FOO is a regular table with 13,035 rows in my local XE; not sure why the query plan shows 1, perhaps it's realising that an INNER JOIN against an empty table won't require a full table scan :-?
Update #2: I've spent a couple of weeks trying everything and nothing provided a real enhancement: query rewriting, optimizer hints, changes in DB design, getting rid of temp tables... Finally, I got a copy of the same 9.2.0.1.0 unpatched Oracle version the customer has (with obvious architecture difference), installed it at my site and... surprise! In my 9i, all execution plans come instantly and queries take from 1 to 10 seconds to complete.
At this point, I'm almost convinced that the customer has a serious misconfiguration issue.
it looks like either you don't have data on your 10g express database, or your statistics are not collected properly. In either case it looks to Oracle like there aren't many rows, and therefore an index-range scan is appropriate.
In your 9i database, the statistics look like they are collected properly and Oracle sees a 4-table join with lots of rows and without a where clause. In that case since you haven't supplied an hint, Oracle builds an explain plan with the default ALL_ROWS optimizer behaviour: Oracle will find the plan that is the most performant to return all rows to the last. In that case the HASH JOIN with full table scans is brutally efficient, it will return big sets of rows faster that with an index NESTED LOOP join.
Maybe you want to use an index because you are only interested in the first few rows of the query. In that case use the hint /*+ FIRST_ROWS*/ that will help Oracle understand that you are more interested in the first row response time than overall total query time.
Maybe you want to use an index because you think this would result in a faster total query time. You can force an explain plan through the use of hints like USE_NL and USE_HASH but most of the time you will see that if the statistics are up-to-date the optimizer will have picked the most efficient plan.
Update: I saw your update about TMP_FOO being a temporary table having no row. The problem with temporary table is that they have no stats so my above answer doesn't apply perfectly to temporary tables. Since the temp table has no stats, Oracle has to make a guess (here it chooses quite arbitrarly 8168 rows) which results in an inefficient plan.
This would be a case where it could be appropriate to use hints. You have several options:
A mix of LEADING, USE_NL and USE_HASH hints can force a specific plan (LEADING to set the order of the joins and USE* to set the join method).
You could use the undocumented CARDINALITY hint to give additional information to the optimizer as described in an AskTom article. While the hint is undocumented, it is arguably safe to use. Note: on 10g+ the DYNAMIC_SAMPLING could be the documented alternative.
You can also set the statistics on the temporary table beforehand with the DBMS_STATS.set_table_stats procedure. This last option would be quite radical since it would potentially modify the plan of all queries against this temp table.
It could be that 9i is doing it exactly right. According to the stats posted, the Oracle 9i database believes it is dealing with a statement returning 98 million rows, whereas the 10G database thinks it will return 1 row. It could be that both are correct, i.e the amount of data in the 2 databases is very very different. Or it could be that you need to gather stats in either or both databases to get a more accurate query plan.
In general it is hard to tune queries when the target version is older and a different edition. You have no chance of tuning a query without realistic volumes of data, or at least realistic statistics.
If you have a good relationship with your client you could ask them to export their statistics using DBMS_STATS.EXPORT_SCHEMA_STATS(). Then you can import the stats using the matching IMPORT_SCHEMA_STATS procedure.
Otherwise you'll have to fake the numbers yourself using the DBMS_STATS.SET_TABLE_STATISTICS() procedure. Find out more.
You could add the following hints which would "force" Oracle to use your indexes (if possible):
Select /*+ index (FOO FOO_PK) */
/*+ index (GEE GEE_PK) */
From ...
Or try to use the FIRST_ROWS hint to indicate you're not going to fetch all these estimated 98 Million rows... Otherwise I doubt the indexes would make a huge difference because you have no Where clause so Oracle would have to read these tables anyways.
The customer had changed a default setting in order to support a very old third-party legacy application: the static parameter OPTIMIZER_FEATURES_ENABLE had been changed from the default value in 9i (9.2.0) to 8.1.7.
I made the same change in a local copy of 9i and I got the same problems: explain plans that take hours to be calculated and so on.
(Knowing this, I've asked a related question at ServerFault, but I believe this solves the original question.)

Resources