Skip to Main Content
  • Questions
  • Outer Join Elimination with User Functions + Domain Indexes

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question, Ahmed.

Asked: November 09, 2012 - 4:02 am UTC

Last updated: November 09, 2012 - 7:53 am UTC

Version: 11.2.0

Viewed 1000+ times

You Asked

Hi Tom,

I have a view with an outer join between two tables, with a domain index on the second table. I want people to be able to run queries against this view, some of which would make use of the domain operator, in which case I'd like Oracle to use the domain index as invoking the corresponding function many times to perform a per-row filter is VERY expensive.

However, it never seems to want to use the Domain index, so the queries run terribly slowly. The problem seems to be that it doesn't seem to work out that the domain operator can be used to infer 'deficient column != null' therefore can transform the outer join to an inner join. It *does* do this transformation if you use a built-in operator.

Here's a grossly simplified schema

CREATE TABLE t1
(tuple_id varchar2(32) PRIMARY KEY,
somecol varchar2(20),
somecol2 varchar2(20)
....
...
)

CREATE TABLE t2
(property_id varchar2(32),
tuple_id varchar2(32),
property_value_string varchar2(2000),
property_value_num number(10)
alter table t2 add constraint t2_pk primary key(property_id, tuple_id)

[so this is a pivot table... not my fault as I am stuck with trying to make the domain bit work with someone else's imposed data model!]

the view is something like

CREATE VIEW txt_char
AS
SELECT a.*, b.property_value_string, b.property_value_num
from t1 a, t2 b
where a.tuple_id = b.tuple_id(+)
and '65DF3E6BEDD84112828C9DE1BAE251CB' = b.property_id(+);

So essentially the view pivots one column out.

Now, if I do

select /*+ FIRST_ROWS */ * from txt_char where rownum < 10

I get

SELECT STATEMENT, GOAL = FIRST_ROWS 167158 9 20934
COUNT STOPKEY
NESTED LOOPS OUTER 167158 165535 385034410
TABLE ACCESS FULL t1 1268 165535 31782720
TABLE ACCESS BY INDEX ROWID t2 2 1 2134
INDEX UNIQUE SCAN t2 1 1

Which is exactly what you'd expect - need to drive from the main table and glue on rows from the inner one if corresponding ones exist, so an outer join.

Now, if I do something like

select /*+ FIRST_ROWS */ * from txt_char t where t.property_value_string like '%abc%'

I get

SELECT STATEMENT, GOAL = HINT: FIRST_ROWS 50201 13 30238
NESTED LOOPS
NESTED LOOPS 50201 13 30238
TABLE ACCESS BY INDEX ROWID t2 50187 13 27742
INDEX RANGE SCAN t2_pk 544 43785
INDEX UNIQUE SCAN t1_pk 1 1
TABLE ACCESS BY INDEX ROWID t1 2 1 192

So it has succesfully inferred that it can transform the outer join to an inner and drive from the 'deficient' table t2.

Now, if I create a context index

create index t2_txt_idx on t2(property_value_string) indextype is ctxsys.context

and try and run

select /*+ FIRST_ROWS */ * from txt_char t where ctxsys.contains(t.property_value_string, 'abc') > 0

Then I get
SELECT STATEMENT, GOAL = FIRST_ROWS 167158 83 194054
FILTER
NESTED LOOPS OUTER 167158 83 194054
TABLE ACCESS FULL t1 1268 165535 31782720
TABLE ACCESS BY INDEX ROWID t2 2 1 2146
INDEX UNIQUE SCAN t2_pk 1 1

In other words, it hasn't recognized that the text predicate means that i'm not interested in any rows in t1 that don't occur in t2 and so it can eliminate the outer join and use the domain index.

Now, this is our own in-house domain index I'm really interested in, but I was unsure if maybe there was a flaw in our implementation that meant it could not make this induction, so I tried the context index for comparison but hit the same problem.

I tried investigating with event 10053 (which I haven't used very much) and found a bit of a smoking gun

It does indeed consider join elimination with the li9ke '%abc%' predicate and with the domain index.

It succeeds in the first case:

OJE: Begin: find best directive for query block SEL$F5BB74E1 (#0)
OJE: Considering outer-join elimination on query block SEL$F5BB74E1 (#0)
OJE: considering predicate"B"."PROPERTY_VALUE_STRING" LIKE '%abc%'

OJE: Converting outer join of t2 and t1 to inner-join.


In the second case you see the lines
OJE: Considering outer-join elimination on query block SEL$2 (#0)
rejected
OJE: outer-join not eliminated
OJE: End: finding best directive for query block SEL$2 (#0)
PL/SQL function (CONTAINS) is not secure.

Can you explain what that means? I'm wondering if it's something trivial like the function being DETERMINISTIC or not?
I think it's the user-function itself (rather than the domain index itself) associated with it, since I think it doesn't actually think about the domain index at this stage - first it needs to realise the function is safe to use to make the transformation to an inner join, whereupon it rebuilds the SQL, and then discovers that a domain index access path is available to it.

Many thanks for your help - hope this is not too esoteric for general consumption!

and Tom said...

contains, when passed a null, returns 0. Hence - if we made up a row - if we had to do the outer join and made up a row in T2, we would get zero back. Now, you know that, I know that - but the optimizer sure cannot make that leap of faith. It cannot just say "we don't need the outer join since contains on a null column would always return 0 and you are asking for things >0"


on the other hand when you say "column like '%abc%'", it can look at that and say to itself "if we made up a row in T2, column would be NULL - null is known to never be like anything - hence we don't need any of the outer joined rows"



You'd have to let us know that the NULL values of property_string_value don't need processing - right now, it appears they could (the optimizer HAS to assume that contains could return a value > 0 for a null value of property_value_string - it could after all (in theory, not in practice).

something like:


ops$tkyte%ORA11GR2> set autotrace traceonly explain
ops$tkyte%ORA11GR2> select * from txt_char t where contains(property_value_string,'abc')>0 and property_value_string is not null;

Execution Plan
----------------------------------------------------------
Plan hash value: 3136321841

----------------------------------------------------------------------------------------------
| Id  | Operation                     | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |              |    25 |  3550 |     3   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                 |              |       |       |            |          |
|   2 |   NESTED LOOPS                |              |    25 |  3550 |     3   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS BY INDEX ROWID| T2           |    25 |  2500 |     0   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN          | T2_PK        |  4000 |       |     0   (0)| 00:00:01 |
|*  5 |    INDEX UNIQUE SCAN          | SYS_C0043333 |     1 |       |     0   (0)| 00:00:01 |
|   6 |   TABLE ACCESS BY INDEX ROWID | T1           |     1 |    42 |     1   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter("B"."PROPERTY_VALUE_STRING" IS NOT NULL AND
              "CTXSYS"."CONTAINS"("B"."PROPERTY_VALUE_STRING",'abc')>0)
   4 - access("B"."PROPERTY_ID"='65DF3E6BEDD84112828C9DE1BAE251CB')
   5 - access("A"."TUPLE_ID"="B"."TUPLE_ID")

ops$tkyte%ORA11GR2> set autotrace off

Rating

  (4 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Brilliant - Now I am trying to force it to make that inference!

Ahmed Hussain, November 09, 2012 - 8:32 am UTC

Brilliant!

I did eventually twig this after asking the question, so have been attacking the problem on those lines.

Of course, Oracle has no way of knowing that my function isn't just something like NVL

I have had some success with this approach, but am seeing different behaviour depending on exactly how I code the query and how I force it to figure out that the column with the domain predicate cannot be NULL.

My actual problem is a little more complex than I specified above - my domain column is a BLOB with our own domain index on it, but I simplified the problem for purposes of comparison.

Also, the domain column, as a LOB, is actually stored in a third table with a simple numerical primary key to link it into the pivot table.

So we have
CREATE TABLE t1
(TUPLE_ID varchar2(32) PRIMARY KEY,
somecol1 VARCHAR2(50),
somecol2 NUMBER...)

CREATE TABLE t2 -- EAV pivot table :-(
(TUPLE_ID varchar2(32),
PROPERTY_ID varchar2(32),
PROPERTY_VALUE_NUM NUMBER,
-- ...String and other property types
BLOB_ID NUMBER(9));

ALTER TABLE t2 ADD CONSTRAINT t2_pk PRIMARY KEY(TUPLE_ID, PROPERTY_ID)


CREATE TABLE t3 -- blob table
(DOM_ID NUMBER(9) PRIMARY KEY,
DOM_VALUE BLOB);

CREATE INDEX MY_DOM_IDX ON t3(CHEM_VALUE) INDEXTYPE IS MY_DOM_IDT


The actual view I am using for testing is like

create view weird as
select t.*, b.property_value_blob_id, b.property_value_num, c.DOM_ID, c.DOM_VALUE
from
t1 t,
t2 b,
t3c
where
t.tuple_id = b.tuple_id (+) -- Not every row in t1 has rows in t2
and
'22986A0707B743a5AF5761361734B2AF' = b.property_id (+)
and
b.BLOB_ID = c.DOM_ID(+) -- Not every property in t2 is a BLOB property...

Now, on my dev box,

I can do either
SELECT * FROM weird x
WHERE MY_OP(x.DOM_VALUE, ...) =1
AND x.DOM_VALUE IS NOT NULL

and it works perfectly

I can alternatively use
x.DOM_ID IS NOT NULL
so use the PK on the domain table t3. This is less ideal as the DOM_ID is a pure join artifice of the model that I don't really want in the views or for the application layer to have to be aware of.

However, on a different test server, it doesn't work (i.e. refuses to transform to an inner join to permit the domain index scan) with DOM_VALUE IS NOT NULL, but does with DOM_ID IS NOT NULL.

BUT - it works either way if I expand the SELECT * FROM weird into SELECT * FROM (... definition of WEIRD view) WHERE MY_OP(...)= 1 AND DOM_VALUE IS NOT NULL

Its obviously something to do with view expanding so I'm looking at the 10053 trace to try and find (a) what's different about the environments between the databases (11.2 on mine, 11.3 on the test server), (b) ways of encouraging the unnesting, (c) why it figures it out either way with the DOM_ID NOT NULL predicate but not the DOM_VALUE NOT NULL.

If I find a solution I'll post it, but any pointers most gratefully received.
[Apologies if this follow-up is a bit too far off-topic]
Many thanks again,
Ahmed


Correction - Definition of view

Ahmed Hussain, November 09, 2012 - 8:38 am UTC

Sorry, just to be clear

Sorry everyone, in my follow up above I wrote

create view weird as
select t.*, b.property_value_blob_id, b.property_value_num, c.chem_data_id, c.property_value_chem

should have read
select t.*, b.blob_id, ...
^^^^

I wasn't following an entirely consistent algorithm in renaming everything to try and simplify the test case to the essentials. Oops.

Correction - Difference between inline and full view reproducible between environments

Ahmed Hussain, November 09, 2012 - 10:25 am UTC

I have rechecked and think i was wrong that I saw different behaviour on 11.2 and 11.3

It's quite consistent:

If I do SELECT * from weird WHERE MY_OP(DOM_VALUE...) AND DOM_VALUE IS NOT NULL

it doesn't transform to an inner join,

but it does work if I use the domain/BLOB table's PK

SELECT * from weird WHERE MY_OP(DOM_VALUE ...) AND DOM_ID IS NOT NULL

However, if I replace weird with the text of the view definition (i.e. use an inline view) then it works either way.

Investigations continue...

Join Elimination - probable bug with LOB columns and views

Ahmed Hussain, November 13, 2012 - 8:58 am UTC

Much digging later, I think I have found a bug and raised an SR about it.

The problem is with query transformation, when the optimizer applies view merging and join elimination and the predicate is on a lob column.

Can reproduce very simply as

CREATE TABLE t1
AS SELECT * FROM ALL_OBJECTS WHERE ROWNUM < 10000;

ALTER TABLE t1 ADD CONSTRAINT t1_pk PRIMARY KEY(OBJECT_ID);
CREATE INDEX t1_idx ON t1(data_object_id);

CREATE TABLE t2_varchar
(id primary key,
interesting_col)
AS
SELECT object_id, OBJECT_NAME
FROM ALL_OBJECTS WHERE ROWNUM < 10000 and DATA_OBJECT_ID < 10000; -- make child table slightly sparse

CREATE TABLE t2_clob
(id primary key,
interesting_col)
AS
SELECT id, to_clob(interesting_col) FROM t2_varchar; -- clone table but with a CLOB

[gather stats]
CREATE OR REPLACE VIEW v1_varchar
AS
SELECT
a.*, b.id, b.interesting_col
FROM t1 a,
t2_varchar b
WHERE
a.object_type = 'TABLE' AND
a.data_object_id = b.id(+); -- bake in outer join

CREATE OR REPLACE VIEW v1_clob
AS
SELECT
a.*, b.id, b.interesting_col
FROM t1 a,
t2_clob b
WHERE
a.object_type = 'TABLE' AND
a.data_object_id = b.id(+); -- bake in outer join

-- Test Case 1:
-- Query against view with varchar2 as interesting column
-- Correctly transforms to a nested loop inner join
-- because of the predicate t.interesting_col is not null
select /*+ FIRST_ROWS */ * from
v1_varchar t
where length(t.interesting_col) = 5
and t.interesting_col is not null;

-- Test Case 2:
-- Join elimination fails - performs a nested loop outer join
-- Thus if length were a user function which could make use
-- of a domain index on t2_clob(interesting_col), the domain
-- index could not be used because the query is still left as
-- an outer join
select /*+ FIRST_ROWS */ * from
v1_clob t
where length(t.interesting_col) = 5
and t.interesting_col is not null;

-- Test Case 3
-- Exactly as Test Case 2, except with the text of the view v1_clob
-- embedded as an inline view
select /*+ FIRST_ROWS */ * from
(SELECT
a."OWNER",a."OBJECT_NAME",a."SUBOBJECT_NAME",a."OBJECT_ID",a."DATA_OBJECT_ID",a."OBJECT_TYPE",a."CREATED",a."LAST_DDL_TIME",a."TIMESTAMP",a."STATUS",a."TEMPORARY",a."GENERATED",a."SECONDARY",a."NAMESPACE",a."EDITION_NAME", b.id, b.interesting_col
FROM t1 a,
t2_clob b
WHERE
a.object_type = 'TABLE' AND
a.data_object_id = b.id(+)) t
where length(t.interesting_col) = 5
and t.interesting_col is not null;



More to Explore

Performance

Get all the information about database performance in the Database Performance guide.