row(s) 1 - 10 of more than 10011 - 20 of 10021 - 30 of 10031 - 40 of 10041 - 50 of 10051 - 60 of 10061 - 70 of 10071 - 80 of 10081 - 90 of 10091 - 100 of 100 Next
Join Cardinality – 2 - 05-Oct-2018 10:37 - Jonathan Lewis

In the previous note I posted about Join Cardinality I described a method for calculating the figure that the optimizer would give for the special case where you had a query that:

• joined two tables
• used a single-column to join on equality
• had no nulls in the join columns
• had a perfect frequency histogram on the columns at the two ends of the join
• had no filter predicates associated with either table

The method simply said: “Match up rows from the two frequency histograms, multiply the corresponding frequencies” and I supplied a simple SQL statement that would read and report the two sets of histogram data, doing the arithmetic and reporting the final cardinality for you. In an update I also added an adjustment needed in 11g (or, you might say, removed in 12c) where gaps in the histograms were replaced by “ghost rows” with a frequency that was half the lowest frequency in the histogram.

This is a nice place to start as the idea is very simple, and it’s likely that extensions of the basic idea will be used in all the other cases we have to consider. There are 25 possibilities that could need separate testing – though only 16 of them ought to be relevant from 12c onwards. Oracle allows for four kinds of histograms – in order of how precisely they describe the data they are:

• Frequency – with a perfect description of the data
• Top-N (a.k.a. Top-Frequency) – which describes all but a tiny fraction (ca. one bucket’s worth) of data perfectly
• Hybrid – which can (but doesn’t usually, by default) describe up to 2,048 popular values perfectly and gives an approximate distribution for the rest
• Height-balanced – which can (but doesn’t usually, by default) describe at most 1,024 popular values with some scope for misinformation.

Finally, of course, we have the general case of no histogram, using only 4 numbers (low value, high value, number of rows, number of distinct values) to give a rough picture of the data – and the need for histograms appears, of course, when the data doesn’t look anything like an even distribution of values between the low and high with close to “number of rows”/”number of distinct values” for each value.

So there are 5 possible statistical descriptions for the data in a column – which means there are 5 * 5 = 25 possible options to consider when we join two columns, or 4 * 4 = 16 if we label height-balanced histograms as obsolete and ignore them (which would be a pity because Chinar has done some very nice work explaining them).

Of course, once we’ve worked out a single-column equijoin between two tables there are plenty more options to consider:  multi-column joins, joins involving range-based predicates, joins involving more than 2 tables, and queries which (as so often happens) have predicates which aren’t involved in the joins.

For the moment I’m going to stick to the simplest case – two tables, one column, equality – and comment on the effects of filter predicates. It seems to be very straightforward as I’ll demonstrate with a new model

```rem
rem     Script:         freq_hist_join_03.sql
rem     Author:         Jonathan Lewis
rem     Dated:          Oct 2018
rem

execute dbms_random.seed(0)

create table t1(
id      number(8,0),
n0040   number(4,0),
n0090   number(4,0),
n0190   number(4,0),
n0990   number(4,0),
n1      number(4,0)
)
;

create table t2(
id      number(8,0),
n0050   number(4,0),
n0110   number(4,0),
n0230   number(4,0),
n1150   number(4,0),
n1      number(4,0)
)
;

insert into t1
with generator as (
select
rownum id
from dual
connect by
level <= 1e4 -- > comment to avoid WordPress format issue
)
select
rownum                                  id,
mod(rownum,   40) + 1                   n0040,
mod(rownum,   90) + 1                   n0090,
mod(rownum,  190) + 1                   n0190,
mod(rownum,  990) + 1                   n0990,
trunc(30 * abs(dbms_random.normal))     n1
from
generator       v1,
generator       v2
where
rownum <= 1e5 -- > comment to avoid WordPress format issue
;

insert into t2
with generator as (
select
rownum id
from dual
connect by
level <= 1e4 -- > comment to avoid WordPress format issue
)
select
rownum                                  id,
mod(rownum,   50) + 1                   n0050,
mod(rownum,  110) + 1                   n0110,
mod(rownum,  230) + 1                   n0230,
mod(rownum, 1150) + 1                   n1150,
trunc(30 * abs(dbms_random.normal))     n1
from
generator       v1,
generator       v2
where
rownum <= 1e5 -- > comment to avoid WordPress format issue
;

begin
dbms_stats.gather_table_stats(
ownname => null,
tabname     => 'T1',
method_opt  => 'for all columns size 1 for columns n1 size 254'
);
dbms_stats.gather_table_stats(
ownname     => null,
tabname     => 'T2',
method_opt  => 'for all columns size 1 for columns n1 size 254'
);
end;
/

```

You’ll notice that in this script I’ve created empty tables and then populated them. This is because of an anomaly that appeared in 18.3 when I used “create as select”, and should allow the results from 18.3 be an exact match for 12c. You don’t need to pay much attention to the Nxxx columns, they were there so I could experiment with a few variations in the selectivity of filter predicates.

Given the purpose of the demonstration I’ve gathered histograms on the column I’m going to use to join the tables (called n1 in this case), and here are the summary results:

```
TABLE_NAME           COLUMN_NAME          HISTOGRAM       NUM_DISTINCT NUM_BUCKETS
-------------------- -------------------- --------------- ------------ -----------
T1                   N1                   FREQUENCY                119         119
T2                   N1                   FREQUENCY                124         124

VALUE  FREQUENCY  FREQUENCY      PRODUCT
---------- ---------- ---------- ------------
0       2488       2619    6,516,072
1       2693       2599    6,999,107
2       2635       2685    7,074,975
3       2636       2654    6,995,944
...
113          1          3            3
115          1          2            2
116          4          3           12
117          1          1            1
120          1          2            2
------------
sum                               188,114,543

```

We’ve got frequencyy histograms, and we can see that they don’t have a perfect overlap. I haven’t printed every single line from the cardinality query, just enough to show you the extreme skew, a few gaps, and the total. So here are three queries with execution plans:

```
set serveroutput off

alter session set statistics_level = all;
alter session set events '10053 trace name context forever';

select
count(*)
from
t1, t2
where
t1.n1 = t2.n1
;

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

select
count(*)
from
t1, t2
where
t1.n1 = t2.n1
and     t1.n0990 = 20
;

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

select
count(*)
from
t1, t2
where
t1.n1 = t2.n1
and     t1.n0990 = 20
and     t2.n1150 = 25
;

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

```

I’ve queried the pure join – the count was exactly the 188,114,543 predicted by the cardinality query, of course – then I’ve applied a filter to one table, then to both tables. The first filter n0990 = 20 will (given the mod(,990)) definition identify one row in 990 from the original 100,000 in t1; the second filter n1150 = 25 will identify one row in 1150 from t2. That’s filtering down to 101 rows and 87 rows respectively from the two tables. So what do we see in the plans:

```
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |      1 |        |      1 |00:00:23.47 |     748 |       |       |          |
|   1 |  SORT AGGREGATE     |      |      1 |      1 |      1 |00:00:23.47 |     748 |       |       |          |
|*  2 |   HASH JOIN         |      |      1 |    188M|    188M|00:00:23.36 |     748 |  6556K|  3619K| 8839K (0)|
|   3 |    TABLE ACCESS FULL| T1   |      1 |    100K|    100K|00:00:00.01 |     374 |       |       |          |
|   4 |    TABLE ACCESS FULL| T2   |      1 |    100K|    100K|00:00:00.01 |     374 |       |       |          |
-----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."N1"="T2"."N1")

-----------------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |      1 |        |      1 |00:00:00.02 |     748 |       |       |          |
|   1 |  SORT AGGREGATE     |      |      1 |      1 |      1 |00:00:00.02 |     748 |       |       |          |
|*  2 |   HASH JOIN         |      |      1 |    190K|    200K|00:00:00.02 |     748 |  2715K|  2715K| 1647K (0)|
|*  3 |    TABLE ACCESS FULL| T1   |      1 |    101 |    101 |00:00:00.01 |     374 |       |       |          |
|   4 |    TABLE ACCESS FULL| T2   |      1 |    100K|    100K|00:00:00.01 |     374 |       |       |          |
-----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."N1"="T2"."N1")
3 - filter("T1"."N0990"=20)

-----------------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |      1 |        |      1 |00:00:00.01 |     748 |       |       |          |
|   1 |  SORT AGGREGATE     |      |      1 |      1 |      1 |00:00:00.01 |     748 |       |       |          |
|*  2 |   HASH JOIN         |      |      1 |    165 |    165 |00:00:00.01 |     748 |  2715K|  2715K| 1678K (0)|
|*  3 |    TABLE ACCESS FULL| T2   |      1 |     87 |     87 |00:00:00.01 |     374 |       |       |          |
|*  4 |    TABLE ACCESS FULL| T1   |      1 |    101 |    101 |00:00:00.01 |     374 |       |       |          |
-----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."N1"="T2"."N1")
3 - filter("T2"."N1150"=25)
4 - filter("T1"."N0990"=20)

```

The first execution plan shows an estimate of 188M rows – but we’ll have to check the trace file to confirm whether that’s only an approximate match to our calculation, or whether it’s an exact match. So here’s the relevant pair of lines:

```
Join Card:  188114543.000000 = outer (100000.000000) * inner (100000.000000) * sel (0.018811)
Join Card - Rounded: 188114543 Computed: 188114543.000000

```

