You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@impala.apache.org by "Paul Rogers (JIRA)" <ji...@apache.org> on 2019/02/19 18:46:00 UTC

[jira] [Created] (IMPALA-8218) Use "simple bin model" to estimate M:N, FK join cardinality

Paul Rogers created IMPALA-8218:
-----------------------------------

             Summary: Use "simple bin model" to estimate M:N, FK join cardinality
                 Key: IMPALA-8218
                 URL: https://issues.apache.org/jira/browse/IMPALA-8218
             Project: IMPALA
          Issue Type: Improvement
          Components: Frontend
    Affects Versions: Impala 3.1.0
            Reporter: Paul Rogers
            Assignee: Paul Rogers


When computing join cardinality, the planner must determine how much a filters reduce the cardinality of join keys. For example, in a M:1 (FK/PK) join, filtering on the left (M, FK) side will reduce the number of rows available to join. How much does that filtering reduce the keys?

The "selectivity" of a filter is the probability that any one row will pass through the filter:

{noformat}
|T'| = |T| * sel(f)

sel(f) = |T'| / |T|
{noformat}

Where:

* \{{|T|}} is the cardinality of some table or relation T.
* \{{|T'|}} is the cardinality of the new relation T' after filtering.
* \{{sel(f)}} is the selectivity of some filter f.

The current model makes the standard assumption that the NDV of key columns reduces by the same amount as the table cardinality. That is:

{noformat}
|T.k'| = |T.k| * sel(f)
{noformat}

This assumption is incorrect as explained below. The correct expression is explained in Swami & Schiefer (S&S), [On the Estimation of Join Result Sizes|https://pdfs.semanticscholar.org/2735/672262940a23cfce8d4cc1e25c0191de7da7.pdf] (S&S), section 5.


h4. Motivation

It is easiest to reason about these issues in the M:1 (FK/PK) case, but the logic also applies to both sides of the M:N (many-to-many, generic) case.

Consider an example. We have a M:1 relationship with two tables: D (for detail) and M (for master). A M:1 relationship exists:

{noformat}
D.fk --> M.pk
{noformat}

The foreign key (fk) in the detail table points to the primary key (pk) in the master table. Note that keys in the master table are unique, so the simple filtering expression works fine:

{noformat}
|M.pk'| = |M.pk| * sel(f)
{noformat}

On the detail side, there are multiple rows that have the same key. What we want to know is, when we apply the filtering above, how many of the foreign keys survive? Since there are multiple keys, the answer is not obvious. To see, this, let's assume that the detail table has 1000 rows, and uses only one FK value. If we select 500 rows, how many foreign keys are left? Obviously, the answer is that there is still one FK value.


h4. Correlated Filtering

There is one case where simple filtering is the right answer: if the filter is on the key column itself. Suppose we know that the left (detail) scan applied a filter on the foreign key column. Maybe \{{D.fk = 123}}. In this case (as explained in the S&S paper above) we know that \{{|D.fk'| = 1}}. In the general case, we may have any operator (!=, <, etc.) in the filter so the cardinality of the foreign key is simply the result of applying that filter:

{noformat}
|D.fk'| = |D.fk| * sel(f)
{noformat}


h4. Uncorrelated Filtering

In the next case, we filter on columns other than the primary key column. For simplicity, let us adopt the uniformity assumption from S&S paper: that applying a filter on a column other than the key results in a random sampling of the key column. As explained in S&S, section 5, we must use the "simple urn model":

{noformat}
|T.k'| = urn( |T.k|, |T'| )

url(c, n) = c * (1 - (1 - 1/c)^n)
{noformat}

Where:

* \{{urn(c, n)}} is the urn model expression which gives the estimated cardinality of a column as the result of a scan
* \{{c}} is the cardinality of a key column
* \{{n}} is the cardinality of the output relation of a scan

See the paper for the reasoning behind this expression.


h4. Combined Filtering

To make the above work, we observe that, in relational theory, we get the same result whether we apply all filters in one go, or apply them one-by-one. We get the same result if we apply the filters during the scan (as Impala does), or by first scanning all rows, then applying the filters afterward (as some other engines do.)

This observation allows us to break the calculation into two parts:

* Determine the key NDV and table cardinality produced by just the correlated filters.
* Use the urn model to predict key cardinality from the uncorrelated filters.

We start by sorting scan filters into two categories:

* Correlated filters applied to a given key column (here, \{{D.fk}}).
* All other filters (the uncorrelated filters.)

We do this by first gathering all predicates applied to the detail (left) relation. The detail for doing so is explained in IMPALA-8217 which describes the need to adjust for filters applied to both sides of a join. Given that set, we can do the following:

* Remove from the set all those predicates which reference \{{D.fk}}. These are the correlated predicates.
* Remove correlated filtering from the scan cardinality to get the cardinality due just to the uncorrelated predicates.
* Apply correlated filtering to the key column NDV to get the reduced NDV after filtering.
* Apply the urn model to compute key cardinality after uncorrelated filtering.

Suppose we have the following:

* \{{F(D)}}: the set of all filters applied to the scan of D: \{{(f1(D), f2(D), ... fn(D))}}.
* \{{F(D.fk)}}: the subset of the above that apply to only the key column \{{D.fk}}.

Then we can compute or define the selectivity of these filters:

{noformat}
sel(scan(D)) = ∏ sel(Fi(D))

= |D'| / |D|

sel(F(D.fk)) = ∏ sel(Fi(D.fk))
{noformat}

Next we can work out the cardinalities required for inputs to the urn model:

{noformat}
|D.fk''| = |D.fk| * sel(F(D.fk))

|D''| = |D| * sel(F(D.fk))

= |D'| / sel(scan(D) * sel(F(D.fk))
{noformat}

Where:

* \{{|D.fk''|}} is the key cardinality obtained by applying just the correlated filters.
* \{{|D''|}} is the scan cardinality that would result if we applied only the correlated filters.

Then, we can apply the uncorrelated filtering using the urn model:

{noformat}
|D.fk'| = urn( |D.fk''|, |D''| )
{noformat}


h4. Compound Keys

The discussion above is for simple keys. If a key is compound (see IMPALA-8014) we can refine the above as follows:

* Compute each column in the key as described above.
* Multiply the adjusted NDV's to get the cardinality of the key as a whole, adjusting as explained in IMPALA-8014.


h4. Complication: Compound Joins

The calculation above is sound, but is complicated by several factors in Impala's implementation. First is that the left side of a join is usually not a table; it is usually another join. (This is a result of the left-deep pattern adopted by most query optimizers.) Because of this, we actually do not know the original table cardinality which we've called \{{|D|}}. Yes, we do know the size of the tables that went into a join, and we know the size of the relation that comes out of the join, but there is no reasonable number for the base table since there are multiple.

Instead, the above calculations have been based on what we do know:

* \{{|T'|}} the result of all filtering (and joins). This is the cardinality input to the present join.
* \{{F}}, the set of all filters applied anywhere in the subtree below a join input.
* \{{|D.fk|}}, the key column NDV as reported from HMS stats.

Using just these values we can calculate key cardinality as:

{noformat}
|D.fk''| = |D.fk| * sel(F(D.fk))

|D''| = |D'| / sel(scan(D) * sel(F(D.fk))

|D.fk'| = urn( |D.fk''|, |D''| )
{noformat}


h4. Tracking Adjusted NDV

The above recomputes adjusted NDV at each join from first principals. It is possible to simplify the task by working only one level at a time. Each scan or join would maintain a running adjusted NDV for each column:

* The scan node starts with the input NDVs as reported by HMS.
* The scan node computes output NDVs by applying filters to each column and tracking the result.
* The join node uses the scan output NDVs as |T.k''| (the NDV after correlated filtering.)
* The join node tracks its own output NDVs that result from applying uncorrelated filtering.

With this approach, changes bubble up the operator tree one step at a time. Debugging is easier since we can visualize the adjusted NDVs.

In the approach described in above sections, we track the combined set of all filters applied up the tree. With the approach described in this section, we track the result of applying the filters rather than the filters themselves.


h4. Complication: Non-Linear Filter Combination

The approach above assumes we can apply filters in any order which is a foundational assumption of relational theory. Unfortunately, Impala takes a different approach: it uses an exponential back-off:

{noformat}
|T'| = |T| * ∏(i =0..) Fi^i
{noformat}

This means that the filters cannot be applied in any order, nor can we easily back one filter out of the combined filter.

Impala does this to compensate, in part, for the fact that Impala does not compute selectivity for any but simple \{{col = const}} predicate.

To allow the calculations above, we must remove the exponential back-off, which requires computing selectivity for all predicates -- something that is a good idea anyway. See IMPALA-8217.


h4. Generalizing to the M:N (Generic) Case

The discussion above discusses foreign keys in a M:1 join. The same logic applies to both sides of a join in a M:N (many-to-many, "generic") join. Calculations for the right-side table are a bit simpler because, in Impala, the right input is always a base table.



--
This message was sent by Atlassian JIRA
(v7.6.3#76005)