WL#6635: Make use of condition filtering in the optimizer
Status: Complete
When the join optimizer calculates the order tables should be joined, fanout is
of great importance. The fanout for a table 't_y' in a join plan consisting of
(t_1,...,t_x,t_y,..) is the average number of rows from 't_y' that will match
each partial row combination from (t_1,...,t_x) and all predicates applicable
to 't_y'. It is important to keep the fanout as low as possible because it
directly influences how many row combinations will be joined with the next table
in the join order.
Currently, MySQL only considers:
* the fanout that is directly determined by the chosen access method.
* a very basic heuristic:
- If 'ref' access is applicable, the fanout for 'scan' is 25% of the
number of rows in the table.
- If 'range' access is applicable, the fanout for 'scan' equals the
number of rows estimated by 'range' access relative to number of rows
in the table ("rows_estimate_range_access/rows_in_table").
This worklog is for making the join optimizer consider fanout estimates for any
access method and for all conditions (except subqueries [1] and outer join ON
predicates [2]), including non-indexed columns, when calculating partial plans.
By taking this into account, query conditions can be evaluated earlier during
query execution which in turn can reduce the number of inspected rows.
[1] Subquery predicates of the form "ty.col IN/ALL/ANY (subquery)" reduce the
fanout for table 'ty', but on the other hand the cost of evaluating the subquery
is currently not part of the cost model. Without a cost estimate of evaluating
the subquery, we cannot determine if it is beneficial to move 'ty' early in the
join sequence.
[2] Technical reason: ON predicates of INNER JOINs are automatically part of the
join condition and are therefore part of condition filtering whereas ON
predicates for OUTER JOINs are stored in TABLE_LIST but are not available until
make_outerjoin_info() has been called. That happens after filtering is
calculated, so at filtering time the conditions are not easily available. It may
be decided later that the these predicates should also be taken into account.
Facts to take into account - selectivity in other products:
===========================================================
Selinger selectivity:
~~~~~~~~~~~~~~~~~~~~~
The following are "selectivity factors" as suggested by Selinger et al. in
"Access Path Selection in a Relational Database Management System", 1979
Selectivity for non-indexed columns:
------------------------------------
AND : P(A and B) = P(A) * P(B)
OR : P(A or B) = P(A) + P(B) - P(A and B)
= : 1/10
<,> : 1/3
BETWEEN : 1/4
IN (list) : MIN(#items_in_list * SEL(=), 1/2)
IN subq : [1]
NOT OP : 1-SEL(OP)
[1] Quote from paper:
columnA IN subquery:
F = (expected cardinality of the subquery result) /
(product of the cardinalities of all the relations in the subq FROM list)
This formula is derived by the following argument:
Consider the simplest case, where subq is of the form
"SELECT columnB FROM relationC ... ". Assume that the set of all columnB
values in relationC contains the set of all columnA values. If all the
tuples of relationC are selected by the subquery, then the predicate is
always TRUE and F=1. If the tuples of the subquery are restricted by a
selectivity factor F', then assume that the set of unique values in the
subquery result that match columnA values is proportionately restricted, i.e.
the selectivity factor for the predicate should be F'. F' is the product of
all the subquery's selectivity factors, namely
(subq cardinality) / (cardinality of all possible subquery answers). With a
little optimism, we can extend this reasoning to include subqueries which
are joins and subqueries in which columnB is replaced by an arithmetic
expression involving column names. This leads to the formula given above.
PostgreSQL selectivity:
~~~~~~~~~~~~~~~~~~~~~~~
Selectivity calculation in PostgreSQL is done in
/src/backend/optimizer/path/clausesel.*
PostgreSQL relies on the following statistics:
* Most Common Value (MCV) array for the 100 most common
values in a table
* "rec_per_key" for values not in MCV
* histograms
These statistics are checked first and are used to calculate selectivity if
applicable. Otherwise, the values below are used to estimate selectivity for
comparison operations.
Selectivity for non-indexed columns:
------------------------------------
= : NULL -> return 0,
BOOL datatype -> return 0.5,
<200 rows -> return 1/rowcount
>200 rows -> return DEFAULT_EQ_SEL
<,<=,>,>= : NULL -> return 0,
DEFAULT_INEQ_SEL
BETWEEN : DEFAULT_RANGE_INEQ_SEL
LIKE : exact match -> see "="
DEFAULT_MATCH_SEL
IS NULL : DEFAULT_UNK_SEL
NOT OP : 1-SEL(OP)
Default values:
---------------
/* default selectivity estimate for equalities such as "A = b" */
#define DEFAULT_EQ_SEL 0.005
/* default selectivity estimate for inequalities such as "A < b" */
#define DEFAULT_INEQ_SEL 0.3333333333333333
/* default selectivity estimate for range inequalities "A > b AND A < c" */
#define DEFAULT_RANGE_INEQ_SEL 0.005
/* default selectivity estimate for pattern-match operators such as LIKE */
#define DEFAULT_MATCH_SEL 0.005
/* default number of distinct values in a table */
#define DEFAULT_NUM_DISTINCT 200
/* default selectivity estimate for boolean and null test nodes */
#define DEFAULT_UNK_SEL 0.005
#define DEFAULT_NOT_UNK_SEL (1.0 - DEFAULT_UNK_SEL)
Functional requirements:
------------------------
F1) The feature shall be turned on and off by using the optimizer_switch
"set optimizer_switch='condition_fanout_filter=off'". The default is
'on'. When the feature is off, all query execution plans shall remain
unchanged.
F2) The feature shall take condition filtering effects into
account when deciding on the table join order. This may
result in changed query execution plans if and only if the
fanout estimate for one or more of the tables changes.
F3) EXPLAIN will show the filtering effect of 'post read filtering' in
the 'filtered' column.
F4) Post read filtering estimates shall only contain estimates
from predicates that are not part of the access method and
only if all tables that predicate depends upon are earlier in
the join sequence. For complete rules, see "Requirements: how
to calculate post read filtering estimates".
Non-functional requirements:
----------------------------
NF1) Performance when the feature is 'off': No performance degradation
is acceptable.
NF2) Performance when the feature is 'on':
a) The performance shall be as good as or better than before for 95%
of all test queries.
b) A performance degradation of 5% for up to 5% of the queries is
tolerable in the general case
c) A higher performance degradation clearly caused by skewed or
covariant data [1] is acceptable as long as NF1 is satisfied.
[1] Covariance between column values (column values "city=NewYork" and
"country=USA" are clearly covariant) cannot be predicted without
covariance histograms. Such histograms are not even planned for the
5.8 time-frame
THE PROBLEM
===========
Consider the following simple self-join:
EXPLAIN
SELECT *
FROM t1 AS t1a JOIN t1 AS t1b ON t1a.idx_col=t1b.idx_col
WHERE t1b.non_idx_col=5;
id select_type table type key key_len ref rows Extra
1 SIMPLE t1a ALL NULL NULL NULL 1000 Using where
1 SIMPLE t1b ref idx_col 5 t1a.idx_col 8 Using where
For this join, MySQL may choose either (t1a,t1b) or (t1b,t1a) join order. The
join order doesn't matter when deciding access method; in either case the QEP
will be table scan on the first table and 'ref' access for the second. MySQL
considers the cost of these join orders equal, and the chosen join order will be
the same as the order of the tables in the query.
What MySQL fails to recognize here is the filtering effect of the WHERE
predicate on t1b.non_idx_col=5. Let's assume for a second that there are 1000
rows in table t1, and that 25% of these rows satisfy the WHERE predicate. Then
MySQL will have to do the following work for the two join orders:
Join order 1: (t1a,t1b)
-----------------------
Scan t1a (one time)
For 1000 rows in t1a: Read and join on average 8 rows from t1b
(=8000 'ref' accesses)
For 8000 joined rows: Apply WHERE predicate (2000 rows will pass)
Return 2000 rows
Join order 2: (t1b,t1a)
-----------------------
Scan t1b (one time)
For 1000 rows in t1b: Apply WHERE predicate (250 rows will pass)
For 250 rows from t1b: Read and join on average 8 rows from t1a
(=2000 'ref' accesses)
Return 2000 rows
Thus, in this example, it would be much better if the join optimizer had chosen
join order 2. To put this in the context of fanout, we have that:
* The fanout for the first table in join order 1 is 1000
* The fanout for the first table in join order 2 is 250
TYPES OF FILTERING
==================
There are two types of filtering, and the fanout is the combination of these:
* Access type filtering: The filtering effect we get from using an access
method that reads fewer than all rows in the table. From the example
above, 'ref' access on the second table in the join order enables us to
read only 8 rows (instead of all 1000 rows) for each row in the first table.
* Post read filtering: The filtering effect we get from evaluating
conditions on rows after they are read from a table, but before joining
with the next table in the join order.
Access type filtering is by far the most important of these. To see why, let's
revisit the example query using join order (t1a,t1b):
QEP with 'ref' on t1b (as shown above, repeated for readability):
-----------------------------------------------------------------
Scan t1a (one time)
For 1000 rows in t1a: Read and join on average 8 rows from t1b
(=8000 'ref' accesses)
For 8000 joined rows: Apply WHERE predicate (2000 rows will pass)
Return 2000 rows
QEP with table scan on t1b (only 'post read filtering'):
--------------------------------------------------------
Scan t1a (one time)
For 1000 rows in t1a: Scan table t1b and join 1000 rows with those read
from t1a (1M joins performed)
For 1M joined rows: Apply ON and WHERE predicate (2000 rows will pass)
Return 2000 rows
Thus, MySQL does a lot more work if t1b is accessed through a table scan. Note,
however, that the *fanout* for t1b is 2 in both cases ("input" is 1000 rows
from t1a, "output" is 2000 rows => fanout=2).
The join optimizer already considers 'access type filtering' when it calculates
the cost of different join orders. In addition, some very basic 'post read
filtering' heuristic is taken into account for 'scan' access types. This
worklog will change the join optimizer so that 'post read filtering' is
considered for all access types and all types of conditions that refer to
fields, regardless of whether or not these fields are indexed. 'Access type
filtering' will remain unchanged, and the old-style 'post read filtering' for
'scan' access types will be replaced.
REQUIREMENTS: HOW TO CALCULATE POST READ FILTERING ESTIMATES
============================================================
A statement's 'post read filtering' effect will be calculated for the
subset of predicates in JOIN::conds that apply to the table currently being
processed.
The following are requirements for making 'post read filtering' estimates for
table 't_y' in join order (t_1,...,t_x,t_y,...):
R1) Only predicates applicable to 't_y' shall be part of the calculation.
R2) If predicate 'a' applies to table 't_y' but can only be evaluated if
table 't_a' has been read, the predicate is only part of the calculation
if 't_a' is in (t_1,...,t_x).
R3) Predicates already part of 'access type filtering' shall not be part of
the calculation. If, e.g., a query contains join condition
"t_a.col = t_y.col" and table t_y is read using 'ref' access with
t_a.col as input, this condition will not contribute to 'post read
filtering'.
R4) The 'post read filtering' estimate shall be visible in the 'filtered'
column of EXPLAIN.
For many predicates, MySQL will not have a high quality source of information
about the filtering effect. For example, a column that is not indexed will not
have any index statistic or range access row estimate. The following are
requirements for how to estimate the filtering effect.
R5) Filtering estimates for a predicate shall use the source of information
with highest quality that is available. Filter estimate sources (sorted on
quality of estimate): RANGE access estimate, [histograms (later)],
index statistics, guesstimate.
R6) Since MySQL has no statistics about covariance between columns,
estimates from multiple predicates shall be calculated under the
assumption that there is no covariance.
Update: During testing it became clear that a lower bound on the filtering
effect for a table was needed to avoid breakdown of the cost model. The
MySQL cost calculations depend on the fact that adding a table to the join
plan increases the cost and that the number of produced rows is a positive
number greater than zero. The lower bound is set so that the minimum fanout
for a table is 0.05 "rows". In other words, this rule is enforced:
"rows_fetched_by_access_method * filter >= 0.05"
Filter calculation details
~~~~~~~~~~~~~~~~~~~~~~~~~~
As specified in R5, range estimates and index statistics are considered to be
better sources for the filtering effect than mere guesstimates. However, when
none of the higher level information sources are available or applicable, the
filtering effect falls back to the guesstimates listed below.
The values are based on the non-indexed guesstimates made by PostgreSQL. See
"Selectivity in other products" in the HLD.
Operation | Value
----------+-------------------------------------------------------------
AND | Calculated as "Conjunction of independent events":
| P(A and B) = P(A) * P(B)
OR | Calculated as "Disjunction of independent events":
| P(A or B) = P(A) + P(B) - P(A and B)
IN | Values in IN are known to not intersect.
| Calculated as "Disjunction of events without replacement":
| MIN(#items_in_list * SEL(=), 1/2)
IN | Calculated as Conjunction of independent ' IN' events:
| SEL ( IN) = SEL(col_1 IN) * ... * SEL(col_n IN)
XOR | Calculated as "Exactly one of independent events":
| P(A and not B) + P(B and not A) = P(A) + P(B) - 2*P(A and B)
= | MAX(0.005, 1/rowcount)
<=> | SEL(=)
<,<= | MAX(1/3, 1/rowcount)
>, >= | MAX(1/3, 1/rowcount)
BETWEEN | MAX(1/9, 1/rowcount)
LIKE | SEL(BETWEEN)
IS NULL | SEL(=)
NOT OP | 1 - SEL(OP)
Fulltext | SEL(=)
Subquery | 1 (see WL#7384)
USER VISIBLE EFFECTS
====================
The result of this worklog will be:
1) The join order of queries (and other multi-table DML statements) may
change because adding 'post read filtering' effects to the join order
calculations will change the join costs.
2) EXPLAIN will show the filtering effect of 'post read filtering' in
the 'filtered' column.
HOW TO DEAL WITH PERFORMANCE REGRESSIONS
========================================
Even though a better join order is the goal of this WL, performance regressions
are inevitable. The reason is that the optimizer does not know all facts about
the data it is optimizing the query for. The main pain points for the optimizer
are as follows:
PP1) When the filtering effect of a non-indexed column is calculated, the
optimizer does not know anything about the data distribution. All
rows may have the same column value, or there may be only distinct
column values. Histograms/statistics for non-indexed columns would
reduce this issue.
PP2) If the data for a column is skewed, the filtering effect may be
calculated completely wrong even for indexed columns because the
optimizer only has access to very basic statistics (degree of
distinctness). Histograms would remove this issue.
PP3) The optimizer has no way to detect the level of covariance between
two columns and may therefore overestimate the filtering effect of
predicates over such column combinations (examples:
"WHERE city=NewYork AND country=USA" or "WHERE name=John AND sex=male")
Covariance histograms would reduce this issue.
Since performance regressions are inevitable due to the pain points listed
above, an optimizer_switch that can be used to turn the feature
'on' and 'off' will be added. The default value will be 'on'.
set optimizer_switch='condition_fanout_filter=off';
A FEW NOTES FOR DOCUMENTATION
=============================
1) Since the filtering effect does not affect the cost of the current table
but only tables reading its output rows, the condition filtering effect
is not calculated for the last table in the join order. There are,
however, a few exceptions. The filtering effect is calculated also for
the last table if:
a) The statement is EXPLAIN, or
b) Cost is calculated for scans with join buffering, or
c) The table is in a subselect (because the cost of potential
materialization depends on the number of output rows), or
d) SemiJoin is used (because the cost of some of the SJ strategies depend
on the number of output rows).
This may seem like a lot of exceptions, but it removes the overhead for
an important class of queries with only a single (non-const) table
like the ones in SysBench. It also significantly reduces the overhead for
joins with only a few tables. For example, for 't1 JOIN t2', the filtering
effect is calculated only for t1 when t1 is first and only for t2 when t2
is first. It would go like this:
Add t1 to plan; calculate filter for t1
Add t2 to plan; do NOT calculate filter (no more tables)
Done; save cost and try alternative plans
Add t2 to plan; calculate filter for t2
Add t1 to plan; do NOT calculate filter (no more tables)
Done; compare costs of "t1-t2" vs "t2-t1" and pick cheapest
Implementation:
===============
Currently, best_access_path() has the following heuristic for the 'scan'
access methods (table/index):
1) If 'ref' access is possible, fanout is 25%.
2) If 'range' access is possible, fanout is calculated as
"rows_estimate_range_access/rows_in_table"
This heuristic will be replaced by the following implementation:
Main filter implementation:
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The main filter calculation function is called from
Optimize_table_order::best_access_path() and relies on
Item*::get_filtering_effect(). The design of get_filtering_effect()
can be found below.
/*
Calculate 'Post read filtering' effect of JOIN::conds for table 'tab'.
Only conditions that are not directly involved in the chosen
access method shall be calculated in this 'Post read filtering' effect.
The function first identifies fields that are directly used by the
access method. This includes columns used by range and ref access types,
and predicates on the identified columns (if any) will not be taken into
account when the filtering effect is calculated.
The function will then calculate the filtering effect of any predicate
that applies to 'tab' and is not depending on the columns used by the
access method. The source of information with highest accuracy is
always preferred and is as follows:
1) Row estimates from the range optimizer
2) Row estimates from index statistics (rec_per_key)
3) Guesstimates
Thus, after identifying columns that are used by the access method,
the function will look for rows estimates made by the range optimizer.
If found, the estimates from the range optimizer are calculated into
the filtering effect.
The function then goes through JOIN::conds to get estimates from any
remaining predicate that applies to 'tab' and does not depend on any
tables that come later in the join sequence. Predicates that depend on
columns that are either used by the access method or used in the row
estimate from the range optimizer will not be considered in this phase.
@param tab The table condition filtering effect is calculated
for
@param keyuse Describes the 'ref' access method (if any) that is
chosen
@param used_tables Tables earlier in the join sequence than 'tab'
@return the 'post read filtering' effect (between 0 and 1) of
JOIN::conds
*/
double calculate_condition_filter(JOIN_TAB *const tab,
Key_use *const keyuse,
table_map used_tables)
{
MY_BITMAP fields_to_ignore;
double filter= 1.0;
if (keyuse) // Chosen access method is 'ref' access
bitmap_set_bit(fields_to_ignore, );
if (tab->quick)
bitmap_set_bit(fields_to_ignore, );
for (uint curkey= 0; curkey < tab->table->s->keys; curkey++)
{
if (tab->table->quick_keys.is_set(curkey))
{
MY_BITMAP fields_used_by_quick;
quick->get_fields_used(&fields_used_by_quick)
/*
Ignore rows estimates from the range optimizer on this index if
the range access method used fields that have already been
accounted for
*/
if (bitmap_is_overlapping(fields_used_by_quick, fields_to_ignore)
continue;
filter*= (tab->table->quick_rows[curkey] /
tab->table->file->stats.records);
bitmap_union(fields_to_ignore, fields_used_by_quick);
}
/*
Filter now includes the filtering effect from the range optimizer,
and fields_to_ignore is tagged with all fields that are either used
by the access method or calculated by the range optimizer.
Proceed with in-depth analysis of all predicates in JOIN::conds to
get the filtering effect of all predicates that apply to fields in
'tab' that are not tagged in fields_to_ignore.
*/
filter*= join->conds->get_filtering_effect(used_tables,
tab->table->map,
&fields_to_ignore);
return filter;
}
/**
Sets the bit of all fields this range access method uses in
'used_fields'
@param[out] indexed fields used by this range access method.
*/
QUICK_*::get_fields_used(MY_BITMAP *used_fields)
{
for (all indexes 'i' used by this quick)
for (all keyparts 'kp' used in 'i')
bitmap_set_bit(used_fields, kp->field->field_index);
}
Item tree implementation:
~~~~~~~~~~~~~~~~~~~~~~~~~
To get the filtering contribution from predicates in JOIN::conds, the function
get_filtering_effect() will be added to Item:
/*
Calculate the filter contribution that is relevant for table
'filter_for_table' for this item.
@param filter_for_table The table we are calculating filter effect for
@param read_tables Tables earlier in the join sequence.
Predicates for table 'filter_for_table' that
rely on values from these tables can be part of
the filter effect.
@param fields_to_ignore Fields in 'filter_for_table' that should not
be part of the filter calculation. The filtering
effect of these fields are already part of the
calculation somehow (e.g. because there is a
predicate "'col' = ", and the optimizer
has decided to do ref access on 'col').
@return the filtering effect (between 0 and 1) this Item contributes with.
*/
double Item*::get_filtering_effect(table_map filter_for_table,
table_map read_tables,
MY_BITMAP *fields_to_ignore);
All Item classes except the ones that may contribute to a filtering effect
(see list in "Filter calculation details" below) will inherit this:
virtual double Item::get_filtering_effect(table_map filter_for_table,
table_map read_tables,
MY_BITMAP *fields_to_ignore)
{
return 1;
}
The Item classes corresponding to the list of filter contributors (see list
below) will override this function. Example:
double Item_cond_and::get_filtering_effect(table_map filter_for_table,
table_map read_tables,
MY_BITMAP *fields_to_ignore)
{
if (!(used_tables() & filter_for_table)
return 1.0; // None of the ANDed conditions apply to this table
double filter= 1.0;
// Calculate P(A and B) = P(A) * P(B)
while (more ANDed conditions)
filter*= condition->get_filtering_effect(filter_for_table,
read_tables,
fields_to_ignore);
return filter;
}
/**
Calculate the filtering effect for 'filter_for_table' for a
multiple equal item. Multiple equal items contains two or more
items that are equal, so there may be multiple fields from a
single table. All fields from 'filter_for_table' will contribute
to the filtering effect as long as there is a value these can be
compared to.
*/
double Item_equal::get_filtering_effect(table_map filter_for_table,
table_map read_tables,
MY_BITMAP *fields_to_ignore)
{
double filter= 1.0;
// No conditions below this apply to the table
if (!(used_tables() & filter_for_table))
return COND_FILTER_NONE;
/*
Do we have a usable value to compare to? We have:
filter_for_table.field = comparable1 = ... = comparableN
comparable_value_found will be true if one or more of comparable1..N
is a) a constant, or b) a field from a table in 'read_tables'
*/
bool comparable_value_found= const_item ? true : false;
for (all fields 'f' in the multiple equality)
{
if (f->used_tables() & read_tables)
comparable_value_found= true;
else
{
if (rec_per_key estimate available for 'f')
filter*= rec_per_key / rows_in_table
else
filter*= (rows_in_table < 200) ? 1/rows_in_table : 0.005;
}
}
return comparable_value_found ? filter : 1.0;
}
/*
Utility function for condition filtering. Used to determine
if a predicate should contribute to condition filtering. In terms of
this function, a predicate shall contribute to the filtering effect
only if it uses one or more fields from 'filter_for_table' that are
not tagged in 'fields_to_ignore'.
@param read_tables Tables earlier in the join sequence
@param filter_for_table The table that filtering effect is
calculated for
@param fields_to_ignore Fields in 'filter_for_table' that are already
accounted for in the filtering effect.
@return true if this item should contribute to filtering effect.
*/
bool Item::item_contributes_to_filter(const table_map read_tables,
const table_map filter_for_table,
const MY_BITMAP *fields_to_ignore)
{
/*
Do we have a usable value to compare to? We have:
filter_for_table.field OP comparable1 OP ... OP comparableN
comparable_value_found will be true if one or more of
comparable1..N is a) a constant, or b) a field from a table in
'read_tables'
*/
bool comparable_value_found= false;
/*
Whether or not we have a field from 'filter_for_table' that is not
in 'fields_to_ignore'
*/
bool found_usable_field= false;
for (all arguments 'a' in item)
{
if ('a' is a field)
{
if ('a' is a field in 'filter_for_table' &&
'a' is not in 'ignore_fields')
{
/*
If a field from 'filter_for_table' was already found,
we have two fields from that table. One can be used as value.
*/
found_usable_field ? comparable_value_found= true :
found_usable_field= true;
}
else if ('a' is a field in 'filter_for_table' ||
'a' is a field in one of 'read_tables')
comparable_value_found= true;
}
}
return comparable_value_found && found_usable_field;
}
double Item_func_X::get_filtering_effect(table_map filter_for_table,
table_map read_tables,
MY_BITMAP *fields_to_ignore)
{
// Does this predicate apply to filter_for_table?
if (!(used_tables() & filter_for_table))
return 1;
/*
Cannot have filtering effect if some dependent table has
not been read.
*/
if (used_tables() & ~(read_tables | filter_for_table))
return 1;
/*
Is the filtering effect of this predicate already calculated
elsewhere?
*/
if (!item_contributes_to_filter(read_tables,
filter_for_table, fields_to_ignore))
return 1;
return ;
}
How filtering is used in the optimizer:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The 'post condition filtering' estimate is calculated by calling
calculate_condition_filter() from Optimize_table_order::best_access_path()
in three locations (see below):
1) Right before calling calculate_scan_cost(). Only called if scan
cost needs to be calculated.
2) After deciding on the access method (only one of the following two
will be called):
a) After deciding to use scan access method
b) After deciding to use ref access method
Optimize_table_order::best_access_path()
{
...
double filter_effect= 1.0;
best_ref= find_best_ref(...);
if ()
...
else
{
/*
Get filtering effect of *constant* predicates only
("col OP ") where OP is any comparison operator. This
filter effect is needed because the cost of scanning 'tab'
depends on how many rows can be filtered away without even
comparing them to rows in the join buffer. @see
calculate_condition_filter()
Note: The cost of using an access method is
read_from_SE_cost
+ CPU_cost_evaluate_row * table_fanout * prefix_rows
The read cost of the best access method is stored in
POSITION::read_cost and the CPU can be calculated based on
the row estimates. However, the way calculate_condition_filter()
is implemented, some of the CPU cost is added to the read_cost
and compensated by reducing the table_fanout. The resulting
total cost remains the same, but the cost of doing scans is
overestimated whenever pure read costs are compared. This
incorrectness can be remedied if const_filter is only calculated
if a join buffer will be used by the scan.
*/
const double const_filter= calculate_condition_filter(tab,
NULL,
0); // 1)
const double scan_read_cost= calculate_scan_cost(...,
const_filter);
const double scan_total_cost= ...;
if (scan_total_cost < ref_total_cost)
{
best_ref= NULL;
...
/*
Condition filtering has no effect if 'tab' is the last table
to join, unless the statement is EXPLAIN.
*/
if (thd->variables.optimizer_condition_fanout_filter &&
(remaining_tables || thd->lex->describe))
{
const double full_filter=
calculate_condition_filter(tab, NULL, read_tables); // 2a)
filter_effect= full_filter / const_filter;
}
else
filter_effect= 1.0;
}
}
...
/*
Calculate filtering effect if ref access was chosen. Condition
filtering has no effect if 'tab' is the last table to join,
unless the statement is EXPLAIN.
*/
if (best_ref &&
thd->variables.optimizer_condition_fanout_filter &&
(remaining_tables || thd->lex->describe))
filter_effect=
calculate_condition_filter(tab, best_ref, read_tables); // 2b)
pos->filter_effect= filter_effect;
...
}
The filtering effect is then taken into account in the join optimizer
like this:
Optimize_table_order::best_extension_by_limited_search()
{
...
best_access_path(...);
/* Compute the cost of extending the plan with 's' */
current_read_time= read_time
+ position->read_cost
+ read_count * position->fanout * ROW_EVALUATE_COST;
/*
Now calculate how many rows this partial plan will produce. This
should not affect current_read_time because all rows returned from
an access method must be evaluated, including those that are filtered
out by query conditions (reflected in position->filter_effect).
*/
current_record_count=
record_count * position->fanout * position->filter_effect;
position->set_prefix_costs(current_read_time, current_record_count);
...
}
Copyright (c) 2000, 2025, Oracle Corporation and/or its affiliates. All rights reserved.