Yes, the cardinality calculation and the execution plan estimates match perfectly. But there are a couple of interesting things to note. First, Oracle seems to be deriving the cardinality by multiplying the individual cardinalities of the two tables with a figure it calls “sel” – the thing that Chinar Aliyev has labelled Jsel the “Join Selectivity”. Secondly, Oracle can’t do arithmetic (or, removing tongue from cheek) the value it’s reported for the join selectivity is reported at only 6 decimal places, but stored to far more. What is the Join Selectivity, though ? It’s the figure we derive from the cardinality SQL divided by the cardinality of the cartesian join of the two tables – i.e. 188,114,543 / (100,000 * 100,000).

With the clue from the first trace file, can we work out why the second and third plans show 190K and 165 rows respectively. How about this – multiply the filtered cardinalities of the two separate tables, then multiply the result by the join selectivity:

• 1a)   n0990 = 20: gives us 1 row in every 990.    100,000 / 990 = 101.010101…    (echoing the rounded execution plan estimate).
• 1b)   100,000 * (100,000/990) * 0.0188114543 = 190,014.69898989…    (which is in the ballpark of the plan and needs confirmation from the trace file).

• 2a)   n1150 = 25: gives us 1 row in every 1,150.    100,000 / 1,150 = 86.9565217…    (echoing the rounded execution plan estimate)
• 2b)   (100,000/990) * (100,000/1,150) * 0.0188114543 = 165.2301651..    (echoing the rounded execution plan estimate).

Cross-checking against extracts from the 10053 trace files:

```
Join Card:  190014.689899 = outer (101.010101) * inner (100000.000000) * sel (0.018811)
Join Card - Rounded: 190015 Computed: 190014.689899

Join Card:  165.230165 = outer (86.956522) * inner (101.010101) * sel (0.018811)
Join Card - Rounded: 165 Computed: 165.230165

```

### Conclusion.

Remembering that we’re still looking at very simple examples with perfect frequency histograms: it looks as if we can work out a “Join Selectivity” (Jsel) – the selectivity of a “pure” unfiltered join of the two tables – by querying the histogram data then use the resulting value to calculate cardinalities for simple two-table equi-joins by multiplying together the individual (filtered) table cardinality estimates and scaling by the Join Selectivity.

### Acknowledgements

Most of this work is based on a document written by Chinar Aliyev in 2016 and presented at the Hotsos Symposium the same year. I am most grateful to him for responding to a recent post of mine and getting me interested in spending some time to get re-acquainted with the topic. His original document is a 35 page pdf file, so there’s plenty more material to work through, experiment with, and write about.

It’s easier to run things unsecured.

It’s unconscionable to run things unsecured.

DON’T BE THIS GUY.

### So Let’s Secure My Database and my Web Services

So I have a collection of REST enabled objects (tables, view, and procedures) and a SLEW of RESTful Service modules. I want ALL of them locked down.

So I need to create a Role and Privilege. And then I need to assign that privilege to what I want protected. Without said privilege, my user doesn’t get to use it (Authorized) even if I know who they are (Authenticated.)

I can do this GUI for PL/SQL, I’m going to show both.

The code:

```BEGIN
ORDS.CREATE_ROLE(p_role_name  => 'storyteller');
l_roles(1)   := 'SQL Developer';
l_roles(2)   := 'storyteller';
l_modules(1) := 'Banking';
l_modules(2) := 'abstract';
l_modules(3) := 'blind_body';
l_modules(4) := 'bool';
l_modules(5) := 'employee';
l_modules(6) := 'forums';
l_modules(7) := 'george';
l_modules(8) := 'norway';
l_modules(9) := 'otn';
l_modules(10) := 'package';
l_modules(11) := 'parameters';
l_modules(12) := 'performance';
l_modules(13) := 'plsql_table';
l_modules(14) := 'resources';
l_modules(15) := 'rpc';
l_modules(16) := 'sample_module';
l_modules(17) := 'simple';
l_modules(18) := 'test';
l_modules(19) := 'test2';
l_modules(20) := 'test3';
l_modules(21) := 'test4';
l_patterns(1):= '/*';
ORDS.DEFINE_PRIVILEGE(
p_privilege_name => 'oracle.jeff.demo',
p_roles          => l_roles,
p_patterns       => l_patterns,
p_modules        => l_modules,
p_label          => '',
p_description    => 'just something i made for giggles',
COMMIT;
END;
/```

This looks like this in the GUI:

I am explicitly listing the protected modules.

So I’ve EXPLICITLY said which modules need protecting – THIS IS THE HARD WAY.

What happens when I add a new module tomorrow, and I forget to update the priv?

Or what about all those REST enabled TABLEs and PL/SQL objects I’ve been working on?

Let’s protect the URI pattern.

NOTHING under HR goes through w/o the proper privs.

We are in the HR REST enabled Schema – so I can say starting from /hr/ (or whatever your schema alias is), protect these patterns that match. SO by putting /* – that ANTYHING after /ords/hr

### No Go

Let’s make the call sans credentials.

Boo, but good.

Now with the ‘keys.’

I could hit peeps/105, peeps/227, peeps/whatever and BE GOOD.

So let’s go back to our PL/SQL code bit for defining the privilege:

`l_patterns(1):= '/*';`

This protects EVERYTHING under the schema where the privilege is defined. It doesn’t hurt to ‘double protect’ the modules by explicitly adding them, but there’s no need either.

### The USER and the Role

This is a Jetty user, and I created it the normal way.

```c:\ORDS\18.2>java -jar ords.war user ords_dev "storyteller"
Enter a password for user ords_dev:
Oct 04, 2018 10:52:05 AM oracle.dbtools.standalone.ModifyUser execute
INFO: Created user: ords_dev in file: c:\ords\config\ords\credentials```

### Don’t Forget APEX!

As of version 18.1 and now improved even more in 18.2, you can do all of your RESTful Service development in APEX. Click the pretty picture to get Doug’s introduction to these new APEX REST development pages.

It was Doug actually who helped me get this going today – I kept trying to secure /hr/* vs /*. Thanks Doug!

### The REST Client I’m Using Today

A co-worker did a a demo playing with our new DB API (coming soon, stay tuned!), and they used this thing called Insomnia. I’ve been using it for two days, and so far, it’s great. They’re even on Twitter if you’re so obliged.

Query Builder: On Inline Views and ANSI Joins - 04-Oct-2018 13:17 - Jeff Smith

Stumbling around on StackOverflow this morning, I came across this person’s HOMEWORK question, and the answer chided them on not using ANSI style JOINs.

I’m not going to post the actual SQL here, but if you follow the previous link, it’s there for you to grab and have a go.

I hope they get an A+

Something I like to do is take REAL code off the innerwebs and use it to test our parser, formatter, and even the Query Builder.

So let’s look at how the Query Builder handles this query:

And YES, I formatted it first. But anyways…it gives us this:

This is pretty boring, it’s just the SELECT * FROM the inline view bit.

The fun or main part of the query can be found here:

Cool.

And now – about the ANSI/Oracle Style JOINs bit…let’s switch that from the SO answer with ANSI joins over to a traditional Oracle-looking query. So right-click:

And now toggle back to the Worksheet:

Cool!

### A Few Parting Notes

If you were building this query from scratch, when you go to build the subquery, right-click in the diagram space:

Then toggle over to ‘Query1’ and ‘draw’ your inline view there

• I’m not debating ANSI or Oracle JOINs here. Or Anywhere for that matter.
• You can read about inline views in general here (Oracle Docs)
• Make sure you’re on version 18.2 or higher of SQLDev

We fixed a HUGE performance issue with the Query Builder in the last drop of SQL Developer. IF you’re running an old copy and you want to give the query builder a play, upgrade FIRST.

A few days ago Joel Kallman announced the release of Oracle Application Express (APEX) 18.2.

In a previous post I mentioned the updates to my Vagrant builds to include this version, as well as updates of Tomcat and Java. I’ve subsequently done the updates for APEX 18.2 on Docker too. If you are interested you can see them here.

In addition to this we’ve rolled APEX 18.2 out at work. We already had some installations of APEX 18.1, but many were stuck on version 5.1.4 because of time constraints. Now everything is up to APEX 18.2. We still have a range of database versions (11.2, 12.1, 12.2 and soon to be 18c) at work, and it’s worked fine on all of them.

I spied a couple of people asking about the upgrade process. There’s no difference to previous versions. In the past, if one of the first two numbers change you do a regular install. If it’s not one of those major version changes you download the patch from MOS and apply it. Since this is a major version number change, I installed it in the normal way and everything was fine. I’m not sure how this will work going forward, as I suspect all releases will start to use the new version format, so does that mean every release from now on will be an “install”, not a “patch”? Someone has probably discussed this already and I missed it.

I only have one little gripe about the upgrades, which is I have to run an ORDS Validate once it’s complete to make sure ORDS is working fine. It would be really nice if APEX could fix whatever gets broken in ORDS, so I don’t have to do it. It’s just one less step to do…

Cheers

Tim…

Oracle Application Express (APEX) 18.2 : Upgrades Complete was first posted on October 3, 2018 at 3:05 pm.
Join Cardinality - 03-Oct-2018 07:01 - Jonathan Lewis

Following up my “Hacking for Skew” article from a couple of days ago, Chinar Aliyev has written an article about a method for persuading the optimizer to calculate the correct cardinality estimate without using any undocumented, or otherwise dubious, mechanisms. His method essentially relies on the optimizer’s mechanism for estimating join cardinality when there are histograms at both ends of the join, so I thought I’d write a short note describing the simplest possible example of the calculation – an example where the query is a single column equi-join with no nulls in either column and a perfect frequency histograms at both ends of the join.  (For a detailed description of more general cases I always refer to the work done by Alberto Dell’Era a few years ago). We start with two data sets that exhibit a strong skew in their data distributions:

```
execute dbms_random.seed(0)

create table t1
nologging
as
with generator as (
select
rownum id
from dual
connect by
level <= 1e4 -- > comment to avoid WordPress format issue
)
select
rownum                                  id,
trunc(3 * abs(dbms_random.normal))      n1
from
generator       v1
;

create table t2
nologging
as
with generator as (
select
rownum id
from dual
connect by
level <= 1e4 -- > comment to avoid WordPress format issue
)
select
rownum                                  id,
trunc(3 * abs(dbms_random.normal))      n1
from
generator       v1
;

begin
dbms_stats.gather_table_stats(
ownname     => null,
tabname     => 'T1',
method_opt  => 'for all columns size 254'
);
dbms_stats.gather_table_stats(
ownname     => null,
tabname     => 'T2',
method_opt  => 'for all columns size 254'
);
end;
/

```

I’ve generated two tables of 10,000 randomly generated values using the dbms_random.normal() function, but I’ve scaled the value up by a factor of three and taken the absolute value – which has given me a range of 12 distinct integer values with a nicely skewed distribution. Then I’ve gathered stats requesting histograms of up to 254 buckets. Since I’ve tested this only on versions from 11.2.0.4 onwards this means I’ll get a perfect histogram on the n1 columns on both tables.

Now I’m going run a query that reports the values and frequencies from the two tables by querying user_tab_histograms using a variant of an analytic query I published a long time ago to convert the cumulative frequencies recorded as the endpoint values into simple frequencies. If, for some reason, this query doesn’t run very efficiently in your tests you could always /*+ materialize */ the two factored subqueries (CTEs – common table expressions):

