2

I have a problem where the execution of a query is too slow in oracle 12c standard edition.

I figured out, the optimizer estimates wrong cardinalities because it misses the correlation between two columns in two different tables, so I tried to create a minimal example explaining the problem.

Given the following tables:

create table test_articles (id number(10,0), cat varchar2(40), CONSTRAINT test_articles_pk PRIMARY KEY (id));
create table test_sales (id number(10,0), article_id number(10,0), country varchar2(40), CONSTRAINT fk_article FOREIGN KEY (article_id) references test_articles (id));

Lets say this shop sales products of several categorys: Toys, Tools and Books. Most products are sold inside the UK, but products of category 'Toys' are mostly exported to France:

begin
for i in 1..100
loop
    insert into test_articles values(i,'Toys');
    insert into test_articles values(100 + i,'Toys');
    insert into test_articles values(200 + i,'Toys');
    insert into test_articles values(300 + i,'Tools');
    insert into test_articles values(400 + i,'Books');
end loop;

for i in 1..50
loop
    insert into test_sales values(i,1,'UK');
end loop;
for i in 51..1050
loop
    insert into test_sales values(i,2,'FR');
end loop;
for i in 1051..5000
loop
    insert into test_sales values(i,301,'UK');
end loop;
end;

I gathered statistics, forcing the creation of histograms and then searched for all Toys sold to the UK and checked the execution plan

exec dbms_stats.gather_table_stats(null,'TEST_SALES',method_opt => 'for all columns');
exec dbms_stats.gather_table_stats(null,'TEST_ARTICLES',method_opt => 'for all columns');

select /*+ gather_plan_statistics */ * from TEST_SALES s join TEST_ARTICLES a on s.ARTICLE_ID = a.ID
where a.cat = 'Toys' and s.country = 'UK';

select *
from table(dbms_xplan.display_cursor(format => 'allstats last advanced +adaptive'));

I got the following result, where - unsurprisingly - expected rows are way higher than actual rows.

---------------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation          | Name          | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time   | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |               |      1 |        |       |     8 (100)|          |     50 |00:00:00.01 |      18 |       |       |          |
|*  1 |  HASH JOIN         |               |      1 |   4000 |   324K|     8   (0)| 00:00:01 |     50 |00:00:00.01 |      18 |  1922K|  1922K| 1680K (0)|
|*  2 |   TABLE ACCESS FULL| TEST_ARTICLES |      1 |    300 | 10500 |     3   (0)| 00:00:01 |    300 |00:00:00.01 |       7 |       |       |          |
|*  3 |   TABLE ACCESS FULL| TEST_SALES    |      1 |   4000 |   187K|     5   (0)| 00:00:01 |   1341 |00:00:00.01 |       8 |       |       |          |
---------------------------------------------------------------------------------------------------------------------------------------------------------

I thought, this could be fixed by using extended statistics with column groups in both tables that include the article-id-columns, which are linked via the foreign key (I forced the creation of histograms for both created column groups, because they were not calculated automatically, when gathering statistics, after the execution of create_extended_stats):

select dbms_stats.create_extended_stats(ownname=>user, tabname=>'TEST_ARTICLES', extension=>'(ID,CAT)') from dual;
select dbms_stats.create_extended_stats(ownname=>user, tabname=>'TEST_SALES', extension=>'(ARTICLE_ID,COUNTRY)') from dual;
exec  DBMS_STATS.GATHER_TABLE_STATS (user,'TEST_ARTICLES', method_opt=>'FOR ALL COLUMNS SIZE AUTO FOR COLUMNS SIZE 254 SYS_STUVDR2B43_Y$B$A85Y33E9BGN');
exec  DBMS_STATS.GATHER_TABLE_STATS (user,'TEST_SALES', method_opt=>'FOR ALL COLUMNS SIZE AUTO FOR COLUMNS SIZE 254 SYS_STU5TK8KZP1UO386PUVC2TYHT$');

But when I rerun the query now

select /*+ gather_plan_statistics */ * from TEST_SALES s join TEST_ARTICLES a on s.ARTICLE_ID = a.ID
where a.cat = 'Toys' and s.country = 'UK';

select *
from table(dbms_xplan.display_cursor(format => 'allstats last advanced +adaptive'));

the expected rows have only changed slightly and are still way off:

---------------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation          | Name          | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time   | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |               |      1 |        |       |     8 (100)|          |     50 |00:00:00.01 |      18 |       |       |          |
|*  1 |  HASH JOIN         |               |      1 |   3993 | 83853 |     8   (0)| 00:00:01 |     50 |00:00:00.01 |      18 |  1922K|  1922K| 1537K (0)|
|*  2 |   TABLE ACCESS FULL| TEST_ARTICLES |      1 |    300 |  3000 |     3   (0)| 00:00:01 |    300 |00:00:00.01 |       7 |       |       |          |
|*  3 |   TABLE ACCESS FULL| TEST_SALES    |      1 |   4000 | 44000 |     5   (0)| 00:00:01 |   1341 |00:00:00.01 |       8 |       |       |          |
---------------------------------------------------------------------------------------------------------------------------------------------------------

Am I doing something wrong? Is the optimizer not able to resolve correlation between multiple tables or is there a way to fix this? I would be thankful, if someone knows how to get the cardinalities right here.

3
  • Why do you have A-Rows=1341 for TEST_SALES? Should be 4000 (and s.country = 'UK'). Can't see any better solution here than using hint OPT_ESTIMATE(JOIN (s a) ROWS=50) and I'm not sure if this will solve the problem. Commented Jun 18 at 12:58
  • I was wondering about the 1341 rows as well, but did not bother too much, as this is not my real case but an example to illustrate the problem. If this helps understanding the problem, maybe someone can explain. How can I adjust this hint to a real database where the number of rows changes regularly and increases over time?
    – egen
    Commented Jun 19 at 6:55
  • Try guessing cardinality better than optimizer, if it's possible in your environment. You can use opt_estimate(join(a b) SCALE_ROWS=0.01) if cardinality shoud be 0.01 of optimizer estimation (in your case 4000*0.01=40 rows) or opt_estimate(join(a b) max=100) for estimation to not exceed 100 rows. Commented Jun 19 at 11:09

1 Answer 1

0

Oracle, and most other DBMS I know of, will only gather statistics on the content of single tables. It can collect statistics at the table level (number of rows) and at the column level (number of different values and statistics on those values). Extended statistics will gather statistics on the correlation of multiple column in the same table, but it cannot correlate columns from different tables.

Everything is described here: https://www.oracle.com/technetwork/database/bi-datawarehousing/twp-statistics-concepts-12c-1963871.pdf

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.