```
prompt  =======================================================================
prompt  Multiply and sum matching frequencies. An outer join is NOT needed
prompt  because rows that don't match won't contributed to the join cardinality
prompt  =======================================================================

break on report skip 1
compute sum of product on report
column product format 999,999,999

with f1 as (
select
endpoint_value                                                            value,
endpoint_number - lag(endpoint_number,1,0) over(order by endpoint_number) frequency
from
user_tab_histograms
where
table_name  = 'T1'
and     column_name = 'N1'
order by
endpoint_value
),
f2 as (
select
endpoint_value                                                            value,
endpoint_number - lag(endpoint_number,1,0) over(order by endpoint_number) frequency
from
user_tab_histograms
where
table_name  = 'T2'
and     column_name = 'N1'
order by
endpoint_value
)
select
f1.value, f1.frequency, f2.frequency, f1.frequency * f2.frequency product
from
f1, f2
where
f2.value = f1.value
;

VALUE  FREQUENCY  FREQUENCY      PRODUCT
---------- ---------- ---------- ------------
0       2658       2532    6,730,056
1       2341       2428    5,683,948
2       1828       1968    3,597,504
3       1305       1270    1,657,350
4        856        845      723,320
5        513        513      263,169
6        294        249       73,206
7        133        117       15,561
8         40         54        2,160
9         23         17          391
10          5          5           25
11          4          2            8
------------
sum                                18,746,698

```

As you can see, the two columns do have a highly skewed data distribution. The pattern of the two data sets is similar though the frequencies aren’t identical, of course. The total I get from this calculation is (I claim) the cardinality (rows) estimate that the optimizer will produce for doing an equi-join on these two tables – so let’s see the test:

```
set serveroutput off
alter session set statistics_level = all;
alter session set events '10053 trace name context forever';

select
count(*)
from
t1, t2
where
t1.n1 = t2.n1
;

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
alter session set statistics_level = typical;
alter session set events '10053 trace name context off';

```

And the resulting output:

```Session altered.
Session altered.

COUNT(*)
----------
18746698

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  0wxytnyqs4b5j, child number 0
-------------------------------------
select  count(*) from  t1, t2 where  t1.n1 = t2.n1

Plan hash value: 906334482
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |      1 |        |      1 |00:00:03.23 |      40 |       |       |          |
|   1 |  SORT AGGREGATE     |      |      1 |      1 |      1 |00:00:03.23 |      40 |       |       |          |
|*  2 |   HASH JOIN         |      |      1 |     18M|     18M|00:00:02.96 |      40 |  2616K|  2616K| 2098K (0)|
|   3 |    TABLE ACCESS FULL| T1   |      1 |  10000 |  10000 |00:00:00.01 |      20 |       |       |          |
|   4 |    TABLE ACCESS FULL| T2   |      1 |  10000 |  10000 |00:00:00.01 |      20 |       |       |          |
-----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."N1"="T2"."N1")

```

As we can see, the estimate for the hash join is “18M” which is in the right ballpark but, in its current format, isn’t entirely helpful which is why I’ve enabled the 10053 trace to get an exact figure from the trace file, and this is what we see:

```
***********************
Best so far:  Table#: 0  cost: 4.352468  card: 9487.000000  bytes: 28461.000000
Table#: 1  cost: 378.482370  card: 18467968.000000  bytes: 110807808.000000
***********************

```

The optimizer’s estimate is exactly the sum of the products of the frequencies of matching values from the (frequency) histogram data. There is a simple rationale for this – it gets the right answer. For each row in t1 with value ‘X’ the (frequency) histogram on t2 tells Oracle how many rows will appear in the join, so multiplying the frequency of ‘X’ in t1 by the frequency of ‘X’ in t2 tells Oracle how many rows the ‘X’s will contribute to the join. Repeat for every distinct value that appears in both (frequency) histograms and sum the results.

As a refinement on this (very simple) example, let’s delete data from the two tables so that we have rows in t1 that won’t join to anything in t2, and vice versa – then re-gather stats, query the histograms, and check the new prediction. We want to check whether a value that appears in the t1 histogram contributes to the join cardinality estimate even if there are no matching values in the t2 histogram (and vice versa):

```
delete from t1 where n1 = 4;
delete from t2 where n1 = 6;

execute dbms_stats.gather_table_stats(user,'t1',method_opt=>'for all columns size 254', no_invalidate=>false)
execute dbms_stats.gather_table_stats(user,'t2',method_opt=>'for all columns size 254', no_invalidate=>false)

with f1 as (
select
endpoint_value                                                            value,
endpoint_number - lag(endpoint_number,1,0) over(order by endpoint_number) frequency
from
user_tab_histograms
where
table_name  = 'T1'
and     column_name = 'N1'
order by
endpoint_value
),
f2 as (
select
endpoint_value                                                            value,
endpoint_number - lag(endpoint_number,1,0) over(order by endpoint_number) frequency
from
user_tab_histograms
where
table_name  = 'T2'
and     column_name = 'N1'
order by
endpoint_value
)
select
f1.value, f1.frequency, f2.frequency, f1.frequency * f2.frequency product
from
f1, f2
where
f2.value = f1.value
;

set serveroutput off
alter session set statistics_level = all;
alter session set events '10053 trace name context forever';

select
count(*)
from
t1, t2
where
t1.n1 = t2.n1
;

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));
alter session set statistics_level = typical;
alter session set events '10053 trace name context off';

```

And the output – with a little cosmetic tidying:

```
856 rows deleted.
249 rows deleted.

PL/SQL procedure successfully completed.
PL/SQL procedure successfully completed.

VALUE  FREQUENCY  FREQUENCY      PRODUCT
---------- ---------- ---------- ------------
0       2658       2532    6,730,056
1       2341       2428    5,683,948
2       1828       1968    3,597,504
3       1305       1270    1,657,350
5        513        513      263,169
7        133        117       15,561
8         40         54        2,160
9         23         17          391
10          5          5           25
11          4          2            8
------------
sum                                17,950,172

Session altered.
Session altered.

COUNT(*)
----------
17950172

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------------------------------------------
SQL_ID  0wxytnyqs4b5j, child number 0
-------------------------------------
select  count(*) from  t1, t2 where  t1.n1 = t2.n1

Plan hash value: 906334482
-----------------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name | Starts | E-Rows | A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |      1 |        |      1 |00:00:02.89 |      40 |       |       |          |
|   1 |  SORT AGGREGATE     |      |      1 |      1 |      1 |00:00:02.89 |      40 |       |       |          |
|*  2 |   HASH JOIN         |      |      1 |     17M|     17M|00:00:02.61 |      40 |  2616K|  2616K| 2134K (0)|
|   3 |    TABLE ACCESS FULL| T1   |      1 |   9144 |   9144 |00:00:00.01 |      20 |       |       |          |
|   4 |    TABLE ACCESS FULL| T2   |      1 |   9751 |   9751 |00:00:00.01 |      20 |       |       |          |
-----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."N1"="T2"."N1")

From the 10053 trace file:
***********************
Best so far:  Table#: 0  cost: 4.340806  card: 9144.000000  bytes: 27432.000000
Table#: 1  cost: 368.100010  card: 17950172.000000  bytes: 107701032.000000
***********************

```

You can see from the frequency histogram report that we “lost” values 4 and 6 from the report; then the total from the report matches the actual number of rows returned by the query, and the cardinality estimate in the plan is again in the right ballpark – with the trace file showing an exact match.

I’ve run this test on 11.2.0.4,  12.1.0.2,  12.2.0.1 and  18.3.0.0 (which generated a different set of random values) – and there’s an anomaly that appears in 11.2.0.4 (though maybe that should be “disappeared from”): the optimizer’s estimate for the cardinality was a little larger than the value generated in the query against user_tab_histograms. [Now explained (probably)]

### Conclusion:

For an incredibly simple class of queries with perfect frequency histograms there’s a very simple way to calculate the cardinality estimate that the optimizer will predict. Match up rows from the two frequency histograms, multiply the corresponding frequencies (making sure you don’t multiply the cumulative frequencies), and sum.

This is, of course, only a tiny step in the direction of seeing how Oracle uses histograms and covers only a type of query that is probably too simple to appear in a production system, but it’s a basis on which I may build in future notes over the next few weeks.

### Update (5th Oct)

The “error” in the 11g calculation irritated me a little, and I woke up this morning with an idea about the solution. In 10.2.0.4 Oracle changed the way the optimizer calculated for a predicate that used a value that did not appear in the frequency histogram: it did the arithmetic for  “half the least frequently occurring value”. So I thought I’d run up a test where for my “sum of products” query I emulated this model. I had to change my query to an “ANSI”-style full outer join, and here it is:

```with f1 as (
select
endpoint_value                                                            value,
endpoint_number - lag(endpoint_number,1,0) over(order by endpoint_number) frequency
from
user_tab_histograms
where
table_name  = 'T1'
and     column_name = 'N1'
),
f2 as (
select
endpoint_value                                                            value,
endpoint_number - lag(endpoint_number,1,0) over(order by endpoint_number) frequency
from
user_tab_histograms
where
table_name  = 'T2'
and     column_name = 'N1'
)
select
f1.value, f2.value,
nvl(f1.frequency, 0)                t1_frequency,
nvl(f2.frequency, 0)                t2_frequency,
nvl(f1.frequency, &t1_least / 2) *
nvl(f2.frequency, &t2_least / 2)    product
from
f1
full outer join
f2
on
f2.value = f1.value
order by
coalesce(f1.value, f2.value)
;

```

Running this code, and noting that the least frequent value in t1 was 4, while the least frequence in t2 was 2, I got the following results (with the 10053 trace file summary following the output)

```
VALUE      VALUE T1_FREQUENCY T2_FREQUENCY      PRODUCT
---------- ---------- ------------ ------------ ------------
0          0         2658         2532    6,730,056
1          1         2341         2428    5,683,948
2          2         1828         1968    3,597,504
3          3         1305         1270    1,657,350
4            0          845        1,690
5          5          513          513      263,169
6                     294            0          294
7          7          133          117       15,561
8          8           40           54        2,160
9          9           23           17          391
10         10            5            5           25
11         11            4            2            8
------------ ------------ ------------
sum                           9144         9751   17,952,156

Join Card:  17952157.000000 = outer (9751.000000) * inner (9144.000000) * sel (0.201341)
Join Card - Rounded: 17952157 Computed: 17952157.00

```

That’s a pretty good match to the trace file result – and the difference of 1 may simply be a rounding error, my calculation for the join selectivity would have been 0.2013405449 but Oracle has rounded (up) to six decimal places.

The question:

We have big database with a lot of stuff and I want to use version control (Git) to manage changes. There are a lot of articles how to do it step by step but one piece is missing for me. Is there standard or recommended way for file structure of whole database (data excluded) and how it can be obtained from existing database?

So I’m not sure about standard or recommended, but we do have a few tools for you to take advantage of.

I realized I had ONLY talked about the Cart previously – which is good for taking a subset or handful of objects, and loading to scripts. But this person wants the entire schema.

So let’s look at the Database Export Wizard, under the Tools menu.

The easiest way to walk this is to login AS the schema you want to unloadI’m going with HR.

Then proceed to the tools menu.

That’s what we’re looking for.

### Step 2: The Most Important Part, the Options

Two things to consider here – what types of objects do you want, and how do you want the output generated.

I’ve highlighted 3 critical areas.

1. Show Schema – do you want to see CREATE TABLE HR.TABLE1 or do you want to see CREATE TABLE TABLE1?
2. Export Data – you probably do NOT want the data included as INSERT statements – unless these are lookup tables
3. Save As – I have mine set to SEPARATE DIRECTORIES, so I will get a directory for each schema object type

I could click ‘Finish’ here – as I am EXPORTING EVERYTHING in my schema. But if you don’t want everything, you could go to the next screen to use the Object Selector to say exactly what you want to export.

And GO!

### The Results

Ta-da!

Let’s look at two files.

First a sample, generated file.

If you don’t like how the DDL is shaped, mind your options when running the wizard.

And there’s also a ‘master’ file in the root output directory.

You could run this script to kick off all of the generated files from our Wizard session.

### Don’t Like GUIs?

You could do this in SQLcl with some JavaScript, courtesy @krisrice.

Have a go with this.

There were a lot of changes in my Vagrant repository on GitHub last week and over the weekend.

First, I got asked a question about 12.2 RAC and I couldn’t be bothered to run through a manual build, so I took my 18c RAC hands-off build and amended it to create a 12.2 RAC hands-off build. Along the way I noticed a couple of hard-coded bits in the 18c build I hadn’t noticed previously, which I altered of course. I also had to move the 18c build to a version-specific sub-directory. I think I’ve altered all references to the location.

I went through some of my individual server builds and updated them to use the latest versions of Tomcat 9, Java 11 and APEX 18.2. All that was pretty straight forward.

On Sunday I was running some tests of the builds on my laptop while I was at my brother’s house, and I noticed I was not pulling packages from the yum repositories properly. I ended up adding “nameserver 8.8.8.8” to pretty much all the “/etc/resolv.conf” files inside the VMs. I’m not sure what has changed as that hasn’t happened before, so I’m not sure if it’s something to do with the networking… Anyway, it fixed everything, so happy days.

While I was doing these builds I learned something new. I forgot to amend the path to my ASM disks from a UNIX style path “/u05/VirtualBox/shared/ol7_183_rac/…” to a Windows style path. Vagrant didn’t care and just created the location under the C drive as “C:\u05\VirtualBox\shared\ol7_183_rac\”. I’ll have to add a note about that to my “README.md” files about that.

I’ve still got to update some Docker builds with the latest software. I’ll probably do that over this week…

Cheers

Tim…

VirtualBox and Vagrant : New RAC Stuff and Changes was first posted on October 1, 2018 at 1:41 pm.
Case Study - 30-Sep-2018 14:59 - Jonathan Lewis

A question about reading execution plans and optimising queries arrived on the ODC database forum a little while ago; the owner says the following statement is taking 14 minutes to return 30,000 rows and wants some help understanding why.

If you look at the original posting you’ll see that we’ve been given the text of the query and the execution plan including rowsource execution stats. There’s an inconsistency between the supplied information and the question asked, and I’ll get back to that shortly, but to keep this note fairly short I’ve excluded the 2nd half of the query (which is a UNION ALL) because the plan says the first part of the query took 13 minutes and 20 second and the user is worried about a total of 14 minutes.

```SELECT /*+ gather_plan_statistics*/ DISTINCT
rct.org_id,
hzp.party_name,
hca.account_number,
rct.customer_trx_id,
rct.trx_number,
rct.trx_date,
rctd.gl_date,
rct.creation_date,
rctl.line_number,
rct.invoice_currency_code inv_currency,
(
SELECT SUM (rct_1.extended_amount)
FROM   apps.ra_customer_trx_lines_all rct_1
WHERE  rct_1.customer_trx_id = rct.customer_trx_id
AND    rct_1.line_type = 'LINE') inv_net_amount,
(
SELECT SUM (rct_2.extended_amount)
FROM   apps.ra_customer_trx_lines_all rct_2
WHERE  rct_2.customer_trx_id = rct.customer_trx_id
AND    rct_2.line_type = 'TAX') inv_tax_amount,
(
SELECT SUM (rct_3.extended_amount)
FROM   apps.ra_customer_trx_lines_all rct_3
WHERE  rct_3.customer_trx_id = rct.customer_trx_id) inv_gross_amount,
gll.currency_code                                    func_currency,
Round((
(
SELECT SUM (rct_4.extended_amount)
FROM   apps.ra_customer_trx_lines_all rct_4
WHERE  rct_4.customer_trx_id = rct.customer_trx_id
AND    rct_4.line_type = 'LINE')*gdr.conversion_rate),2) func_net_amount,
Round((
(
SELECT SUM (rct_5.extended_amount)
FROM   apps.ra_customer_trx_lines_all rct_5
WHERE  rct_5.customer_trx_id = rct.customer_trx_id
AND    rct_5.line_type = 'TAX')*gdr.conversion_rate),2) func_tax_amount,
Round((
(
SELECT SUM (rct_6.extended_amount)
FROM   apps.ra_customer_trx_lines_all rct_6
WHERE  rct_6.customer_trx_id = rct.customer_trx_id)*gdr.conversion_rate),2) func_gross_amount,
glcc.segment1                                                                 company,
glcc.segment2                                                                 account,
hg.geography_name                                                             billing_country,
gdr.conversion_rate
FROM            apps.hz_parties hzp,
apps.hz_cust_accounts hca,
apps.ra_customer_trx_all rct,
apps.ra_customer_trx_lines_all rctl,
apps.ra_cust_trx_line_gl_dist_all rctd,
apps.gl_code_combinations_kfv glcc,
apps.hz_cust_site_uses_all hcsua,
apps.hz_cust_acct_sites_all hcasa,
apps.hz_party_sites hps,
apps.hz_locations hl,
apps.hz_geographies hg,
apps.gl_ledgers gll,
apps.gl_daily_rates gdr
WHERE           hzp.party_id = hca.party_id
AND             hca.cust_account_id = rct.bill_to_customer_id
AND             hca.cust_account_id = hcasa.cust_account_id
AND             rct.customer_trx_id = rctl.customer_trx_id
AND             rctl.customer_trx_line_id = rctd.customer_trx_line_id
AND             glcc.code_combination_id = rctd.code_combination_id
AND             rct.bill_to_site_use_id = hcsua.site_use_id
AND             hcsua.cust_acct_site_id = hcasa.cust_acct_site_id
AND             hcasa.party_site_id = hps.party_site_id
AND             hps.location_id = hl.location_id
AND             hl.country = hg.country_code
AND             hg.geography_type = 'COUNTRY'
AND             rctl.line_type = 'TAX'
AND             gll.ledger_id = rct.set_of_books_id
AND             gdr.from_currency = rct.invoice_currency_code
AND             gdr.to_currency = gll.currency_code
AND             to_date(gdr.conversion_date) = to_date(rctd.gl_date)
AND             gdr.conversion_type = 'Corporate'
AND             rctd.gl_date BETWEEN To_date ('01-JAN-2018', 'DD-MON-YYYY') AND  To_date ('31-JAN-2018', 'DD-MON-YYYY')
AND             glcc.segment1 = '2600'
AND             glcc.segment2 = '206911'
GROUP BY        hzp.party_name,
hca.account_number,
rct.trx_number,
rct.trx_date,
rct.creation_date,
rctl.line_number,
rctl.unit_selling_price,
rct.org_id,
rctd.gl_date,
rct.customer_trx_id,
glcc.segment1,
glcc.segment2,
hg.geography_name,
rct.invoice_currency_code,
gll.currency_code,
gdr.conversion_rate
```

We note that there are six scalar subqueries in the text I’ve reported – and they form two groups of three, and the difference between the two groups is that one group is multiplied by a conversion rate while the other isn’t; moreover in each group the three subqueries are simply querying subsets of the same correlated data set. So it looks as if all 6 scalar subqueries could be eliminated and replaced by the inclusion of an aggregate view in the from clause and the projection of 6 columns from that view.

However, before pursuing that option, take a look at the plan with the rowsource execution stats – where is the time going ?

```
-----------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                                  | Name                         | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                           |                              |      1 |        |    501 |00:13:20.17 |    3579K|
|   1 |  UNION-ALL                                                 |                              |      1 |        |    501 |00:13:20.17 |    3579K|
|   2 |   HASH UNIQUE                                              |                              |      1 |      1 |    501 |00:13:20.17 |    3579K|
|   3 |    HASH GROUP BY                                           |                              |      1 |      1 |  19827 |00:13:20.15 |    3579K|
|   4 |     NESTED LOOPS                                           |                              |      1 |        |  21808 |00:13:10.26 |    3579K|
|   5 |      NESTED LOOPS                                          |                              |      1 |      1 |  21808 |00:13:10.11 |    3578K|
|   6 |       NESTED LOOPS OUTER                                   |                              |      1 |      1 |  21808 |00:13:09.90 |    3576K|
|   7 |        NESTED LOOPS OUTER                                  |                              |      1 |      1 |  21808 |00:13:09.25 |    3501K|
|   8 |         NESTED LOOPS OUTER                                 |                              |      1 |      1 |  21808 |00:13:08.48 |    3426K|
|   9 |          NESTED LOOPS OUTER                                |                              |      1 |      1 |  21808 |00:13:07.66 |    3333K|
|  10 |           NESTED LOOPS OUTER                               |                              |      1 |      1 |  21808 |00:13:06.92 |    3258K|
|  11 |            NESTED LOOPS OUTER                              |                              |      1 |      1 |  21808 |00:13:06.08 |    3183K|
|  12 |             NESTED LOOPS                                   |                              |      1 |      1 |  21808 |00:13:04.69 |    3090K|
|  13 |              NESTED LOOPS                                  |                              |      1 |      1 |  21808 |00:13:05.75 |    3026K|
|  14 |               NESTED LOOPS                                 |                              |      1 |      1 |  21808 |00:13:03.30 |    2961K|
|  15 |                NESTED LOOPS                                |                              |      1 |      1 |  33459 |00:00:04.33 |    1123K|
|  16 |                 NESTED LOOPS                               |                              |      1 |    351 |  33459 |00:00:03.67 |    1025K|
|  17 |                  NESTED LOOPS                              |                              |      1 |    351 |  33459 |00:00:03.06 |     926K|
|  18 |                   NESTED LOOPS                             |                              |      1 |    351 |  33459 |00:00:02.47 |     827K|
|* 19 |                    HASH JOIN                               |                              |      1 |    351 |  33459 |00:00:01.90 |     730K|
|  20 |                     TABLE ACCESS FULL                      | GL_LEDGERS                   |      1 |     38 |     39 |00:00:00.01 |      15 |
|  21 |                     NESTED LOOPS                           |                              |      1 |        |  33459 |00:00:01.75 |     730K|
|  22 |                      NESTED LOOPS                          |                              |      1 |    351 |  33459 |00:00:01.44 |     696K|
|  23 |                       NESTED LOOPS                         |                              |      1 |    351 |  33459 |00:00:01.11 |     646K|
|* 24 |                        HASH JOIN                           |                              |      1 |    385 |  33459 |00:00:00.40 |     526K|
|* 25 |                         TABLE ACCESS BY INDEX ROWID BATCHED| GL_CODE_COMBINATIONS         |      1 |     35 |      1 |00:00:00.01 |     108 |
|* 26 |                          INDEX RANGE SCAN                  | GL_CODE_COMBINATIONS_N2      |      1 |    499 |     77 |00:00:00.01 |       3 |
|* 27 |                         TABLE ACCESS BY INDEX ROWID BATCHED| RA_CUST_TRX_LINE_GL_DIST_ALL |      1 |    651K|   1458K|00:00:02.22 |     526K|
|* 28 |                          INDEX RANGE SCAN                  | RA_CUST_TRX_LINE_GL_DIST_N2  |      1 |    728K|   1820K|00:00:01.60 |   11147 |
|* 29 |                        TABLE ACCESS BY INDEX ROWID         | RA_CUSTOMER_TRX_LINES_ALL    |  33459 |      1 |  33459 |00:00:00.53 |     119K|
|* 30 |                         INDEX UNIQUE SCAN                  | RA_CUSTOMER_TRX_LINES_U1     |  33459 |      1 |  33459 |00:00:00.31 |   86364 |
|* 31 |                       INDEX UNIQUE SCAN                    | RA_CUSTOMER_TRX_U1           |  33459 |      1 |  33459 |00:00:00.21 |   49850 |
|  32 |                      TABLE ACCESS BY INDEX ROWID           | RA_CUSTOMER_TRX_ALL          |  33459 |      1 |  33459 |00:00:00.20 |   33459 |
|  33 |                    TABLE ACCESS BY INDEX ROWID             | HZ_CUST_ACCOUNTS             |  33459 |      1 |  33459 |00:00:00.42 |   97887 |
|* 34 |                     INDEX UNIQUE SCAN                      | HZ_CUST_ACCOUNTS_U1          |  33459 |      1 |  33459 |00:00:00.24 |   64428 |
|  35 |                   TABLE ACCESS BY INDEX ROWID              | HZ_PARTIES                   |  33459 |      1 |  33459 |00:00:00.44 |   98783 |
|* 36 |                    INDEX UNIQUE SCAN                       | HZ_PARTIES_U1                |  33459 |      1 |  33459 |00:00:00.26 |   65175 |
|  37 |                  TABLE ACCESS BY INDEX ROWID               | HZ_CUST_SITE_USES_ALL        |  33459 |      1 |  33459 |00:00:00.46 |   98374 |
|* 38 |                   INDEX UNIQUE SCAN                        | HZ_CUST_SITE_USES_U1         |  33459 |      1 |  33459 |00:00:00.28 |   64915 |
|* 39 |                 TABLE ACCESS BY INDEX ROWID                | HZ_CUST_ACCT_SITES_ALL       |  33459 |      1 |  33459 |00:00:00.45 |   98195 |
|* 40 |                  INDEX UNIQUE SCAN                         | HZ_CUST_ACCT_SITES_U1        |  33459 |      1 |  33459 |00:00:00.26 |   64736 |
|  41 |                TABLE ACCESS BY INDEX ROWID BATCHED         | GL_DAILY_RATES               |  33459 |      1 |  21808 |00:12:44.59 |    1838K|
|* 42 |                 INDEX RANGE SCAN                           | GL_DAILY_RATES_U1            |  33459 |      1 |  21808 |00:13:08.16 |    1837K|
|  43 |               TABLE ACCESS BY INDEX ROWID                  | HZ_PARTY_SITES               |  21808 |      1 |  21808 |00:00:00.35 |   64339 |
|* 44 |                INDEX UNIQUE SCAN                           | HZ_PARTY_SITES_U1            |  21808 |      1 |  21808 |00:00:00.23 |   42531 |
|  45 |              TABLE ACCESS BY INDEX ROWID                   | HZ_LOCATIONS                 |  21808 |      1 |  21808 |00:00:00.33 |   64353 |
|* 46 |               INDEX UNIQUE SCAN                            | HZ_LOCATIONS_U1              |  21808 |      1 |  21808 |00:00:00.18 |   42545 |
|  47 |             VIEW PUSHED PREDICATE                          | VW_SSQ_1                     |  21808 |      1 |  21808 |00:00:01.17 |   93476 |
|  48 |              SORT GROUP BY                                 |                              |  21808 |      1 |  21808 |00:00:01.06 |   93476 |
|  49 |               TABLE ACCESS BY INDEX ROWID BATCHED          | RA_CUSTOMER_TRX_LINES_ALL    |  21808 |     16 |    145K|00:00:00.84 |   93476 |
|* 50 |                INDEX RANGE SCAN                            | XXC_CUSTOMER_GETPAID         |  21808 |     16 |    145K|00:00:00.36 |   59938 |
|  51 |            VIEW PUSHED PREDICATE                           | VW_SSQ_2                     |  21808 |      1 |  21808 |00:00:00.69 |   74433 |
|  52 |             SORT GROUP BY                                  |                              |  21808 |      1 |  21808 |00:00:00.59 |   74433 |
|  53 |              TABLE ACCESS BY INDEX ROWID BATCHED           | RA_CUSTOMER_TRX_LINES_ALL    |  21808 |      8 |  92201 |00:00:00.49 |   74433 |
|* 54 |               INDEX RANGE SCAN                             | XXC_CUSTOMER_GETPAID         |  21808 |     12 |  92201 |00:00:00.24 |   59903 |
|  55 |           VIEW PUSHED PREDICATE                            | VW_SSQ_3                     |  21808 |      1 |  21808 |00:00:00.61 |   74852 |
|  56 |            SORT GROUP BY                                   |                              |  21808 |      1 |  21808 |00:00:00.51 |   74852 |
|  57 |             TABLE ACCESS BY INDEX ROWID BATCHED            | RA_CUSTOMER_TRX_LINES_ALL    |  21808 |      8 |  53060 |00:00:00.38 |   74852 |
|* 58 |              INDEX RANGE SCAN                              | XXC_CUSTOMER_GETPAID         |  21808 |     12 |  53060 |00:00:00.19 |   59148 |
|  59 |          VIEW PUSHED PREDICATE                             | VW_SSQ_4                     |  21808 |      1 |  21808 |00:00:00.70 |   93490 |
|  60 |           SORT GROUP BY                                    |                              |  21808 |      1 |  21808 |00:00:00.61 |   93490 |
|  61 |            TABLE ACCESS BY INDEX ROWID BATCHED             | RA_CUSTOMER_TRX_LINES_ALL    |  21808 |     16 |    145K|00:00:00.63 |   93490 |
|* 62 |             INDEX RANGE SCAN                               | XXC_CUSTOMER_GETPAID         |  21808 |     16 |    145K|00:00:00.25 |   59950 |
|  63 |         VIEW PUSHED PREDICATE                              | VW_SSQ_5                     |  21808 |      1 |  21808 |00:00:00.63 |   74427 |
|  64 |          SORT GROUP BY                                     |                              |  21808 |      1 |  21808 |00:00:00.54 |   74427 |
|  65 |           TABLE ACCESS BY INDEX ROWID BATCHED              | RA_CUSTOMER_TRX_LINES_ALL    |  21808 |      8 |  92201 |00:00:00.44 |   74427 |
|* 66 |            INDEX RANGE SCAN                                | XXC_CUSTOMER_GETPAID         |  21808 |     12 |  92201 |00:00:00.21 |   59900 |
|  67 |        VIEW PUSHED PREDICATE                               | VW_SSQ_6                     |  21808 |      1 |  21808 |00:00:00.59 |   74846 |
|  68 |         SORT GROUP BY                                      |                              |  21808 |      1 |  21808 |00:00:00.50 |   74846 |
|  69 |          TABLE ACCESS BY INDEX ROWID BATCHED               | RA_CUSTOMER_TRX_LINES_ALL    |  21808 |      8 |  53060 |00:00:00.35 |   74846 |
|* 70 |           INDEX RANGE SCAN                                 | XXC_CUSTOMER_GETPAID         |  21808 |     12 |  53060 |00:00:00.17 |   59144 |
|* 71 |       INDEX RANGE SCAN                                     | HZ_GEOGRAPHIES_N11           |  21808 |   5812 |  21808 |00:00:00.13 |    2684 |
|  72 |      TABLE ACCESS BY INDEX ROWID                           | HZ_GEOGRAPHIES               |  21808 |    168 |  21808 |00:00:00.07 |     620 |
-----------------------------------------------------------------------------------------------------------------------------------------------------

```

Let’s start by raising some concerns about the quality of information available.

First, the OP says it takes 14 minutes to return 30,000 rows: but the top line of the plan says it has taken 13 minutes and 20 seconds to return the first 501 rows, and if we look a little further down the plan operation 3 (Hash Group By) reports 00:13:20.15 to aggregate down to 19,827 rows. So this half of the plan cannot return more than 19,827 rows, and the half I have discarded (for the moment) must be returning the other 10,000+ rows. The information we have is incomplete.

Of course you may think that whatever the rest of the plan does is fairly irrelevant – it’s only going to be responsible for at most another 40 seconds of processing – except my previous experience of rowsource execution statistics tells me that when you do a large number of small operations the times reported can be subject to fairly large rounding errors and that enabling the measurement can increase the execution time by a factor of three or four. It’s perfectly feasible that this half of the query is actually the faster half under normal run-time circumstances but runs much more slowly (with a much higher level of CPU utilisation) when rowsource execution stats is in enabled. So let’s not get too confident.

With that warning in mind, what can we see in this half of the plan.

Big picture: the inline scalar subqueries have disappeared. In 12c the optimimzer can unnest scalar subqueries in the select list and turn them into outer joins, and we can see that there are 6 “Nested Loop Outer” operations, corresponding to 6 “View Pushed Predicate” operations against views labelled VW_SSQ1 through to VW_SSQ6 (SSQ = Scalar Sub Query ?). This goes back to my early comment – a person could probably rewrite the 6 scalar subqueries as a single aggregate view in the from clause: the optimizer isn’t quite clever enough to manage that in this case, but in simpler cases it might be able to do exactly that.

Big picture 2: most of the 13 minutes 20 seconds appears at operation 14 as it processes the 33,459 rows supplied to it from the 4.33 seconds of work done by operation 15 and its descendants. Reducing this part of the execution plan to the smallest relevant section we get the following:

```-----------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                                  | Name                         | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------------------------------------------------------
|  14 |               NESTED LOOPS                                 |                              |      1 |      1 |  21808 |00:13:03.30 |    2961K|
|  15 |                NESTED LOOPS                                |                              |      1 |      1 |  33459 |00:00:04.33 |    1123K|
|  41 |                TABLE ACCESS BY INDEX ROWID BATCHED         | GL_DAILY_RATES               |  33459 |      1 |  21808 |00:12:44.59 |    1838K|
|* 42 |                 INDEX RANGE SCAN                           | GL_DAILY_RATES_U1            |  33459 |      1 |  21808 |00:13:08.16 |    1837K|
-----------------------------------------------------------------------------------------------------------------------------------------------------

```

For each row supplied by operation 15 Oracle calls operation 41, which calls operation 42 to do an index range scan to supply a set of rowids so that operation 41 can access a table and return rows. Apparently the total time spent by operation 41 waiting for operation 42 to return rowids and then doing its own work is 12 minutes 44 seconds, while the range scans alone (all 33,459 of them) take 13 minutes and 8 seconds. Remember, though, that “lots of small operations = scope of rounding errors” when you look at these timings. Despite the inconsistency between the timings for operations 41 and 42 it’s reasonable to conclude that between them that’s where most of the execution time went.

Two questions – (a) can we refine our analysis of how the time is split between the two operations and (b) why do these lines take so much time.

Check the Starts and the A-rows: (reminder: for comparison, we expect A-rows to be approximately E-rows * Starts) for both operations we see 33,459 starts and 21,808 rows. The index range scans return (on average) a single rowid about two-thirds of the time, and every time a range scan returns a rowid the corresponding row is returned from the table (If you check the Id column there’s no asterisk on operation 41 so no extra predicate is applied as Oracle accesses the table row – but even if there were an extra predicate we’d still be happy to infer that if 21,808 rowids returned from operation 42 turned into 21,808 rows returned from the table then there are no wasted accesses to the table).

Now look at the Buffers for the index range scan – 1.837M: that’s roughly 56 buffers per range scan – that’s a lot of index to range through to find one rowid, which is a good clue that perhaps we do a lot of work with each Start and really do use up a lot of CPU on this operation. Let’s see what the Predicate Section of the plan tells us about this range scan:

```
Predicate Information (identified by operation id):
---------------------------------------------------
42 - access("GDR"."FROM_CURRENCY"="RCT"."INVOICE_CURRENCY_CODE" AND "GDR"."TO_CURRENCY"="GLL"."CURRENCY_CODE" AND
"GDR"."CONVERSION_TYPE"='Corporate')
filter(("GDR"."CONVERSION_TYPE"='Corporate' AND TO_DATE(INTERNAL_FUNCTION("GDR"."CONVERSION_DATE"))=TO_DATE(INTERNAL_FUNCTION("RCTD"."
GL_DATE"))))

```

We have access predicates (things which narrow down the number of leaf blocks that we walk through) and filter predicates (things we do to test every key entry we access). Notably the gdr.conversion type is a filter predciate as well as an access predicate – and that suggests that our set of predicates has “skipped over” a column in the index: from_currency and to_currency might be the first two columns in the index, but conversion_type is then NOT the third.

More significantly, though, there’s a column called conversion_date in the index (maybe that’s column 3 in the index – it feels like it ought to be); but for every index entry we’ve selected from the 56 blocks we walk through we do some sort of internal conversion (or un-translated transformation) to the column then convert the result to a date to compare it with another date (similarly processed from an earlier operation). What is that “internal function” – let’s check the query:

```
AND             gdr.from_currency = rct.invoice_currency_code
AND             gdr.to_currency = gll.currency_code
AND             gdr.conversion_type = 'Corporate'
AND             to_date(gdr.conversion_date) = to_date(rctd.gl_date)
AND             rctd.gl_date BETWEEN To_date ('01-JAN-2018', 'DD-MON-YYYY') AND  To_date ('31-JAN-2018', 'DD-MON-YYYY')

```

(I’ve swapped the order of a couple of lines to highlight a detail).

The filter predicate is comparing gdr.conversion_date with rctd.gl_date – and we can probably assume that both columns really are dates because (a) the word “date” is in their names and (b) the rctd.gl_date is being compared with genuine date values in the next predicate down (and – though I haven’t shown it – the way the plan reports the next predicate proves that the column really is a date datatype).

So the predicate in the SQL applies the to_date() function to two columns that are dates – which means the optimizer has to convert the date columns to some default character format and then convert them back to dates. The “internal function” is a to_char() call. Conversions between date and character formats are CPU-intensive, and we’re doing a double conversion (to_date(to_char(column_value)) to every data value in roughly 56 blocks of an index each time we call that line of the plan. It’s not surprising we spend a lot of time in that line.

### Initial strategy:

Check the column types for those two columns, if they are both date types decide whether or not the predicate could be modified to a simple gdr.conversion_date = rctd.gl_date (though it’s possible that something slightly more sophisticated should be used) but whatever you do avoid the redundant conversion through character format.

Ideally, of course, if we can avoid this conversion we may find that Oracle can be more accurate in its range scan through the index, but we may still find that we do a large range scan even if we do manage to do it a little more efficiently, in which case we may want to see if there is an alternative index which will allow use to pick the one rowid we need from the index without  visiting so many leaf blocks in the index.

### Warning

Simply eliminating the to_date() calls may changes the results. Here’s a demonstration of how nasty things happen when you apply to_date() to a date:

```
SQL> desc t1
Name                          Null?    Type
----------------------------- -------- --------------------
D1                                     DATE
D2                                     DATE

SQL> insert into t1 values(sysdate, sysdate + 10/86400);

1 row created.

SQL&g```
Partitioning -- 6 : Hash Partitioning - 30-Sep-2018 07:25 - Hemant K Chitale
Unlike Range or List Partitioning where you define the rule which identifies which Partition a row will be inserted into (based on the value in the Partition Key Column(s)),  Hash Partitioning relies on Oracle applying a "hashing formula (algorithm)" to "randomly" distribute incoming rows across the available Partitions in the table.
This would be useful when you want to break up a table into smaller physical segments (maybe into even separate Tablespaces on different disks) without considering grouping of data.  In Date based Range Partitioning data is grouped into different Partitions -- i.e. physical segments on disk --  based on the Date value (e.g. by Month or Year).  In List Partitioning, data is grouped based on the value in the Partition Key Column.

Here is a small example on Hash Partitioning :

`SQL> create table iot_incoming_data  2  (data_item_number number,  3   data_item_key varchar2(32),  4   data_item_value varchar2(64),  5   data_item_timestamp timestamp)  6  partition by hash (data_item_number)  7  (partition p1 tablespace hash_ptn_1,  8   partition p2 tablespace hash_ptn_2,  9   partition p3 tablespace hash_ptn_3, 10   partition p4 tablespace hash_ptn_4) 11  /Table created.SQL> `

In this definition of the table, I have "randomly" distributed incoming rows across 4 Partitions in 4 different Tablespaces.  Given the incoming "data_item_number" values (either machine generated or from a sequence), each of the 4 Partitions would be equally loaded.
(In contrast, in Date based Range Partitioning of, say, a SALES table, you might have fewer rows in older Partitions and an increasing number of rows in new Partitions as your business and Sales Volume grow over time !).

Unlike Range Partitioning, Hash Partitioning will not perform well for a "range based query"  (e.g. a range of sales dates or a range of data item numbers).  It is suitable for "equality" or "in-list" predicates.  If you do need a range based query, you would need a Global Index.

Note that it is advised that you should use a Power of 2 for the number of Hash Partitions.

Let me demonstrate insertion of data :

`SQL> insert into iot_incoming_data  2  select rownum,                                 3  dbms_random.string('X',16),  4  dbms_random.string('X',32),  5  systimestamp  6  from dual  7  connect by level < 10001;10000 rows created.SQL> commit;Commit complete.SQL> SQL> exec dbms_stats.gather_table_stats('HEMANT','IOT_INCOMING_DATA');PL/SQL procedure successfully completed.SQL> select partition_name, num_rows  2  from user_tab_partitions  3  where table_name = 'IOT_INCOMING_DATA'  4  order by partition_position  5  /PARTITION_NAME                   NUM_ROWS------------------------------ ----------P1                                   2471P2                                   2527P3                                   2521P4                                   2481SQL> `

Note that I have inserted the 10,000 rows from a single session.  In the real world, you would have multiple sessions concurrently inserting rows into the table.
Based on the Hashing algorithm that Oracle used (note : this is internal to Oracle and we cannot use any custom algorithm), Oracle has more or less evenly distributed the incoming rows across the 4 Partitions.

Let me select some random rows from the Partitions :

`SQL> select data_item_number    2  from iot_incoming_data partition (P1)  3  where rownum < 6   4  order by 1;DATA_ITEM_NUMBER----------------            8361            8362            8369            8379            8380SQL> SQL> select data_item_number  2  from iot_incoming_data partition (P2)  3  where rownum < 6  4  order by 1  5  /DATA_ITEM_NUMBER----------------            8087            8099            8101            8105            8109SQL> SQL> select data_item_number  2  from iot_incoming_data partition (P3)  3  where rownum < 6  4  and data_item_number < 100  5  order by 1  6  /DATA_ITEM_NUMBER----------------               2               5               8              18              20SQL> SQL> select data_item_number  2  from iot_incoming_data partition (P4)  3  where rownum < 6  4  and data_item_number between 1000 and 1100  5  order by 1  6  /DATA_ITEM_NUMBER----------------            1001            1002            1005            1008            1009SQL> `

(The first two queries returned rows with values greater than 8000 simply because I didn't specify a range of values as a filter and those rows came from the first few blocks that Oracle read from the buffer cache).
Note how the DATA_ITEM_NUMBER values indicate "near-random" distribution of rows across the Partitions.  It is likely that if I had created multiple sessions concurrently running inserts into the table, distribution of the rows would have been even more "random".

Hacking for Skew - 28-Sep-2018 08:23 - Jonathan Lewis

In my presentation to the UKOUG SIG yesterday “Struggling with Statistics – part 2” I described a problem that I wrote about a few months ago: when you join a fact table with a massively skewed distribution on one of the surrogate key columns to a dimension holding the unique list of keys and descriptions a query against a description “loses” the skew. Here’s an demo of the problem that’s a little simpler than the one in the previous article.

```
rem
rem     Script:         bitmap_join_histogram.sql
rem     Author:         Jonathan Lewis
rem     Dated:          June 2016
rem     Updated:        Sep 2018
rem

execute dbms_random.seed(0)

create table facts
nologging
as
with generator as (
select  --+ materialize
rownum id
from dual
connect by
level <= 1e4 --> comment to avoid wordpress format issue
)
select
rownum                                  id,
trunc(3 * abs(dbms_random.normal))      id_status,
from
generator       v1,
generator       v2
where
rownum <= 1e5 --> comment to avoid wordpress format issue
;

alter table facts add constraint fct_pk primary key(id);
alter table facts modify id_status not null;

create table statuses
as
select
id,
chr(65 + id)            status_code,
from    (
select
distinct(id_status)             id
from
facts
)
;

alter table statuses modify status_code not null;

alter table statuses add constraint sta_pk primary key (id);
alter table facts add constraint fct_fk_sta foreign key (id_status) references statuses(id);

create bitmap index fct_b1 on facts(id_status);

begin
dbms_stats.gather_table_stats(
ownname          => user,
tabname          =>'facts',
method_opt       => 'for all columns size skewonly'
);

dbms_stats.gather_table_stats(
ownname          => user,
tabname          =>'statuses',
method_opt       => 'for all columns size 254'
);
end;
/

```

The definition of the facts.id_status column means I get a nice skewing effect on the data and this is what my data looks like:

```
select id_status, count(*) from facts group by id_status order by id_status;

ID_STATUS   COUNT(*)
---------- ----------
0      26050
1      23595
2      18995
3      13415
4       8382
5       4960
6       2643
7       1202
8        490
9        194
10         55
11         17
12          2

13 rows selected.

```

The statuses table translates the numbers 0 – 12 into the letters ‘A’ – ‘M’.

A quick check will show you that there are 55 rows for id_status = 10, which means 55 rows for status_code = ‘K’. So what happens when we write the two queries that should show us these results. I don’t really care what the execution plans are at this point, I’m interested only in the optimizer’s estimate of cardinality – so here are two queries, each followed by its execution plan:

```
select
sum(fct.id)
from
facts   fct
where
fct.id_status = 10
;

-----------------------------------------------------------------------------------------------
| Id  | Operation                            | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |        |     1 |     8 |    12   (0)| 00:00:01 |
|   1 |  SORT AGGREGATE                      |        |     1 |     8 |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID BATCHED| FACTS  |    55 |   440 |    12   (0)| 00:00:01 |
|   3 |    BITMAP CONVERSION TO ROWIDS       |        |       |       |            |          |
|*  4 |     BITMAP INDEX SINGLE VALUE        | FCT_B1 |       |       |            |          |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("FCT"."ID_STATUS"=10)

select
sum(fct.id)
from
facts           fct,
statuses        sta
where
fct.id_status = sta.id
and     sta.status_code = 'K'
;

--------------------------------------------------------------------------------
| Id  | Operation           | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |          |     1 |    13 |   233   (4)| 00:00:01 |
|   1 |  SORT AGGREGATE     |          |     1 |    13 |            |          |
|*  2 |   HASH JOIN         |          |  7692 | 99996 |   233   (4)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| STATUSES |     1 |     5 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| FACTS    |   100K|   781K|   229   (3)| 00:00:01 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("FCT"."ID_STATUS"="STA"."ID")
3 - filter("STA"."STATUS_CODE"='K')

```

The estimated cardinality for the query against the base column reflects the value 55 from the histogram, but the estimated cardinality of the join is 7,692 – which is num_rows(facts) / num_distinct(id_status). Oracle has lost information about the skew. There is a way to get Oracle to produce a correct estimate (shown in the previous article) by rewriting the join as an IN subquery with the (undocumented) “precompute_subquery” hint, but there is an alternative which David Kurtz hypothesized in a conversation after the presentation was over (in fact someone else had described their use of exactly his suggested approach in a comment on a much older blog note about this problem): take the histogram from the id_status column on the facts table and “apply it” to the status_code column on the statuses table. In discussion with David I expressed the opinion that this probably shouldn’t work, and it wasn’t really a bit of fakery I’d want to apply to a production system – but we both tried it when we got home … with differing degrees of success.

Here’s a piece of code that I inserted into my script immediately after gathering stats on the statuses table. I’ll explain the details below as it makes a couple of assumptions that need to be pointed out:

```
declare

srec                    dbms_stats.statrec;

m_distcnt               number;
m_density               number;
m_nullcnt               number;
m_avgclen               number;

c_array                 dbms_stats.chararray;

begin

dbms_stats.get_column_stats(
ownname         => 'test_user',
tabname         => 'facts',
colname         => 'id_status',
distcnt         => m_distcnt,
density         => m_density,
nullcnt         => m_nullcnt,
srec            => srec,
avgclen         => m_avgclen
);

srec.bkvals := dbms_stats.numarray();
c_array     := dbms_stats.chararray();

for r in (
select  stt.status_code, count(*) ct
from    facts fct, statuses stt
where   stt.id = fct.id_status
group by
stt.status_code
order by
stt.status_code
) loop

c_array.extend;
c_array(c_array.count) := r.status_code;
srec.bkvals.extend;
srec.bkvals(srec.bkvals.count) := r.ct;

end loop;

dbms_stats.prepare_column_values(srec, c_array);

dbms_stats.set_column_stats(
ownname         => 'test_user',
tabname         => 'statuses',
colname         => 'status_code',
distcnt         => m_distcnt,
density         => m_density,
nullcnt         => m_nullcnt,
srec            => srec,
avgclen         => m_avgclen
);

end;
/

alter system flush shared_pool;

```

The code isn’t intended to be efficient, and I’ve been a bit lazy in setting up the content.

The first step gets the column stats from facts.id_status – and I know that I’ve got a frequency histogram that covers exactly the right number of distinct values on that column so almost everything is set up correctly to copy the stats across to statuses.status_code, except one column is numeric and the other is character and (although I know it’s true because of the way I defined the status_code values) I need to ensure that the bucket values I write to the status_code need to be arranged in alphabetic order of status_code.

So my second step is to run a query against the facts table to get the counts of status_code in alphabetical order and copy the results in order into a pair of arrays – one being a standalone array of the type defined in the dbms_stats package as an array of character types, the other being the array of bucket values that already exists in the stats record for the facts.id_status column that I’ve pulled into memory. (The bucket values array is stored as cumulative frequency values, so I do have to overwrite it with the simple frequency values at this point).

Finally I “prepare column values” and “set column stats” into the correct column, and the job is done. The flush of the shared pool is there to avoid any accidents of cursors surviving previous tests and causing confusion.

So what happens when I run a couple of queries with these faked stats in place ?

```set autotrace traceonly explain

select
sum(fct.id)
from
facts           fct,
statuses        sta
where
fct.id_status = sta.id
and     sta.status_code = 'K'
;

--------------------------------------------------------------------------------
| Id  | Operation           | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |          |     1 |    14 |   233   (4)| 00:00:01 |
|   1 |  SORT AGGREGATE     |          |     1 |    14 |            |          |
|*  2 |   HASH JOIN         |          |    55 |   770 |   233   (4)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| STATUSES |     1 |     6 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| FACTS    |   100K|   781K|   229   (3)| 00:00:01 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("FCT"."ID_STATUS"="STA"."ID")
3 - filter("STA"."STATUS_CODE"='K')

select
sum(fct.id)
from
facts           fct,
statuses        sta
where
fct.id_status = sta.id
and     sta.status_code = 'D'
;

--------------------------------------------------------------------------------
| Id  | Operation           | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |          |     1 |    14 |   233   (4)| 00:00:01 |
|   1 |  SORT AGGREGATE     |          |     1 |    14 |            |          |
|*  2 |   HASH JOIN         |          | 13415 |   183K|   233   (4)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| STATUSES |     2 |    12 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| FACTS    |   100K|   781K|   229   (3)| 00:00:01 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("FCT"."ID_STATUS"="STA"."ID")
3 - filter("STA"."STATUS_CODE"='D')

```

Querying for ‘K’ the prediction is 55 rows, querying for ‘D’ the prediction is for 13,415 rows – both estimates are exactly right. Wow !!!

Problem – that’s not what David Kurtz saw. In an email to me he said: “To my surprise, if I fake a histogram on the dimension table using the skew on the join column from the fact table I do get the correct number of rows calculated in the execution plan (provided it is less than the value if the histogram was not present)”. To make that concrete – when he queried for ‘K’ he got the correct prediction, when he queried for ‘D’ he was back to a prediction of 7,692. Looking at the report of the actual data, he’d get the right prediction for codes ‘F’ to ‘M’ and the wrong prediction for codes ‘A’ to ‘E’.

So what went wrong (and with whom) ?

When I run up new tests I tend to test Oracle versions in the order 12.1.0.2, then 11.2.0.4, then 12.2.0.1, then 18.3.0.0 – it’s the order of popularity that I currently see. So I was running my test on 12.1.0.2; David was running his test on 18.3.0.0. So I jumped a step and ran my test on 12.2.0.1: here are my results when querying for status_code = ‘D’:

```
--------------------------------------------------------------------------------
| Id  | Operation           | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |          |     1 |    14 |   233   (4)| 00:00:01 |
|   1 |  SORT AGGREGATE     |          |     1 |    14 |            |          |
|*  2 |   HASH JOIN         |          |  7692 |   105K|   233   (4)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| STATUSES |     1 |     6 |     2   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| FACTS    |   100K|   781K|   229   (3)| 00:00:01 |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("FCT"."ID_STATUS"="STA"."ID")
3 - filter("STA"."STATUS_CODE"='D')

```

As David has seen with 18.3, Oracle used the num_distinct to estimate the cardinality for  ‘D’. (It still used the value indicated by the histogram for ‘K’.) When I set the optimizer_features_enable parameter back to 12.1.0.2 the cardinality estimate for ‘D’ wentback to 13,415 – so it looks as if this is a deliberate piece of coding. 172 fix controls and 31 optimizer state parameters changed, but none of the more likely looking candidates had any effect when I tried testing them separately; possibly there’s a new sanity check when the number of rows recorded for the table is a long way off the total histogram bucket count.

I took a quick look at the 10053 trace in 12.2, with and without the change to optimizer_features_enable. The key difference was in the single table access path analysis – which didn’t give me any further clues.

```With optimizer_features_enable = 12.1.0.2
=========================================
Access path analysis for STATUSES
***************************************
SINGLE TABLE ACCESS PATH
Single Table Cardinality Estimation for STATUSES[STA]
SPD: Return code in qosdDSDirSetup: NOCTX, estType = TABLE

kkecdn: Single Table Predicate:"STA"."STATUS_CODE"='K'
Estimated selectivity: 5.5000e-04 , endpoint value predicate, col: #2

Access path analysis for STATUSES
***************************************
SINGLE TABLE ACCESS PATH
Single Table Cardinality Estimation for STATUSES[STA]
SPD: Return code in qosdDSDirSetup: NOCTX, estType = TABLE

kkecdn: Single Table Predicate:"STA"."STATUS_CODE"='D'
Estimated selectivity: 0.134150 , endpoint value predicate, col: #2

With optimizer_features_enable defaulting to 12.2.0.1
=====================================================
Access path analysis for STATUSES
***************************************
SINGLE TABLE ACCESS PATH
Single Table Cardinality Estimation for STATUSES[STA]
SPD: Return code in qosdDSDirSetup: NOCTX, estType = TABLE

kkecdn: Single Table Predicate:"STA"."STATUS_CODE"='K'
Estimated selectivity: 5.5000e-04 , endpoint value predicate, col: #2

Access path analysis for STATUSES
***************************************
SINGLE TABLE ACCESS PATH
Single Table Cardinality Estimation for STATUSES[STA]
SPD: Return code in qosdDSDirSetup: NOCTX, estType = TABLE

kkecdn: Single Table Predicate:"STA"."STATUS_CODE"='D'
Estimated selectivity: 0.076923 , endpoint value predicate, col: #2

```

Bottom line on this – there’s at least one person who already uses this method to work around the optimizer limitation, they need to be careful when they upgrade to 12.2 (or above) as the method no longer works in all cases.

 row(s) 1 - 10 of more than 10011 - 20 of 10021 - 30 of 10031 - 40 of 10041 - 50 of 10051 - 60 of 10061 - 70 of 10071 - 80 of 10081 - 90 of 10091 - 100 of 100 Next