11825 lines
453 KiB
C++
11825 lines
453 KiB
C++
/* Copyright (c) 2000, 2024, Oracle and/or its affiliates.
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License, version 2.0,
|
|
as published by the Free Software Foundation.
|
|
|
|
This program is designed to work with certain software (including
|
|
but not limited to OpenSSL) that is licensed under separate terms,
|
|
as designated in a particular file or component or in included license
|
|
documentation. The authors of MySQL hereby grant you an additional
|
|
permission to link the program and your derivative works with the
|
|
separately licensed software that they have either included with
|
|
the program or referenced in the documentation.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License, version 2.0, for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
|
|
|
|
/**
|
|
@file
|
|
|
|
@brief Optimize query expressions: Make optimal table join order, select
|
|
optimal access methods per table, apply grouping, sorting and
|
|
limit processing.
|
|
|
|
@defgroup Query_Optimizer Query Optimizer
|
|
@{
|
|
*/
|
|
|
|
#include "sql/sql_optimizer.h"
|
|
#include "my_base.h"
|
|
#include "sql/sql_optimizer_internal.h"
|
|
|
|
#include <limits.h>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <cmath>
|
|
#include <deque>
|
|
#include <limits>
|
|
#include <new>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "field_types.h" // enum_field_types
|
|
#include "ft_global.h"
|
|
#include "mem_root_deque.h"
|
|
#include "memory_debugging.h"
|
|
#include "my_bitmap.h"
|
|
#include "my_compiler.h"
|
|
#include "my_dbug.h"
|
|
#include "my_inttypes.h"
|
|
#include "my_sqlcommand.h"
|
|
#include "my_sys.h"
|
|
#include "mysql/strings/m_ctype.h"
|
|
#include "mysql/udf_registration_types.h"
|
|
#include "mysql_com.h"
|
|
#include "mysqld_error.h"
|
|
#include "scope_guard.h"
|
|
#include "sql/check_stack.h"
|
|
#include "sql/current_thd.h"
|
|
#include "sql/debug_sync.h" // DEBUG_SYNC
|
|
#include "sql/derror.h" // ER_THD
|
|
#include "sql/enum_query_type.h"
|
|
#include "sql/error_handler.h" // Functional_index_error_handler
|
|
#include "sql/handler.h"
|
|
#include "sql/item.h"
|
|
#include "sql/item_cmpfunc.h"
|
|
#include "sql/item_func.h"
|
|
#include "sql/item_row.h"
|
|
#include "sql/item_subselect.h"
|
|
#include "sql/item_sum.h" // Item_sum
|
|
#include "sql/iterators/basic_row_iterators.h"
|
|
#include "sql/iterators/timing_iterator.h"
|
|
#include "sql/join_optimizer/access_path.h"
|
|
#include "sql/join_optimizer/bit_utils.h"
|
|
#include "sql/join_optimizer/join_optimizer.h"
|
|
#include "sql/join_optimizer/optimizer_trace.h"
|
|
#include "sql/join_optimizer/relational_expression.h"
|
|
#include "sql/join_optimizer/walk_access_paths.h"
|
|
#include "sql/key.h"
|
|
#include "sql/key_spec.h"
|
|
#include "sql/lock.h" // mysql_unlock_some_tables
|
|
#include "sql/mysqld.h" // stage_optimizing
|
|
#include "sql/nested_join.h"
|
|
#include "sql/opt_costmodel.h"
|
|
#include "sql/opt_explain.h" // join_type_str
|
|
#include "sql/opt_hints.h" // hint_table_state
|
|
#include "sql/opt_trace.h" // Opt_trace_object
|
|
#include "sql/opt_trace_context.h"
|
|
#include "sql/parse_tree_node_base.h"
|
|
#include "sql/parser_yystype.h"
|
|
#include "sql/query_options.h"
|
|
#include "sql/query_result.h"
|
|
#include "sql/range_optimizer/partition_pruning.h"
|
|
#include "sql/range_optimizer/path_helpers.h"
|
|
#include "sql/range_optimizer/range_optimizer.h"
|
|
#include "sql/sql_base.h" // init_ftfuncs
|
|
#include "sql/sql_bitmap.h"
|
|
#include "sql/sql_class.h"
|
|
#include "sql/sql_const.h"
|
|
#include "sql/sql_const_folding.h"
|
|
#include "sql/sql_error.h"
|
|
#include "sql/sql_join_buffer.h" // JOIN_CACHE
|
|
#include "sql/sql_planner.h" // calculate_condition_filter
|
|
#include "sql/sql_test.h" // print_where
|
|
#include "sql/sql_tmp_table.h"
|
|
#include "sql/system_variables.h"
|
|
#include "sql/table.h"
|
|
#include "sql/thd_raii.h"
|
|
#include "sql/window.h"
|
|
#include "sql_string.h"
|
|
#include "template_utils.h"
|
|
|
|
using std::ceil;
|
|
using std::max;
|
|
using std::min;
|
|
|
|
const char *antijoin_null_cond = "<ANTIJOIN-NULL>";
|
|
|
|
static bool optimize_semijoin_nests_for_materialization(JOIN *join);
|
|
static void calculate_materialization_costs(JOIN *join, Table_ref *sj_nest,
|
|
uint n_tables,
|
|
Semijoin_mat_optimize *sjm);
|
|
static bool make_join_query_block(JOIN *join, Item *item);
|
|
static bool list_contains_unique_index(JOIN_TAB *tab,
|
|
bool (*find_func)(Field *, void *),
|
|
void *data);
|
|
static bool find_field_in_item_list(Field *field, void *data);
|
|
static bool find_field_in_order_list(Field *field, void *data);
|
|
static TABLE *get_sort_by_table(ORDER *a, ORDER *b, Table_ref *tables);
|
|
static void trace_table_dependencies(Opt_trace_context *trace,
|
|
JOIN_TAB *join_tabs, uint table_count);
|
|
static bool update_ref_and_keys(THD *thd, Key_use_array *keyuse,
|
|
JOIN_TAB *join_tab, uint tables, Item *cond,
|
|
table_map normal_tables,
|
|
Query_block *query_block,
|
|
SARGABLE_PARAM **sargables);
|
|
static bool pull_out_semijoin_tables(JOIN *join);
|
|
static void add_loose_index_scan_and_skip_scan_keys(JOIN *join,
|
|
JOIN_TAB *join_tab);
|
|
static ha_rows get_quick_record_count(THD *thd, JOIN_TAB *tab, ha_rows limit,
|
|
Item *condition);
|
|
static bool only_eq_ref_tables(JOIN *join, ORDER *order, table_map tables,
|
|
table_map *cached_eq_ref_tables,
|
|
table_map *eq_ref_tables);
|
|
static bool setup_join_buffering(JOIN_TAB *tab, JOIN *join, uint no_jbuf_after);
|
|
|
|
static bool test_if_skip_sort_order(JOIN_TAB *tab, ORDER_with_src &order,
|
|
ha_rows select_limit, const bool no_changes,
|
|
const Key_map *map, int *best_idx);
|
|
|
|
static Item_func_match *test_if_ft_index_order(ORDER *order);
|
|
|
|
static uint32 get_key_length_tmp_table(Item *item);
|
|
static bool can_switch_from_ref_to_range(THD *thd, JOIN_TAB *tab,
|
|
enum_order ordering,
|
|
bool recheck_range);
|
|
|
|
static bool has_not_null_predicate(Item *cond, Item_field *not_null_item);
|
|
|
|
JOIN::JOIN(THD *thd_arg, Query_block *select)
|
|
: query_block(select),
|
|
thd(thd_arg),
|
|
// @todo Can this be substituted with select->is_explicitly_grouped()?
|
|
grouped(select->is_explicitly_grouped()),
|
|
// Inner tables may always be considered to be constant:
|
|
const_table_map(INNER_TABLE_BIT),
|
|
found_const_table_map(INNER_TABLE_BIT),
|
|
// Needed in case optimizer short-cuts, set properly in
|
|
// make_tmp_tables_info()
|
|
fields(&select->fields),
|
|
tmp_table_param(thd_arg->mem_root),
|
|
lock(thd->lock),
|
|
// @todo Can this be substituted with select->is_implicitly_grouped()?
|
|
implicit_grouping(select->is_implicitly_grouped()),
|
|
select_distinct(select->is_distinct()),
|
|
keyuse_array(thd->mem_root),
|
|
order(select->order_list.first, ESC_ORDER_BY),
|
|
group_list(select->group_list.first, ESC_GROUP_BY),
|
|
m_windows(select->m_windows),
|
|
/*
|
|
Those four members are meaningless before JOIN::optimize(), so force a
|
|
crash if they are used before that.
|
|
*/
|
|
where_cond(reinterpret_cast<Item *>(1)),
|
|
having_cond(reinterpret_cast<Item *>(1)),
|
|
having_for_explain(reinterpret_cast<Item *>(1)),
|
|
tables_list(reinterpret_cast<Table_ref *>(1)),
|
|
current_ref_item_slice(REF_SLICE_SAVED_BASE),
|
|
with_json_agg(select->json_agg_func_used()) {
|
|
rollup_state = RollupState::NONE;
|
|
if (select->order_list.first) explain_flags.set(ESC_ORDER_BY, ESP_EXISTS);
|
|
if (select->group_list.first) explain_flags.set(ESC_GROUP_BY, ESP_EXISTS);
|
|
if (select->is_distinct()) explain_flags.set(ESC_DISTINCT, ESP_EXISTS);
|
|
if (m_windows.elements > 0) explain_flags.set(ESC_WINDOWING, ESP_EXISTS);
|
|
// Calculate the number of groups
|
|
for (ORDER *group = group_list.order; group; group = group->next)
|
|
send_group_parts++;
|
|
}
|
|
|
|
bool JOIN::alloc_ref_item_slice(THD *thd_arg, int sliceno) {
|
|
assert(sliceno > 0);
|
|
assert(ref_items[sliceno].is_null());
|
|
size_t count = ref_items[0].size();
|
|
Item **slice = thd_arg->mem_root->ArrayAlloc<Item *>(count);
|
|
if (slice == nullptr) return true;
|
|
ref_items[sliceno] = Ref_item_array(slice, count);
|
|
return false;
|
|
}
|
|
|
|
bool JOIN::alloc_indirection_slices() {
|
|
const int num_slices = REF_SLICE_WIN_1 + m_windows.elements;
|
|
|
|
assert(ref_items == nullptr);
|
|
ref_items = (*THR_MALLOC)->ArrayAlloc<Ref_item_array>(num_slices);
|
|
if (ref_items == nullptr) return true;
|
|
|
|
tmp_fields =
|
|
(*THR_MALLOC)
|
|
->ArrayAlloc<mem_root_deque<Item *>>(num_slices, *THR_MALLOC);
|
|
if (tmp_fields == nullptr) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
The List<Item_equal> in COND_EQUAL partially overlaps with the argument list
|
|
in various Item_cond via C-style casts. However, the hypergraph optimizer can
|
|
modify the lists in Item_cond (by calling compile()), causing an Item_equal to
|
|
be replaced with Item_func_eq, and this can cause a List<Item_equal> not to
|
|
contain Item_equal pointers anymore. This is is obviously bad if anybody wants
|
|
to actually look into these lists after optimization (in particular, NDB
|
|
wants this).
|
|
|
|
Since untangling this spaghetti seems very hard, we solve it by brute force:
|
|
Make a copy of all the COND_EQUAL lists, so that they no longer reach into the
|
|
Item_cond. This allows us to modify the Item_cond at will.
|
|
*/
|
|
static void SaveCondEqualLists(COND_EQUAL *cond_equal) {
|
|
if (cond_equal == nullptr) {
|
|
return;
|
|
}
|
|
List<Item_equal> copy;
|
|
for (Item_equal &item : cond_equal->current_level) {
|
|
copy.push_back(&item);
|
|
}
|
|
cond_equal->current_level = std::move(copy);
|
|
SaveCondEqualLists(cond_equal->upper_levels);
|
|
}
|
|
|
|
bool JOIN::check_access_path_with_fts() const {
|
|
// Only relevant to the old optimizer.
|
|
assert(!thd->lex->using_hypergraph_optimizer());
|
|
|
|
assert(query_block->has_ft_funcs());
|
|
assert(rollup_state != RollupState::NONE);
|
|
|
|
// Find all tables referenced from non-aggregated MATCH calls in the SELECT
|
|
// list or in any hidden items lifted to the SELECT list from other clauses.
|
|
table_map fulltext_tables = 0;
|
|
for (Item *field : *fields) {
|
|
WalkItem(field, enum_walk::PREFIX | enum_walk::POSTFIX,
|
|
NonAggregatedFullTextSearchVisitor(
|
|
[&fulltext_tables](Item_func_match *item) {
|
|
fulltext_tables |= item->used_tables();
|
|
return false;
|
|
}));
|
|
}
|
|
|
|
if (fulltext_tables == 0) return false;
|
|
|
|
// See if any of those tables is accessed without materialization between the
|
|
// table access path and the aggregate access path.
|
|
bool found = false;
|
|
WalkAccessPaths(
|
|
root_access_path(), this, WalkAccessPathPolicy::ENTIRE_QUERY_BLOCK,
|
|
[fulltext_tables, &found](const AccessPath *path, const JOIN *) {
|
|
if (path->type == AccessPath::AGGREGATE) {
|
|
// Does the aggregate path access any of "fulltext_tables" without an
|
|
// intermediate materialization step? GetUsedTableMap() does not see
|
|
// through materialization and returns RAND_TABLE_BIT instead of the
|
|
// actual tables if "path" reads materialized results.
|
|
found |=
|
|
Overlaps(fulltext_tables,
|
|
GetUsedTableMap(path, /*include_pruned_tables=*/true));
|
|
}
|
|
return found;
|
|
});
|
|
|
|
if (found) {
|
|
my_error(ER_NOT_SUPPORTED_YET, MYF(0),
|
|
"reading non-aggregated results of the MATCH full-text search "
|
|
"function after GROUP BY WITH ROLLUP");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Move unstructured trace text (as used by Hypergraph) into the JSON tree.
|
|
static void MoveUnstructuredToStructuredTrace(THD *thd) {
|
|
assert(thd->opt_trace.is_started());
|
|
const Opt_trace_object trace_wrapper{&thd->opt_trace};
|
|
Opt_trace_array join_optimizer{&thd->opt_trace, "join_optimizer"};
|
|
Mem_root_array<char> line{thd->mem_root};
|
|
|
|
thd->opt_trace.unstructured_trace()->contents().ForEachRemove([&](char ch) {
|
|
if (ch == '\n') {
|
|
join_optimizer.add_utf8(line.data(), line.size());
|
|
line.clear();
|
|
} else {
|
|
line.push_back(ch);
|
|
}
|
|
});
|
|
|
|
// The last line should also be terminated by '\n'.
|
|
assert(line.empty());
|
|
thd->opt_trace.set_unstructured_trace(nullptr);
|
|
}
|
|
|
|
/**
|
|
Optimizes one query block into a query execution plan (QEP.)
|
|
|
|
This is the entry point to the query optimization phase. This phase
|
|
applies both logical (equivalent) query rewrites, cost-based join
|
|
optimization, and rule-based access path selection. Once an optimal
|
|
plan is found, the member function creates/initializes all
|
|
structures needed for query execution. The main optimization phases
|
|
are outlined below:
|
|
|
|
-# Logical transformations:
|
|
- Outer to inner joins transformation.
|
|
- Equality/constant propagation.
|
|
- Partition pruning.
|
|
- COUNT(*), MIN(), MAX() constant substitution in case of
|
|
implicit grouping.
|
|
- ORDER BY optimization.
|
|
-# Perform cost-based optimization of table order and access path
|
|
selection. See JOIN::make_join_plan()
|
|
-# Post-join order optimization:
|
|
- Create optimal table conditions from the where clause and the
|
|
join conditions.
|
|
- Inject outer-join guarding conditions.
|
|
- Adjust data access methods after determining table condition
|
|
(several times.)
|
|
- Optimize ORDER BY/DISTINCT.
|
|
-# Code generation
|
|
- Set data access functions.
|
|
- Try to optimize away sorting/distinct.
|
|
- Setup temporary table usage for grouping and/or sorting.
|
|
|
|
@retval false Success.
|
|
@retval true Error, error code saved in member JOIN::error.
|
|
*/
|
|
bool JOIN::optimize(bool finalize_access_paths) {
|
|
DBUG_TRACE;
|
|
|
|
uint no_jbuf_after = UINT_MAX;
|
|
Query_block *const set_operand_block =
|
|
query_expression()->non_simple_result_query_block();
|
|
|
|
assert(query_block->leaf_table_count == 0 ||
|
|
thd->lex->is_query_tables_locked() ||
|
|
query_block == set_operand_block);
|
|
assert(tables == 0 && primary_tables == 0 && tables_list == (Table_ref *)1);
|
|
|
|
// to prevent double initialization on EXPLAIN
|
|
if (optimized) return false;
|
|
|
|
DEBUG_SYNC(thd, "before_join_optimize");
|
|
|
|
THD_STAGE_INFO(thd, stage_optimizing);
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
Opt_trace_object trace_optimize(trace, "join_optimization");
|
|
trace_optimize.add_select_number(query_block->select_number);
|
|
Opt_trace_array trace_steps(trace, "steps");
|
|
|
|
count_field_types(query_block, &tmp_table_param, *fields, false, false);
|
|
|
|
assert(tmp_table_param.sum_func_count == 0 || !group_list.empty() ||
|
|
implicit_grouping);
|
|
|
|
const bool has_windows = m_windows.elements != 0;
|
|
|
|
if (has_windows && Window::setup_windows2(thd, &m_windows))
|
|
return true; /* purecov: inspected */
|
|
|
|
if (query_block->olap == ROLLUP_TYPE && optimize_rollup())
|
|
return true; /* purecov: inspected */
|
|
|
|
if (alloc_func_list()) return true; /* purecov: inspected */
|
|
|
|
if (query_block->get_optimizable_conditions(thd, &where_cond, &having_cond))
|
|
return true;
|
|
|
|
for (Item_rollup_group_item *item : query_block->rollup_group_items) {
|
|
rollup_group_items.push_back(item);
|
|
}
|
|
for (Item_rollup_sum_switcher *item : query_block->rollup_sums) {
|
|
rollup_sums.push_back(item);
|
|
}
|
|
|
|
set_optimized();
|
|
|
|
tables_list = query_block->leaf_tables;
|
|
|
|
if (alloc_indirection_slices()) return true;
|
|
|
|
// The base ref items from query block are assigned as JOIN's ref items
|
|
ref_items[REF_SLICE_ACTIVE] = query_block->base_ref_items;
|
|
|
|
/* dump_TABLE_LIST_graph(query_block, query_block->leaf_tables); */
|
|
/*
|
|
Run optimize phase for all derived tables/views used in this SELECT,
|
|
including those in semi-joins.
|
|
*/
|
|
// if (query_block->materialized_derived_table_count) {
|
|
{ // WL#6570
|
|
for (Table_ref *tl = query_block->leaf_tables; tl; tl = tl->next_leaf) {
|
|
tl->access_path_for_derived = nullptr;
|
|
if (tl->is_view_or_derived()) {
|
|
if (tl->optimize_derived(thd)) return true;
|
|
} else if (tl->is_table_function()) {
|
|
TABLE *const table = tl->table;
|
|
if (!table->has_storage_handler()) {
|
|
if (setup_tmp_table_handler(
|
|
thd, table,
|
|
query_block->active_options() | TMP_TABLE_ALL_COLUMNS))
|
|
return true; /* purecov: inspected */
|
|
}
|
|
|
|
table->file->stats.records = 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (thd->lex->using_hypergraph_optimizer()) {
|
|
// The hypergraph optimizer also wants all subselect items to be optimized,
|
|
// so that it has cost information to attach to filter nodes.
|
|
for (Query_expression *unit = query_block->first_inner_query_expression();
|
|
unit; unit = unit->next_query_expression()) {
|
|
// Derived tables and const subqueries are already optimized
|
|
if (!unit->is_optimized() &&
|
|
unit->optimize(thd, /*materialize_destination=*/nullptr,
|
|
/*create_iterators=*/false,
|
|
/*finalize_access_paths=*/false))
|
|
return true;
|
|
}
|
|
|
|
// The hypergraph optimizer does not do const tables,
|
|
// nor does it evaluate subqueries during optimization.
|
|
query_block->add_active_options(OPTION_NO_CONST_TABLES |
|
|
OPTION_NO_SUBQUERY_DURING_OPTIMIZATION);
|
|
}
|
|
|
|
has_lateral = false;
|
|
|
|
/* dump_TABLE_LIST_graph(query_block, query_block->leaf_tables); */
|
|
|
|
row_limit = ((select_distinct || !order.empty() || !group_list.empty())
|
|
? HA_POS_ERROR
|
|
: query_expression()->select_limit_cnt);
|
|
// m_select_limit is used to decide if we are likely to scan the whole table.
|
|
m_select_limit = query_expression()->select_limit_cnt;
|
|
|
|
if (query_expression()->first_query_block()->active_options() &
|
|
OPTION_FOUND_ROWS) {
|
|
/*
|
|
Calculate found rows (ie., keep counting rows even after we hit LIMIT) if
|
|
- LIMIT is set, and
|
|
- This is the outermost query block (for a UNION query, this is the
|
|
block that contains the limit applied on the final UNION
|
|
evaluation, cf query_term.h for explanation).
|
|
*/
|
|
calc_found_rows = m_select_limit != HA_POS_ERROR &&
|
|
(!query_expression()->is_set_operation() ||
|
|
query_block == set_operand_block);
|
|
}
|
|
if (having_cond || calc_found_rows) m_select_limit = HA_POS_ERROR;
|
|
|
|
if (query_expression()->select_limit_cnt == 0 && !calc_found_rows) {
|
|
zero_result_cause = "Zero limit";
|
|
best_rowcount = 0;
|
|
set_root_access_path(create_access_paths_for_zero_rows());
|
|
goto setup_subq_exit;
|
|
}
|
|
|
|
if (where_cond || query_block->outer_join) {
|
|
if (optimize_cond(thd, &where_cond, &cond_equal, &query_block->m_table_nest,
|
|
&query_block->cond_value)) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from optimize_cond"));
|
|
return true;
|
|
}
|
|
if (query_block->cond_value == Item::COND_FALSE) {
|
|
zero_result_cause = "Impossible WHERE";
|
|
best_rowcount = 0;
|
|
set_root_access_path(create_access_paths_for_zero_rows());
|
|
goto setup_subq_exit;
|
|
}
|
|
}
|
|
if (having_cond) {
|
|
if (optimize_cond(thd, &having_cond, &cond_equal, nullptr,
|
|
&query_block->having_value)) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from optimize_cond"));
|
|
return true;
|
|
}
|
|
if (query_block->having_value == Item::COND_FALSE) {
|
|
zero_result_cause = "Impossible HAVING";
|
|
best_rowcount = 0;
|
|
set_root_access_path(create_access_paths_for_zero_rows());
|
|
goto setup_subq_exit;
|
|
}
|
|
}
|
|
|
|
if (query_block->partitioned_table_count && prune_table_partitions()) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from prune_partitions"));
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
Try to optimize count(*), min() and max() to const fields if
|
|
there is implicit grouping (aggregate functions but no
|
|
group_list). In this case, the result set shall only contain one
|
|
row.
|
|
*/
|
|
if (tables_list && implicit_grouping &&
|
|
!(query_block->active_options() & OPTION_NO_CONST_TABLES)) {
|
|
aggregate_evaluated outcome;
|
|
if (optimize_aggregated_query(thd, query_block, *fields, where_cond,
|
|
&outcome)) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from optimize_aggregated_query"));
|
|
return true;
|
|
}
|
|
switch (outcome) {
|
|
case AGGR_REGULAR:
|
|
// Query was not (fully) evaluated. Revert to regular optimization.
|
|
break;
|
|
case AGGR_DELAYED:
|
|
// Query was not (fully) evaluated. Revert to regular optimization,
|
|
// but indicate that storage engine supports HA_COUNT_ROWS_INSTANT.
|
|
select_count = true;
|
|
break;
|
|
case AGGR_COMPLETE: {
|
|
// All SELECT expressions are fully evaluated
|
|
DBUG_PRINT("info", ("Select tables optimized away"));
|
|
zero_result_cause = "Select tables optimized away";
|
|
tables_list = nullptr; // All tables resolved
|
|
best_rowcount = 1;
|
|
const_tables = tables = primary_tables = query_block->leaf_table_count;
|
|
AccessPath *path =
|
|
NewFakeSingleRowAccessPath(thd, /*count_examined_rows=*/true);
|
|
path = attach_access_paths_for_having_and_limit(path);
|
|
m_root_access_path = path;
|
|
/*
|
|
There are no relevant conditions left from the WHERE;
|
|
optimize_aggregated_query() will not return AGGR_COMPLETE if there are
|
|
any table-independent conditions, and all other conditions have been
|
|
optimized away by it. Thus, remove the condition, unless we have
|
|
EXPLAIN (in which case we will keep it for printing).
|
|
*/
|
|
if (!thd->lex->is_explain()) {
|
|
#ifndef NDEBUG
|
|
// Verify, to be sure.
|
|
if (where_cond != nullptr) {
|
|
Item *table_independent_conds = make_cond_for_table(
|
|
thd, where_cond, PSEUDO_TABLE_BITS, table_map(0),
|
|
/*exclude_expensive_cond=*/true);
|
|
assert(table_independent_conds == nullptr);
|
|
}
|
|
#endif
|
|
where_cond = nullptr;
|
|
}
|
|
goto setup_subq_exit;
|
|
}
|
|
case AGGR_EMPTY:
|
|
// It was detected that the result tables are empty
|
|
DBUG_PRINT("info", ("No matching min/max row"));
|
|
zero_result_cause = "No matching min/max row";
|
|
set_root_access_path(create_access_paths_for_zero_rows());
|
|
goto setup_subq_exit;
|
|
}
|
|
}
|
|
|
|
if (thd->lex->using_hypergraph_optimizer() &&
|
|
query_block->is_table_value_constructor) {
|
|
// Let the hypergraph optimizer handle table value constructors, even though
|
|
// they are table-less queries.
|
|
} else if (tables_list == nullptr) {
|
|
DBUG_PRINT("info", ("No tables"));
|
|
best_rowcount = 1;
|
|
error = 0;
|
|
if (make_tmp_tables_info()) return true;
|
|
count_field_types(query_block, &tmp_table_param, *fields, false, false);
|
|
// Make plan visible for EXPLAIN
|
|
set_plan_state(NO_TABLES);
|
|
create_access_paths();
|
|
return false;
|
|
}
|
|
error = -1; // Error is sent to client
|
|
|
|
{
|
|
m_windowing_steps = false; // initialization
|
|
m_windows_sort = false;
|
|
List_iterator<Window> li(m_windows);
|
|
Window *w;
|
|
while ((w = li++))
|
|
if (w->needs_sorting()) {
|
|
m_windows_sort = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!thd->lex->using_hypergraph_optimizer()) {
|
|
sort_by_table = get_sort_by_table(order.order, group_list.order,
|
|
query_block->leaf_tables);
|
|
}
|
|
|
|
if ((where_cond || !group_list.empty() || !order.empty()) &&
|
|
substitute_gc(thd, query_block, where_cond, group_list.order,
|
|
order.order)) {
|
|
// We added hidden fields to the all_fields list, count them.
|
|
count_field_types(query_block, &tmp_table_param, query_block->fields, false,
|
|
false);
|
|
}
|
|
// Ensure there are no errors prior making query plan
|
|
if (thd->is_error()) return true;
|
|
|
|
if (thd->lex->using_hypergraph_optimizer()) {
|
|
// Get the WHERE and HAVING clauses with the IN-to-EXISTS predicates
|
|
// removed, so that we can plan both with and without the IN-to-EXISTS
|
|
// conversion.
|
|
Item *where_cond_no_in2exists = remove_in2exists_conds(where_cond);
|
|
Item *having_cond_no_in2exists = remove_in2exists_conds(having_cond);
|
|
|
|
UnstructuredTrace unstructured_trace;
|
|
if (thd->opt_trace.is_started()) {
|
|
thd->opt_trace.set_unstructured_trace(&unstructured_trace);
|
|
}
|
|
|
|
// Add the contents of unstructured_trace to the JSON tree when we exit
|
|
// this scope.
|
|
const auto copy_trace = create_scope_guard([&]() {
|
|
if (thd->opt_trace.is_started()) {
|
|
MoveUnstructuredToStructuredTrace(thd);
|
|
}
|
|
});
|
|
|
|
SaveCondEqualLists(cond_equal);
|
|
|
|
m_root_access_path = FindBestQueryPlan(thd, query_block);
|
|
if (finalize_access_paths && m_root_access_path != nullptr) {
|
|
if (FinalizePlanForQueryBlock(thd, query_block)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If this query block was modified by IN-to-EXISTS conversion,
|
|
// the outer query block may want to undo that conversion and materialize
|
|
// us instead, depending on cost. (Materialization has high initial cost,
|
|
// but looking up in the materialized table is typically cheaper than
|
|
// running the entire query.) If so, we will need to plan the query again,
|
|
// but with all extra conditions added by IN-to-EXISTS removed, as those
|
|
// are specific to the values referred to by the outer query.
|
|
//
|
|
// Thus, we detect this here, and plan a second query plan. There are
|
|
// computations that could be shared between the two plans (e.g. join order
|
|
// between tables for which there is no IN-to-EXISTS-related condition),
|
|
// so it is somewhat wasteful, but experiments have shown that planning
|
|
// both at the same time quickly clutters the code with such handling;
|
|
// there are so many places such filters could be added (base table filters,
|
|
// filters after various types of joins, join conditions, post-join filters,
|
|
// HAVING, possibly others) that trying to plan paths both with and without
|
|
// them incurs complexity that is not justified by the small computational
|
|
// gain it would bring.
|
|
if (where_cond != where_cond_no_in2exists ||
|
|
having_cond != having_cond_no_in2exists) {
|
|
if (TraceStarted(thd)) {
|
|
Trace(thd)
|
|
<< "\nPlanning an alternative with in2exists conditions removed:\n";
|
|
}
|
|
where_cond = where_cond_no_in2exists;
|
|
having_cond = having_cond_no_in2exists;
|
|
assert(!finalize_access_paths);
|
|
m_root_access_path_no_in2exists = FindBestQueryPlan(thd, query_block);
|
|
} else {
|
|
m_root_access_path_no_in2exists = nullptr;
|
|
}
|
|
|
|
if (m_root_access_path == nullptr) {
|
|
return true;
|
|
}
|
|
set_plan_state(PLAN_READY);
|
|
DEBUG_SYNC(thd, "after_join_optimize");
|
|
return false;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// All of this is never called for the hypergraph join optimizer!
|
|
// ----------------------------------------------------------------------------
|
|
|
|
assert(!thd->lex->using_hypergraph_optimizer());
|
|
// Don't expect to get here if the hypergraph optimizer is enabled via an
|
|
// optimizer switch. The "is_regular()" case is necessary for SET statements.
|
|
assert(!thd->optimizer_switch_flag(OPTIMIZER_SWITCH_HYPERGRAPH_OPTIMIZER) ||
|
|
!thd->stmt_arena->is_regular());
|
|
|
|
// Set up join order and initial access paths
|
|
THD_STAGE_INFO(thd, stage_statistics);
|
|
if (make_join_plan()) {
|
|
if (thd->killed) thd->send_kill_message();
|
|
DBUG_PRINT("error", ("Error: JOIN::make_join_plan() failed"));
|
|
return true;
|
|
}
|
|
|
|
// At this stage, join_tab==NULL, JOIN_TABs are listed in order by best_ref.
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
if (zero_result_cause != nullptr) { // Can be set by make_join_plan().
|
|
set_root_access_path(create_access_paths_for_zero_rows());
|
|
goto setup_subq_exit;
|
|
}
|
|
|
|
if (!query_block->is_non_primitive_grouped()) {
|
|
/* Remove distinct if only const tables */
|
|
select_distinct &= !plan_is_const();
|
|
}
|
|
|
|
if (const_tables && !thd->locked_tables_mode &&
|
|
!(query_block->active_options() & SELECT_NO_UNLOCK)) {
|
|
TABLE *ct[MAX_TABLES];
|
|
for (uint i = 0; i < const_tables; i++) {
|
|
ct[i] = best_ref[i]->table();
|
|
ct[i]->file->ha_index_or_rnd_end();
|
|
}
|
|
mysql_unlock_some_tables(thd, ct, const_tables);
|
|
}
|
|
if (!where_cond && query_block->outer_join) {
|
|
/* Handle the case where we have an OUTER JOIN without a WHERE */
|
|
where_cond = new Item_func_true(); // Always true
|
|
}
|
|
|
|
error = 0;
|
|
/*
|
|
Among the equal fields belonging to the same multiple equality
|
|
choose the one that is to be retrieved first and substitute
|
|
all references to these in where condition for a reference for
|
|
the selected field.
|
|
*/
|
|
if (where_cond) {
|
|
where_cond =
|
|
substitute_for_best_equal_field(thd, where_cond, cond_equal, map2table);
|
|
if (thd->is_error()) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from substitute_for_best_equal"));
|
|
return true;
|
|
}
|
|
where_cond->update_used_tables();
|
|
DBUG_EXECUTE("where",
|
|
print_where(thd, where_cond, "after substitute_best_equal",
|
|
QT_ORDINARY););
|
|
}
|
|
|
|
/*
|
|
Perform the same optimization on field evaluation for all join conditions.
|
|
*/
|
|
for (uint i = const_tables; i < tables; ++i) {
|
|
JOIN_TAB *const tab = best_ref[i];
|
|
if (tab->position() != nullptr && tab->join_cond() != nullptr) {
|
|
tab->set_join_cond(substitute_for_best_equal_field(
|
|
thd, tab->join_cond(), tab->cond_equal, map2table));
|
|
if (thd->is_error()) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from substitute_for_best_equal"));
|
|
return true;
|
|
}
|
|
tab->join_cond()->update_used_tables();
|
|
if (tab->join_cond()->walk(&Item::cast_incompatible_args,
|
|
enum_walk::POSTFIX, nullptr)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (init_ref_access()) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from init_ref_access"));
|
|
return true;
|
|
}
|
|
|
|
// Update table dependencies after assigning ref access fields
|
|
update_depend_map();
|
|
|
|
THD_STAGE_INFO(thd, stage_preparing);
|
|
|
|
if (make_join_query_block(this, where_cond)) {
|
|
if (thd->is_error()) return true;
|
|
|
|
zero_result_cause = "Impossible WHERE noticed after reading const tables";
|
|
set_root_access_path(create_access_paths_for_zero_rows());
|
|
goto setup_subq_exit;
|
|
}
|
|
|
|
// Inject cast nodes into the WHERE conditions
|
|
if (where_cond != nullptr && where_cond->walk(&Item::cast_incompatible_args,
|
|
enum_walk::POSTFIX, nullptr)) {
|
|
return true;
|
|
}
|
|
error = -1; /* if goto err */
|
|
|
|
if (optimize_distinct_group_order()) return true;
|
|
|
|
if ((query_block->active_options() & SELECT_NO_JOIN_CACHE) ||
|
|
query_block->ftfunc_list->elements)
|
|
no_jbuf_after = 0;
|
|
|
|
/* Perform FULLTEXT search before all regular searches */
|
|
if (query_block->has_ft_funcs() && optimize_fts_query()) return true;
|
|
|
|
/*
|
|
By setting child_subquery_can_materialize so late we gain the following:
|
|
JOIN::compare_costs_of_subquery_strategies() can test this variable to
|
|
know if we are have finished evaluating constant conditions, which itself
|
|
helps determining fanouts.
|
|
*/
|
|
child_subquery_can_materialize = true;
|
|
|
|
/*
|
|
It's necessary to check const part of HAVING cond as
|
|
there is a chance that some cond parts may become
|
|
const items after make_join_plan() (for example
|
|
when Item is a reference to const table field from
|
|
outer join).
|
|
This check is performed only for those conditions
|
|
which do not use aggregate functions. In such case
|
|
temporary table may not be used and const condition
|
|
elements may be lost during further having
|
|
condition transformation in JOIN::exec.
|
|
*/
|
|
if (having_cond && !having_cond->has_aggregation() && (const_tables > 0)) {
|
|
having_cond->update_used_tables();
|
|
if (remove_eq_conds(thd, having_cond, &having_cond,
|
|
&query_block->having_value)) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from remove_eq_conds"));
|
|
return true;
|
|
}
|
|
if (query_block->having_value == Item::COND_FALSE) {
|
|
having_cond = new Item_func_false();
|
|
zero_result_cause =
|
|
"Impossible HAVING noticed after reading const tables";
|
|
set_root_access_path(create_access_paths_for_zero_rows());
|
|
goto setup_subq_exit;
|
|
}
|
|
}
|
|
|
|
// Inject cast nodes into the HAVING conditions
|
|
if (having_cond != nullptr &&
|
|
having_cond->walk(&Item::cast_incompatible_args, enum_walk::POSTFIX,
|
|
nullptr)) {
|
|
return true;
|
|
}
|
|
// Traverse the expressions and inject cast nodes to compatible data types,
|
|
// if needed.
|
|
for (Item *item : *fields) {
|
|
if (item->walk(&Item::cast_incompatible_args, enum_walk::POSTFIX,
|
|
nullptr)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Also GROUP BY expressions, so that find_in_group_list() doesn't
|
|
// inadvertently fail because the SELECT list has casts that GROUP BY doesn't.
|
|
for (ORDER *ord = group_list.order; ord != nullptr; ord = ord->next) {
|
|
if ((*ord->item)
|
|
->walk(&Item::cast_incompatible_args, enum_walk::POSTFIX,
|
|
nullptr)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// See if this subquery can be evaluated with subselect_indexsubquery_engine
|
|
if (const int ret = replace_index_subquery()) {
|
|
if (ret == -1) {
|
|
// Error (e.g. allocation failed, or some condition was attempted
|
|
// evaluated statically and failed).
|
|
return true;
|
|
}
|
|
|
|
create_access_paths_for_index_subquery();
|
|
set_plan_state(PLAN_READY);
|
|
/*
|
|
We leave optimize() because the rest of it is only about order/group
|
|
which those subqueries don't have and about setting up plan which
|
|
we're not going to use due to different execution method.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
{
|
|
/*
|
|
If the hint FORCE INDEX FOR ORDER BY/GROUP BY is used for the first
|
|
table (it does not make sense for other tables) then we cannot do join
|
|
buffering.
|
|
*/
|
|
if (!plan_is_const()) {
|
|
const TABLE *const first = best_ref[const_tables]->table();
|
|
if ((first->force_index_order && !order.empty()) ||
|
|
(first->force_index_group && !group_list.empty()))
|
|
no_jbuf_after = 0;
|
|
}
|
|
|
|
bool simple_sort = true;
|
|
const Table_map_restorer deps_lateral(
|
|
&deps_of_remaining_lateral_derived_tables);
|
|
// Check whether join cache could be used
|
|
for (uint i = const_tables; i < tables; i++) {
|
|
JOIN_TAB *const tab = best_ref[i];
|
|
if (!tab->position()) continue;
|
|
if (setup_join_buffering(tab, this, no_jbuf_after)) return true;
|
|
if (tab->use_join_cache() != JOIN_CACHE::ALG_NONE) simple_sort = false;
|
|
assert(tab->type() != JT_FT ||
|
|
tab->use_join_cache() == JOIN_CACHE::ALG_NONE);
|
|
if (has_lateral && get_lateral_deps(*best_ref[i]) != 0) {
|
|
deps_of_remaining_lateral_derived_tables =
|
|
calculate_deps_of_remaining_lateral_derived_tables(all_table_map,
|
|
i + 1);
|
|
}
|
|
}
|
|
if (!simple_sort) {
|
|
/*
|
|
A join buffer is used for this table. We here inform the optimizer
|
|
that it should not rely on rows of the first non-const table being in
|
|
order thanks to an index scan; indeed join buffering of the present
|
|
table subsequently changes the order of rows.
|
|
*/
|
|
simple_order = simple_group = false;
|
|
}
|
|
}
|
|
|
|
if (!plan_is_const() && !order.empty()) {
|
|
/*
|
|
Force using of tmp table if sorting by a SP or UDF function due to
|
|
their expensive and probably non-deterministic nature.
|
|
*/
|
|
for (ORDER *tmp_order = order.order; tmp_order;
|
|
tmp_order = tmp_order->next) {
|
|
Item *item = *tmp_order->item;
|
|
if (item->cost().IsExpensive()) {
|
|
/* Force tmp table without sort */
|
|
simple_order = simple_group = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Check if we need to create a temporary table prior to any windowing.
|
|
|
|
(1) If there is ROLLUP, which happens before DISTINCT, windowing and ORDER
|
|
BY, any of those clauses needs the result of ROLLUP in a tmp table.
|
|
|
|
Rows which ROLLUP adds to the result are visible only to DISTINCT,
|
|
windowing and ORDER BY which we handled above. So for the rest of
|
|
conditions ((2), etc), we can do as if there were no ROLLUP.
|
|
|
|
(2) If all tables are constant, the query's result is guaranteed to have 0
|
|
or 1 row only, so all SQL clauses discussed below (DISTINCT, ORDER BY,
|
|
GROUP BY, windowing, SQL_BUFFER_RESULT) are useless and need no tmp
|
|
table.
|
|
|
|
(3) If there is GROUP BY which isn't resolved by using an index or sorting
|
|
the first table, we need a tmp table to compute the grouped rows.
|
|
GROUP BY happens before windowing; so it is a pre-windowing tmp
|
|
table.
|
|
|
|
(4) (5) If there is DISTINCT, or ORDER BY which isn't resolved by using an
|
|
index or sorting the first table, those clauses need an input tmp table.
|
|
If we have windowing, as those clauses are used after windowing, they can
|
|
use the last window's tmp table.
|
|
|
|
(6) If there are different ORDER BY and GROUP BY orders, ORDER BY needs an
|
|
input tmp table, so it's like (5).
|
|
|
|
(7) If the user wants us to buffer the result, we need a tmp table. But
|
|
windowing creates one anyway, and so does the materialization of a derived
|
|
table.
|
|
|
|
See also the computation of Window::m_short_circuit,
|
|
where we make sure to create a tmp table if the clauses above want one.
|
|
|
|
(8) If the first windowing step needs sorting, filesort() will be used; it
|
|
can sort one table but not a join of tables, so we need a tmp table
|
|
then. If GROUP BY was optimized away, the pre-windowing result is 0 or 1
|
|
row so doesn't need sorting.
|
|
*/
|
|
|
|
if (rollup_state != RollupState::NONE && // (1)
|
|
(select_distinct || has_windows || !order.empty()))
|
|
need_tmp_before_win = true;
|
|
|
|
/*
|
|
If we have full-text columns involved in aggregation, we may need to
|
|
materialize them. Materialization is needed if the result of a full-text
|
|
search (the MATCH function) is accessed after aggregation, as the saving and
|
|
loading of rows in AggregateIterator does not include FTS information. If we
|
|
have a GROUP BY, we'll either have an aggregate-to-table or a sort, which
|
|
fixes the issue. However, in the case of implicit grouping, we need to force
|
|
the temporary table here.
|
|
*/
|
|
if (!need_tmp_before_win && implicit_grouping &&
|
|
contains_non_aggregated_fts()) {
|
|
need_tmp_before_win = true;
|
|
}
|
|
|
|
if (!plan_is_const()) // (2)
|
|
{
|
|
if ((!group_list.empty() && !simple_group) || // (3)
|
|
(!has_windows && (select_distinct || // (4)
|
|
(!order.empty() && !simple_order) || // (5)
|
|
(!group_list.empty() && !order.empty()))) || // (6)
|
|
((query_block->active_options() & OPTION_BUFFER_RESULT) &&
|
|
!has_windows &&
|
|
!(query_expression()->derived_table &&
|
|
query_expression()
|
|
->derived_table->uses_materialization())) || // (7)
|
|
(has_windows && (primary_tables - const_tables) > 1 && // (8)
|
|
m_windows[0]->needs_sorting() && !group_optimized_away))
|
|
need_tmp_before_win = true;
|
|
}
|
|
|
|
DBUG_EXECUTE("info", TEST_join(this););
|
|
|
|
if (alloc_qep(tables)) return (error = 1); /* purecov: inspected */
|
|
|
|
if (!plan_is_const()) {
|
|
// Test if we can use an index instead of sorting
|
|
test_skip_sort();
|
|
|
|
if (finalize_table_conditions(thd)) return true;
|
|
}
|
|
|
|
if (make_join_readinfo(this, no_jbuf_after))
|
|
return true; /* purecov: inspected */
|
|
|
|
if (make_tmp_tables_info()) return true;
|
|
|
|
/*
|
|
If we decided to not sort after all, update the cost of the JOIN.
|
|
Windowing sorts are handled elsewhere
|
|
*/
|
|
if (sort_cost > 0.0 &&
|
|
!explain_flags.any(ESP_USING_FILESORT, ESC_WINDOWING)) {
|
|
best_read -= sort_cost;
|
|
sort_cost = 0.0;
|
|
}
|
|
|
|
count_field_types(query_block, &tmp_table_param, *fields, false, false);
|
|
|
|
create_access_paths();
|
|
|
|
// Creating iterators may evaluate a constant hash join condition, which may
|
|
// fail:
|
|
if (thd->is_error()) return true;
|
|
|
|
if (rollup_state != RollupState::NONE && query_block->has_ft_funcs()) {
|
|
if (check_access_path_with_fts()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
At this stage, we have set up an AccessPath 'plan'. Traverse the
|
|
AccessPath structures and find components which may be offloaded to
|
|
the engines. This process is allowed to modify the AccessPath itself.
|
|
(Removing/modifying FILTERs where pushed to the engines, change JOIN*
|
|
algorithms being used, modify aggregate expressions, ...).
|
|
This will later affects which type of Iterator we should create. Thus no
|
|
Iterators should be set up until after push_to_engines() has completed.
|
|
|
|
Note that when the Hypergraph optimizer is used, there is an entirely
|
|
different code path to push_to_engine(). (We create the AcccesPath directly
|
|
instead of converting the QEP_TABs into an AccessPath structure).
|
|
In the HG case we push_to_engine() when FinalizePlanForQueryBlock()
|
|
has finalized the 'plan'.
|
|
*/
|
|
if (push_to_engines()) return true;
|
|
|
|
// Make plan visible for EXPLAIN
|
|
set_plan_state(PLAN_READY);
|
|
|
|
DEBUG_SYNC(thd, "after_join_optimize");
|
|
|
|
error = 0;
|
|
return false;
|
|
|
|
setup_subq_exit:
|
|
|
|
assert(zero_result_cause != nullptr);
|
|
assert(m_root_access_path != nullptr);
|
|
/*
|
|
Even with zero matching rows, subqueries in the HAVING clause may
|
|
need to be evaluated if there are aggregate functions in the
|
|
query. If this JOIN is part of an outer query, subqueries in HAVING may
|
|
be evaluated several times in total; so subquery materialization makes
|
|
sense.
|
|
*/
|
|
child_subquery_can_materialize = true;
|
|
|
|
trace_steps.end(); // because all steps are done
|
|
Opt_trace_object(trace, "empty_result").add_alnum("cause", zero_result_cause);
|
|
|
|
having_for_explain = having_cond;
|
|
error = 0;
|
|
|
|
if (!qep_tab && best_ref) {
|
|
/*
|
|
After creation of JOIN_TABs in make_join_plan(), we have shortcut due to
|
|
some zero_result_cause. For simplification, if we have JOIN_TABs we
|
|
want QEP_TABs too.
|
|
*/
|
|
if (alloc_qep(tables)) return true; /* purecov: inspected */
|
|
unplug_join_tabs();
|
|
}
|
|
|
|
set_plan_state(ZERO_RESULT);
|
|
return false;
|
|
}
|
|
|
|
void JOIN::change_to_access_path_without_in2exists() {
|
|
if (m_root_access_path_no_in2exists != nullptr) {
|
|
m_root_access_path = m_root_access_path_no_in2exists;
|
|
}
|
|
}
|
|
|
|
AccessPath *JOIN::create_access_paths_for_zero_rows() const {
|
|
assert(zero_result_cause != nullptr);
|
|
AccessPath *path;
|
|
if (send_row_on_empty_set()) {
|
|
// Aggregate no rows into an aggregate row.
|
|
path = NewZeroRowsAggregatedAccessPath(thd, zero_result_cause);
|
|
path = attach_access_paths_for_having_and_limit(path);
|
|
} else {
|
|
// Send no row at all (so also no need to check HAVING or LIMIT).
|
|
path = NewZeroRowsAccessPath(thd, zero_result_cause);
|
|
}
|
|
path = attach_access_path_for_update_or_delete(path);
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
Push (parts of) the query execution down to the storage engines if they
|
|
can provide faster execution of the query, or part of it.
|
|
|
|
The handler will inspect the QEP through the
|
|
AQP (Abstract Query Plan) and extract from it whatever
|
|
it might implement of pushed execution.
|
|
|
|
It is the responsibility of the handler to store
|
|
any information it need for the later execution of
|
|
pushed queries and conditions.
|
|
|
|
@retval false Success.
|
|
@retval true Error, error code saved in member JOIN::error.
|
|
*/
|
|
bool JOIN::push_to_engines() {
|
|
DBUG_TRACE;
|
|
assert(m_root_access_path != nullptr);
|
|
|
|
for (Table_ref *tl = query_block->leaf_tables; tl; tl = tl->next_leaf) {
|
|
const handlerton *hton = tl->table->file->hton_supporting_engine_pushdown();
|
|
if (hton != nullptr) { // Involved an engine supporting pushdown.
|
|
if (unlikely(hton->push_to_engine(thd, m_root_access_path, this))) {
|
|
return true;
|
|
}
|
|
break; // Assume that at most a single handlerton per query support
|
|
// pushdown
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Substitute all expressions in the WHERE condition and ORDER/GROUP lists
|
|
that match generated columns (GC) expressions with GC fields, if any.
|
|
|
|
@details This function does 3 things:
|
|
1) Creates list of all GC fields that are a part of a key and the GC
|
|
expression is a function. All query tables are scanned. If there's no
|
|
such fields, function exits.
|
|
2) By means of Item::compile() WHERE clause is transformed.
|
|
@see Item_func::gc_subst_transformer() for details.
|
|
3) If there's ORDER/GROUP BY clauses, this function tries to substitute
|
|
expressions in these lists with GC too. It removes from the list of
|
|
indexed GC all elements which index blocked by hints. This is done to
|
|
reduce amount of further work. Next it goes through ORDER/GROUP BY list
|
|
and matches the expression in it against GC expressions in indexed GC
|
|
list. When a match is found, the expression is replaced with a new
|
|
Item_field for the matched GC field. Also, this new field is added to
|
|
the hidden part of all_fields list.
|
|
|
|
@param thd thread handle
|
|
@param query_block the current select
|
|
@param where_cond the WHERE condition, possibly NULL
|
|
@param group_list the GROUP BY clause, possibly NULL
|
|
@param order the ORDER BY clause, possibly NULL
|
|
|
|
@return true if the GROUP BY clause or the ORDER BY clause was
|
|
changed, false otherwise
|
|
*/
|
|
|
|
bool substitute_gc(THD *thd, Query_block *query_block, Item *where_cond,
|
|
ORDER *group_list, ORDER *order) {
|
|
List<Field> indexed_gc;
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
Opt_trace_object subst_gc(trace, "substitute_generated_columns");
|
|
|
|
// Collect all GCs that are a part of a key
|
|
for (Table_ref *tl = query_block->leaf_tables; tl; tl = tl->next_leaf) {
|
|
if (tl->table->s->keys == 0) continue;
|
|
for (uint i = 0; i < tl->table->s->fields; i++) {
|
|
Field *fld = tl->table->field[i];
|
|
if (fld->is_gcol() &&
|
|
!(fld->part_of_key.is_clear_all() &&
|
|
fld->part_of_prefixkey.is_clear_all()) &&
|
|
fld->gcol_info->expr_item->can_be_substituted_for_gc()) {
|
|
// Don't check allowed keys here as conditions/group/order use
|
|
// different keymaps for that.
|
|
indexed_gc.push_back(fld);
|
|
}
|
|
}
|
|
}
|
|
// No GC in the tables used in the query
|
|
if (indexed_gc.elements == 0) return false;
|
|
|
|
if (where_cond) {
|
|
// Item_func::compile will dereference this pointer, provide valid value.
|
|
uchar i, *dummy = &i;
|
|
if (where_cond->compile(&Item::gc_subst_analyzer, &dummy,
|
|
&Item::gc_subst_transformer,
|
|
pointer_cast<uchar *>(&indexed_gc)) == nullptr)
|
|
return true;
|
|
subst_gc.add("resulting_condition", where_cond);
|
|
}
|
|
|
|
// An error occur during substitution. Let caller handle it.
|
|
if (thd->is_error()) return false;
|
|
|
|
if (!(group_list || order)) return false;
|
|
// Filter out GCs that do not have index usable for GROUP/ORDER
|
|
Field *gc;
|
|
List_iterator<Field> li(indexed_gc);
|
|
|
|
while ((gc = li++)) {
|
|
Key_map tkm = gc->part_of_key;
|
|
tkm.intersect(group_list ? gc->table->keys_in_use_for_group_by
|
|
: gc->table->keys_in_use_for_order_by);
|
|
if (tkm.is_clear_all()) li.remove();
|
|
}
|
|
if (!indexed_gc.elements) return false;
|
|
|
|
// Index could be used for ORDER only if there is no GROUP
|
|
ORDER *list = group_list ? group_list : order;
|
|
bool changed = false;
|
|
for (ORDER *ord = list; ord; ord = ord->next) {
|
|
li.rewind();
|
|
if (!(*ord->item)->can_be_substituted_for_gc()) continue;
|
|
while ((gc = li++)) {
|
|
Item_field *const field =
|
|
get_gc_for_expr(*ord->item, gc, gc->result_type());
|
|
if (field != nullptr) {
|
|
changed = true;
|
|
/* Add new field to field list. */
|
|
Item **new_field = query_block->add_hidden_item(field);
|
|
thd->change_item_tree(ord->item, *new_field);
|
|
query_block->hidden_items_from_optimization++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// An error occur during substitution. Let caller handle it.
|
|
if (thd->is_error()) return false;
|
|
|
|
if (changed && trace->is_started()) {
|
|
String str;
|
|
Query_block::print_order(
|
|
thd, &str, list,
|
|
enum_query_type(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER |
|
|
QT_NO_DEFAULT_DB));
|
|
subst_gc.add_utf8(group_list ? "resulting_GROUP_BY" : "resulting_ORDER_BY",
|
|
str.ptr(), str.length());
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
/**
|
|
Sets the plan's state of the JOIN. This is always the final step of
|
|
optimization; starting from this call, we expose the plan to other
|
|
connections (via EXPLAIN CONNECTION) so the plan has to be final.
|
|
keyread_optim is set here.
|
|
*/
|
|
void JOIN::set_plan_state(enum_plan_state plan_state_arg) {
|
|
// A plan should not change to another plan:
|
|
assert(plan_state_arg == NO_PLAN || plan_state == NO_PLAN);
|
|
if (plan_state == NO_PLAN && plan_state_arg != NO_PLAN) {
|
|
if (qep_tab != nullptr) {
|
|
/*
|
|
We want to cover primary tables, tmp tables. Note that
|
|
make_tmp_tables_info() may have added a sort to the first non-const
|
|
primary table, so it's important to do this assignment after
|
|
make_tmp_tables_info().
|
|
*/
|
|
for (uint i = const_tables; i < tables; ++i) {
|
|
qep_tab[i].set_condition_optim();
|
|
qep_tab[i].set_keyread_optim();
|
|
}
|
|
}
|
|
}
|
|
|
|
DEBUG_SYNC(thd, "before_set_plan");
|
|
|
|
// If SQLCOM_END, no thread is explaining our statement anymore.
|
|
const bool need_lock = thd->query_plan.get_command() != SQLCOM_END;
|
|
|
|
if (need_lock) thd->lock_query_plan();
|
|
plan_state = plan_state_arg;
|
|
if (need_lock) thd->unlock_query_plan();
|
|
}
|
|
|
|
bool JOIN::alloc_qep(uint n) {
|
|
static_assert(MAX_TABLES <= INT_MAX8, "plan_idx needs to be wide enough.");
|
|
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
qep_tab = new (thd->mem_root)
|
|
QEP_TAB[n + 1]; // The last one holds only the final op_type.
|
|
if (!qep_tab) return true; /* purecov: inspected */
|
|
for (uint i = 0; i < n; ++i) qep_tab[i].init(best_ref[i]);
|
|
return false;
|
|
}
|
|
|
|
void QEP_TAB::init(JOIN_TAB *jt) {
|
|
jt->share_qs(this);
|
|
set_table(table()); // to update table()->reginfo.qep_tab
|
|
table_ref = jt->table_ref;
|
|
}
|
|
|
|
/// @returns semijoin strategy for this table.
|
|
uint QEP_TAB::get_sj_strategy() const {
|
|
if (first_sj_inner() == NO_PLAN_IDX) return SJ_OPT_NONE;
|
|
const uint s = join()->qep_tab[first_sj_inner()].position()->sj_strategy;
|
|
assert(s != SJ_OPT_NONE);
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
Return the index used for a table in a QEP
|
|
|
|
The various access methods have different places where the index/key
|
|
number is stored, so this function is needed to return the correct value.
|
|
|
|
@returns index number, or MAX_KEY if not applicable.
|
|
|
|
JT_SYSTEM and JT_ALL does not use an index, and will always return MAX_KEY.
|
|
|
|
JT_INDEX_MERGE supports more than one index. Hence MAX_KEY is returned and
|
|
a further inspection is needed.
|
|
*/
|
|
uint QEP_TAB::effective_index() const {
|
|
switch (type()) {
|
|
case JT_SYSTEM:
|
|
assert(ref().key == -1);
|
|
return MAX_KEY;
|
|
|
|
case JT_CONST:
|
|
case JT_EQ_REF:
|
|
case JT_REF_OR_NULL:
|
|
case JT_REF:
|
|
assert(ref().key != -1);
|
|
return uint(ref().key);
|
|
|
|
case JT_INDEX_SCAN:
|
|
case JT_FT:
|
|
return index();
|
|
|
|
case JT_INDEX_MERGE:
|
|
assert(used_index(range_scan()) == MAX_KEY);
|
|
return MAX_KEY;
|
|
|
|
case JT_RANGE:
|
|
return used_index(range_scan());
|
|
|
|
case JT_ALL:
|
|
default:
|
|
// @todo Check why JT_UNKNOWN is a valid value here.
|
|
assert(type() == JT_ALL || type() == JT_UNKNOWN);
|
|
return MAX_KEY;
|
|
}
|
|
}
|
|
|
|
uint JOIN_TAB::get_sj_strategy() const {
|
|
if (first_sj_inner() == NO_PLAN_IDX) return SJ_OPT_NONE;
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(join());
|
|
JOIN_TAB *tab = join()->best_ref[first_sj_inner()];
|
|
const uint s = tab->position()->sj_strategy;
|
|
assert(s != SJ_OPT_NONE);
|
|
return s;
|
|
}
|
|
|
|
int JOIN::replace_index_subquery() {
|
|
DBUG_TRACE;
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
Query_expression *qe = query_expression();
|
|
if (!group_list.empty() ||
|
|
!(qe->item != nullptr &&
|
|
qe->item->subquery_type() == Item_subselect::IN_SUBQUERY) ||
|
|
primary_tables != 1 || where_cond == nullptr || qe->is_set_operation())
|
|
return 0;
|
|
|
|
// Guaranteed by remove_redundant_subquery_clauses():
|
|
assert(order.empty() && !select_distinct);
|
|
|
|
Item_in_subselect *const in_subs = down_cast<Item_in_subselect *>(qe->item);
|
|
bool found_engine = false;
|
|
|
|
JOIN_TAB *const first_join_tab = best_ref[0];
|
|
|
|
if (in_subs->strategy == Subquery_strategy::SUBQ_MATERIALIZATION) {
|
|
// We cannot have two engines at the same time
|
|
} else if (first_join_tab->table_ref->is_view_or_derived() &&
|
|
first_join_tab->table_ref->derived_query_expression()
|
|
->is_recursive()) {
|
|
// The index subquery engine, which runs the derived table machinery
|
|
// from the old executor, is not capable of materializing a WITH RECURSIVE
|
|
// query from the iterator executor. Thus, be conservative here, so that the
|
|
// case never happens.
|
|
} else if (having_cond == nullptr) {
|
|
const join_type type = first_join_tab->type();
|
|
if ((type == JT_EQ_REF || type == JT_REF) &&
|
|
first_join_tab->ref().items[0]->item_name.ptr() == in_left_expr_name) {
|
|
found_engine = true;
|
|
}
|
|
} else if (first_join_tab->type() == JT_REF_OR_NULL &&
|
|
first_join_tab->ref().items[0]->item_name.ptr() ==
|
|
in_left_expr_name &&
|
|
having_cond->created_by_in2exists()) {
|
|
found_engine = true;
|
|
}
|
|
|
|
if (!found_engine) return 0;
|
|
|
|
/* Remove redundant predicates and cache constant expressions */
|
|
if (finalize_table_conditions(thd)) return -1;
|
|
|
|
if (alloc_qep(tables)) return -1; /* purecov: inspected */
|
|
unplug_join_tabs();
|
|
|
|
error = 0;
|
|
QEP_TAB *const first_qep_tab = &qep_tab[0];
|
|
|
|
if (first_qep_tab->table()->covering_keys.is_set(first_qep_tab->ref().key)) {
|
|
assert(!first_qep_tab->table()->no_keyread);
|
|
first_qep_tab->table()->set_keyread(true);
|
|
}
|
|
|
|
subselect_indexsubquery_engine *engine =
|
|
new (thd->mem_root) subselect_indexsubquery_engine(
|
|
first_qep_tab->table(), first_qep_tab->table_ref,
|
|
first_qep_tab->ref(), first_qep_tab->type(),
|
|
down_cast<Item_in_subselect *>(query_expression()->item),
|
|
first_qep_tab->condition(), having_cond);
|
|
query_expression()->item->set_indexsubquery_engine(engine);
|
|
return 1;
|
|
}
|
|
|
|
bool JOIN::optimize_distinct_group_order() {
|
|
DBUG_TRACE;
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
const bool windowing = m_windows.elements > 0;
|
|
const bool may_trace = select_distinct || !group_list.empty() ||
|
|
!order.empty() || windowing ||
|
|
tmp_table_param.sum_func_count;
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_disable_I_S trace_disabled(trace, !may_trace);
|
|
const Opt_trace_object wrapper(trace);
|
|
Opt_trace_object trace_opt(trace, "optimizing_distinct_group_by_order_by");
|
|
/* Optimize distinct away if possible */
|
|
{
|
|
ORDER *org_order = order.order;
|
|
order = ORDER_with_src(
|
|
remove_const(order.order, where_cond, rollup_state == RollupState::NONE,
|
|
&simple_order, false),
|
|
order.src, /*const_optimized=*/true);
|
|
if (thd->is_error()) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from remove_const"));
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
If we are using ORDER BY NULL or ORDER BY const_expression,
|
|
return result in any order (even if we are using a GROUP BY)
|
|
*/
|
|
if (order.empty() && org_order) skip_sort_order = true;
|
|
}
|
|
/*
|
|
Check if we can optimize away GROUP BY/DISTINCT.
|
|
We can do that if there are no aggregate functions, the
|
|
fields in DISTINCT clause (if present) and/or columns in GROUP BY
|
|
(if present) contain direct references to all key parts of
|
|
an unique index (in whatever order) and if the key parts of the
|
|
unique index cannot contain NULLs.
|
|
Note that the unique keys for DISTINCT and GROUP BY should not
|
|
be the same (as long as they are unique).
|
|
|
|
The FROM clause must contain a single non-constant table.
|
|
|
|
@todo Apart from the LIS test, every condition depends only on facts
|
|
which can be known in Query_block::prepare(), possibly this block should
|
|
move there.
|
|
*/
|
|
|
|
JOIN_TAB *const tab = best_ref[const_tables];
|
|
|
|
if (plan_is_single_table() && (!group_list.empty() || select_distinct) &&
|
|
!tmp_table_param.sum_func_count &&
|
|
(!tab->range_scan() ||
|
|
tab->range_scan()->type != AccessPath::GROUP_INDEX_SKIP_SCAN)) {
|
|
if (!group_list.empty() && rollup_state == RollupState::NONE &&
|
|
list_contains_unique_index(tab, find_field_in_order_list,
|
|
(void *)group_list.order)) {
|
|
/*
|
|
We have found that grouping can be removed since groups correspond to
|
|
only one row anyway.
|
|
*/
|
|
group_list.clean();
|
|
grouped = false;
|
|
}
|
|
if (select_distinct &&
|
|
list_contains_unique_index(tab, find_field_in_item_list, fields)) {
|
|
select_distinct = false;
|
|
trace_opt.add("distinct_is_on_unique", true)
|
|
.add("removed_distinct", true);
|
|
}
|
|
}
|
|
if (!(!group_list.empty() || tmp_table_param.sum_func_count || windowing) &&
|
|
select_distinct &&
|
|
(plan_is_single_table() || query_block->original_tables_map == 1) &&
|
|
rollup_state == RollupState::NONE) {
|
|
int order_idx = -1, group_idx = -1;
|
|
/*
|
|
We are only using one table. In this case we change DISTINCT to a
|
|
GROUP BY query if:
|
|
- The GROUP BY can be done through indexes (no sort) and the ORDER
|
|
BY only uses selected fields.
|
|
(In this case we can later optimize away GROUP BY and ORDER BY)
|
|
- We are scanning the whole table without LIMIT
|
|
This can happen if:
|
|
- We are using CALC_FOUND_ROWS
|
|
- We are using an ORDER BY that can't be optimized away.
|
|
- Selected expressions are not set functions (those cannot be put
|
|
into GROUP BY).
|
|
|
|
We don't want to use this optimization when we are using LIMIT
|
|
because in this case we can just create a temporary table that
|
|
holds LIMIT rows and stop when this table is full.
|
|
*/
|
|
if (!order.empty()) {
|
|
skip_sort_order = test_if_skip_sort_order(
|
|
tab, order, m_select_limit,
|
|
true, // no_changes
|
|
&tab->table()->keys_in_use_for_order_by, &order_idx);
|
|
count_field_types(query_block, &tmp_table_param, *fields, false, false);
|
|
}
|
|
ORDER *o;
|
|
bool all_order_fields_used;
|
|
if ((o = create_order_from_distinct(
|
|
thd, ref_items[REF_SLICE_ACTIVE], order.order, fields,
|
|
/*skip_aggregates=*/true,
|
|
/*convert_bit_fields_to_long=*/true, &all_order_fields_used))) {
|
|
group_list = ORDER_with_src(o, ESC_DISTINCT);
|
|
const bool skip_group =
|
|
skip_sort_order &&
|
|
test_if_skip_sort_order(tab, group_list, m_select_limit,
|
|
true, // no_changes
|
|
&tab->table()->keys_in_use_for_group_by,
|
|
&group_idx);
|
|
count_field_types(query_block, &tmp_table_param, *fields, false, false);
|
|
// ORDER BY and GROUP BY are using different indexes, can't skip sorting
|
|
if (group_idx >= 0 && order_idx >= 0 && group_idx != order_idx)
|
|
skip_sort_order = false;
|
|
if ((skip_group && all_order_fields_used) ||
|
|
m_select_limit == HA_POS_ERROR ||
|
|
(!order.empty() && !skip_sort_order)) {
|
|
/* Change DISTINCT to GROUP BY */
|
|
select_distinct = false;
|
|
/*
|
|
group_list was created with ORDER BY clause as prefix and
|
|
replaces it. So it must respect ordering. If there is no
|
|
ORDER BY, GROUP BY need not have to provide order.
|
|
*/
|
|
if (order.empty()) {
|
|
for (ORDER *group = group_list.order; group; group = group->next)
|
|
group->direction = ORDER_NOT_RELEVANT;
|
|
}
|
|
if (all_order_fields_used && skip_sort_order && !order.empty()) {
|
|
/*
|
|
Force MySQL to read the table in sorted order to get result in
|
|
ORDER BY order.
|
|
*/
|
|
tmp_table_param.allow_group_via_temp_table = false;
|
|
}
|
|
grouped = true; // For end_write_group
|
|
trace_opt.add("changed_distinct_to_group_by", true);
|
|
} else
|
|
group_list.clean();
|
|
} else if (thd->is_fatal_error()) // End of memory
|
|
return true;
|
|
}
|
|
simple_group = false;
|
|
|
|
ORDER *old_group_list = group_list.order;
|
|
group_list = ORDER_with_src(
|
|
remove_const(group_list.order, where_cond,
|
|
rollup_state == RollupState::NONE, &simple_group, true),
|
|
group_list.src, /*const_optimized=*/true);
|
|
if (thd->is_error()) {
|
|
error = 1;
|
|
DBUG_PRINT("error", ("Error from remove_const"));
|
|
return true;
|
|
}
|
|
if (old_group_list && group_list.empty()) select_distinct = false;
|
|
|
|
if (group_list.empty() && grouped) {
|
|
order.clean(); // The output has only one row
|
|
simple_order = true;
|
|
select_distinct = false; // No need in distinct for 1 row
|
|
group_optimized_away = true;
|
|
}
|
|
|
|
calc_group_buffer(this, group_list.order);
|
|
send_group_parts = tmp_table_param.group_parts; /* Save org parts */
|
|
|
|
/*
|
|
If ORDER BY is a prefix of GROUP BY and if windowing or ROLLUP
|
|
doesn't change this order, ORDER BY can be removed and we can
|
|
enforce GROUP BY to provide order.
|
|
Also true if the result is one row.
|
|
*/
|
|
if ((test_if_subpart(group_list.order, order.order) && !m_windows_sort &&
|
|
query_block->olap != ROLLUP_TYPE) ||
|
|
(group_list.empty() && tmp_table_param.sum_func_count)) {
|
|
if (!order.empty()) {
|
|
order.clean();
|
|
trace_opt.add("removed_order_by", true);
|
|
}
|
|
if (is_indexed_agg_distinct(this, nullptr)) streaming_aggregation = false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void JOIN::test_skip_sort() {
|
|
DBUG_TRACE;
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
JOIN_TAB *const tab = best_ref[const_tables];
|
|
|
|
assert(m_ordered_index_usage == ORDERED_INDEX_VOID);
|
|
|
|
if (!group_list.empty()) // GROUP BY honoured first
|
|
// (DISTINCT was rewritten to GROUP BY if skippable)
|
|
{
|
|
/*
|
|
If the SQL_BIG_RESULT option is set on the query block or a JSON
|
|
aggregation function is used, check if it is possible to sort using index
|
|
for GROUP BY and thus avoid materializing the row set to disk, unless:
|
|
|
|
1. A group min-max optimization will be used, or
|
|
2. Some non-aggregated full-text search results must be accessible after
|
|
aggregation.
|
|
*/
|
|
if (!(query_block->active_options() & SELECT_BIG_RESULT || with_json_agg) ||
|
|
(tab->range_scan() &&
|
|
tab->range_scan()->type == AccessPath::GROUP_INDEX_SKIP_SCAN) ||
|
|
contains_non_aggregated_fts()) {
|
|
if (simple_group && // GROUP BY is possibly skippable
|
|
!select_distinct) // .. if not preceded by a DISTINCT
|
|
{
|
|
/*
|
|
Calculate a possible 'limit' of table rows for 'GROUP BY':
|
|
A specified 'LIMIT' is relative to the final resultset.
|
|
'need_tmp' implies that there will be more postprocessing
|
|
so the specified 'limit' should not be enforced yet.
|
|
*/
|
|
const ha_rows limit =
|
|
(need_tmp_before_win ? HA_POS_ERROR : m_select_limit);
|
|
int dummy;
|
|
|
|
if (test_if_skip_sort_order(tab, group_list, limit, false,
|
|
&tab->table()->keys_in_use_for_group_by,
|
|
&dummy)) {
|
|
m_ordered_index_usage = ORDERED_INDEX_GROUP_BY;
|
|
}
|
|
}
|
|
|
|
/*
|
|
If we are going to use semi-join LooseScan, it will depend
|
|
on the selected index scan to be used. If index is not used
|
|
for the GROUP BY, we risk that sorting is put on the LooseScan
|
|
table. In order to avoid this, force use of temporary table.
|
|
TODO: Explain the allow_group_via_temp_table part of the test below.
|
|
*/
|
|
if ((m_ordered_index_usage != ORDERED_INDEX_GROUP_BY) &&
|
|
(tmp_table_param.allow_group_via_temp_table ||
|
|
(tab->emb_sj_nest &&
|
|
tab->position()->sj_strategy == SJ_OPT_LOOSE_SCAN))) {
|
|
need_tmp_before_win = true;
|
|
simple_order = simple_group = false; // Force tmp table without sort
|
|
}
|
|
}
|
|
} else if (!order.empty() && // ORDER BY wo/ preceding GROUP BY
|
|
(simple_order ||
|
|
skip_sort_order) && // which is possibly skippable,
|
|
!m_windows_sort) // and WFs will not shuffle rows
|
|
{
|
|
int dummy;
|
|
if ((skip_sort_order = test_if_skip_sort_order(
|
|
tab, order, m_select_limit, false,
|
|
&tab->table()->keys_in_use_for_order_by, &dummy))) {
|
|
m_ordered_index_usage = ORDERED_INDEX_ORDER_BY;
|
|
/*
|
|
Update plan cost if there is only one table. Multi-table/join scenarios
|
|
are more complex and will not reflect updated costs after access change.
|
|
*/
|
|
if (primary_tables == 1 && tab->table()->s->has_secondary_engine()) {
|
|
best_read = qep_tab->position()->prefix_cost + sort_cost;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Test if ORDER BY is a single MATCH function(ORDER BY MATCH)
|
|
and sort order is descending.
|
|
|
|
@param order pointer to ORDER struct.
|
|
|
|
@retval
|
|
Pointer to MATCH function if order is 'ORDER BY MATCH() DESC'
|
|
@retval
|
|
NULL otherwise
|
|
*/
|
|
|
|
static Item_func_match *test_if_ft_index_order(ORDER *order) {
|
|
if (order && order->next == nullptr && order->direction == ORDER_DESC &&
|
|
is_function_of_type(*order->item, Item_func::FT_FUNC))
|
|
return down_cast<Item_func_match *>(*order->item)->get_master();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
Test if this is a prefix index.
|
|
|
|
@param table table
|
|
@param idx index to check
|
|
|
|
@return TRUE if this is a prefix index
|
|
*/
|
|
bool is_prefix_index(TABLE *table, uint idx) {
|
|
if (!table || !table->key_info) {
|
|
return false;
|
|
}
|
|
|
|
KEY *key_info = table->key_info;
|
|
const uint key_parts = key_info[idx].user_defined_key_parts;
|
|
KEY_PART_INFO *key_part = key_info[idx].key_part;
|
|
|
|
for (uint i = 0; i < key_parts; i++, key_part++) {
|
|
if (key_part->field &&
|
|
!(table->field[key_part->fieldnr - 1]
|
|
->part_of_prefixkey.is_clear_all()) &&
|
|
!(key_info->flags & (HA_FULLTEXT | HA_SPATIAL))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Test if one can use the key to resolve ordering.
|
|
|
|
@param order_src Sort order
|
|
@param table Table to sort
|
|
@param idx Index to check
|
|
@param[out] used_key_parts NULL by default, otherwise return value for
|
|
used key parts.
|
|
@param[out] skip_quick Whether found index can be used for backward range
|
|
scans
|
|
|
|
@note
|
|
used_key_parts is set to correct key parts used if return value != 0
|
|
(On other cases, used_key_part may be changed)
|
|
Note that the value may actually be greater than the number of index
|
|
key parts. This can happen for storage engines that have the primary
|
|
key parts as a suffix for every secondary key.
|
|
|
|
@retval
|
|
1 key is ok.
|
|
@retval
|
|
0 Key can't be used
|
|
@retval
|
|
-1 Reverse key can be used
|
|
*/
|
|
|
|
int test_if_order_by_key(ORDER_with_src *order_src, TABLE *table, uint idx,
|
|
uint *used_key_parts, bool *skip_quick) {
|
|
DBUG_TRACE;
|
|
KEY_PART_INFO *key_part, *key_part_end;
|
|
key_part = table->key_info[idx].key_part;
|
|
key_part_end = key_part + table->key_info[idx].user_defined_key_parts;
|
|
key_part_map const_key_parts = table->const_key_parts[idx];
|
|
int reverse = 0;
|
|
uint key_parts;
|
|
bool on_pk_suffix = false;
|
|
// Whether [extended] key has key parts with mixed ASC/DESC order
|
|
bool mixed_order = false;
|
|
// Order direction of the first key part
|
|
const bool reverse_sorted = (bool)(key_part->key_part_flag & HA_REVERSE_SORT);
|
|
ORDER *order = order_src->order;
|
|
*skip_quick = false;
|
|
|
|
for (; order; order = order->next, const_key_parts >>= 1) {
|
|
/*
|
|
Since only fields can be indexed, ORDER BY <something> that is
|
|
not a field cannot be resolved by using an index.
|
|
*/
|
|
Item *real_itm = (*order->item)->real_item();
|
|
if (real_itm->type() != Item::FIELD_ITEM) return 0;
|
|
|
|
const Field *field = down_cast<const Item_field *>(real_itm)->field;
|
|
|
|
/*
|
|
Skip key parts that are constants in the WHERE clause if these are
|
|
already removed in the ORDER expression by check_field_is_const().
|
|
If they are not removed in the ORDER expression yet, then we skip
|
|
the constant keyparts that are not part of the ORDER expression.
|
|
*/
|
|
for (; const_key_parts & 1 && key_part < key_part_end &&
|
|
(order_src->is_const_optimized() || key_part->field != field);
|
|
const_key_parts >>= 1) {
|
|
key_part++;
|
|
}
|
|
|
|
/* Avoid usage of prefix index for sorting a partition table */
|
|
if (table->part_info && key_part != table->key_info[idx].key_part &&
|
|
key_part != key_part_end && is_prefix_index(table, idx))
|
|
return 0;
|
|
|
|
if (key_part == key_part_end) {
|
|
/*
|
|
We are at the end of the key. Check if the engine has the primary
|
|
key as a suffix to the secondary keys. If it has continue to check
|
|
the primary key as a suffix.
|
|
*/
|
|
if (!on_pk_suffix &&
|
|
(table->file->ha_table_flags() & HA_PRIMARY_KEY_IN_READ_INDEX) &&
|
|
table->s->primary_key != MAX_KEY && table->s->primary_key != idx) {
|
|
on_pk_suffix = true;
|
|
key_part = table->key_info[table->s->primary_key].key_part;
|
|
key_part_end =
|
|
key_part +
|
|
table->key_info[table->s->primary_key].user_defined_key_parts;
|
|
const_key_parts = table->const_key_parts[table->s->primary_key];
|
|
|
|
/*
|
|
Skip key parts that are constants in the WHERE clause if these are
|
|
already removed in the ORDER expression by check_field_is_const().
|
|
If they are not removed in the ORDER expression yet, then we skip
|
|
the constant keyparts that are not part of the ORDER expression.
|
|
*/
|
|
for (; const_key_parts & 1 && key_part < key_part_end &&
|
|
(order_src->is_const_optimized() || key_part->field != field);
|
|
const_key_parts >>= 1) {
|
|
key_part++;
|
|
}
|
|
/*
|
|
The primary and secondary key parts were all const (i.e. there's
|
|
one row). The sorting doesn't matter.
|
|
*/
|
|
if (key_part == key_part_end && reverse == 0) {
|
|
key_parts = 0;
|
|
reverse = 1;
|
|
goto ok;
|
|
}
|
|
} else
|
|
return 0;
|
|
}
|
|
|
|
if (key_part->field != field || !field->part_of_sortkey.is_set(idx))
|
|
return 0;
|
|
if (order->direction != ORDER_NOT_RELEVANT) {
|
|
const enum_order keypart_order =
|
|
(key_part->key_part_flag & HA_REVERSE_SORT) ? ORDER_DESC : ORDER_ASC;
|
|
/* set flag to 1 if we can use read-next on key, else to -1 */
|
|
const int cur_scan_dir = (order->direction == keypart_order) ? 1 : -1;
|
|
if (reverse && cur_scan_dir != reverse) return 0;
|
|
reverse = cur_scan_dir; // Remember if reverse
|
|
}
|
|
mixed_order |=
|
|
(reverse_sorted != (bool)((key_part)->key_part_flag & HA_REVERSE_SORT));
|
|
|
|
key_part++;
|
|
}
|
|
/*
|
|
The index picked here might be used for range scans with multiple ranges.
|
|
This will require tricky reordering in case of ranges would have to be
|
|
scanned backward and index consists of mixed ASC/DESC key parts. Due to that
|
|
backward scans on such indexes are disabled.
|
|
*/
|
|
if (mixed_order && reverse < 0) *skip_quick = true;
|
|
|
|
if (!reverse) {
|
|
/*
|
|
We get here when the key is suitable and we don't care about it's
|
|
order, i.e. GROUP BY/DISTINCT. Use forward scan.
|
|
*/
|
|
reverse = 1;
|
|
}
|
|
if (on_pk_suffix) {
|
|
const uint used_key_parts_secondary =
|
|
table->key_info[idx].user_defined_key_parts;
|
|
const uint used_key_parts_pk =
|
|
(uint)(key_part - table->key_info[table->s->primary_key].key_part);
|
|
key_parts = used_key_parts_pk + used_key_parts_secondary;
|
|
|
|
if (reverse == -1 &&
|
|
(!(table->file->index_flags(idx, used_key_parts_secondary - 1, true) &
|
|
HA_READ_PREV) ||
|
|
!(table->file->index_flags(table->s->primary_key,
|
|
used_key_parts_pk - 1, true) &
|
|
HA_READ_PREV)))
|
|
reverse = 0; // Index can't be used
|
|
} else {
|
|
key_parts = (uint)(key_part - table->key_info[idx].key_part);
|
|
if (reverse == -1 &&
|
|
!(table->file->index_flags(idx, key_parts - 1, true) & HA_READ_PREV))
|
|
reverse = 0; // Index can't be used
|
|
}
|
|
ok:
|
|
if (used_key_parts != nullptr) *used_key_parts = key_parts;
|
|
return reverse;
|
|
}
|
|
|
|
/**
|
|
Find shortest key suitable for full table scan.
|
|
|
|
@param table Table to scan
|
|
@param usable_keys Allowed keys
|
|
|
|
@note
|
|
As far as
|
|
1) clustered primary key entry data set is a set of all record
|
|
fields (key fields and not key fields) and
|
|
2) secondary index entry data is a union of its key fields and
|
|
primary key fields (at least InnoDB and its derivatives don't
|
|
duplicate primary key fields there, even if the primary and
|
|
the secondary keys have a common subset of key fields),
|
|
then secondary index entry data is always a subset of primary key entry.
|
|
Unfortunately, key_info[nr].key_length doesn't show the length
|
|
of key/pointer pair but a sum of key field lengths only, thus
|
|
we can't estimate index IO volume comparing only this key_length
|
|
value of secondary keys and clustered PK.
|
|
So, try secondary keys first, and choose PK only if there are no
|
|
usable secondary covering keys or found best secondary key include
|
|
all table fields (i.e. same as PK):
|
|
|
|
@return
|
|
MAX_KEY no suitable key found
|
|
key index otherwise
|
|
*/
|
|
|
|
uint find_shortest_key(TABLE *table, const Key_map *usable_keys) {
|
|
uint best = MAX_KEY;
|
|
const uint usable_clustered_pk = (table->file->primary_key_is_clustered() &&
|
|
table->s->primary_key != MAX_KEY &&
|
|
usable_keys->is_set(table->s->primary_key))
|
|
? table->s->primary_key
|
|
: MAX_KEY;
|
|
if (!usable_keys->is_clear_all()) {
|
|
uint min_length = (uint)~0;
|
|
for (uint nr = 0; nr < table->s->keys; nr++) {
|
|
if (nr == usable_clustered_pk) continue;
|
|
if (usable_keys->is_set(nr)) {
|
|
/*
|
|
Cannot do full index scan on rtree index. It is not supported by
|
|
Innodb as it's rtree index does not store data, but only the
|
|
minimum bouding box (maybe makes sense only for geometries of
|
|
type POINT). Index scans on rtrees are probabaly not supported
|
|
by other storage engines either.
|
|
A multi-valued key requires unique filter, and won't be the most
|
|
fast option even if it will be the shortest one.
|
|
*/
|
|
const KEY &key_ref = table->key_info[nr];
|
|
assert(!(key_ref.flags & HA_MULTI_VALUED_KEY) &&
|
|
!(key_ref.flags & HA_SPATIAL));
|
|
if (key_ref.key_length < min_length) {
|
|
min_length = key_ref.key_length;
|
|
best = nr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (usable_clustered_pk != MAX_KEY) {
|
|
/*
|
|
If the primary key is clustered and found shorter key covers all table
|
|
fields then primary key scan normally would be faster because amount of
|
|
data to scan is the same but PK is clustered.
|
|
It's safe to compare key parts with table fields since duplicate key
|
|
parts aren't allowed.
|
|
*/
|
|
if (best == MAX_KEY ||
|
|
table->key_info[best].user_defined_key_parts >= table->s->fields)
|
|
best = usable_clustered_pk;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/**
|
|
Test if a second key is the subkey of the first one.
|
|
|
|
@param key_part First key parts
|
|
@param ref_key_part Second key parts
|
|
@param ref_key_part_end Last+1 part of the second key
|
|
|
|
@note
|
|
Second key MUST be shorter than the first one.
|
|
|
|
@retval
|
|
1 is a subkey
|
|
@retval
|
|
0 no sub key
|
|
*/
|
|
|
|
inline bool is_subkey(KEY_PART_INFO *key_part, KEY_PART_INFO *ref_key_part,
|
|
KEY_PART_INFO *ref_key_part_end) {
|
|
for (; ref_key_part < ref_key_part_end; key_part++, ref_key_part++)
|
|
if (!key_part->field->eq(ref_key_part->field)) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
Test if REF_OR_NULL optimization will be used if the specified
|
|
ref_key is used for REF-access to 'tab'
|
|
|
|
@retval
|
|
true JT_REF_OR_NULL will be used
|
|
@retval
|
|
false no JT_REF_OR_NULL access
|
|
*/
|
|
|
|
static bool is_ref_or_null_optimized(const JOIN_TAB *tab, uint ref_key) {
|
|
if (tab->keyuse()) {
|
|
const Key_use *keyuse = tab->keyuse();
|
|
while (keyuse->key != ref_key && keyuse->table_ref == tab->table_ref)
|
|
keyuse++;
|
|
|
|
const table_map const_tables = tab->join()->const_table_map;
|
|
while (keyuse->key == ref_key && keyuse->table_ref == tab->table_ref) {
|
|
if (!(keyuse->used_tables & ~const_tables)) {
|
|
if (keyuse->optimize & KEY_OPTIMIZE_REF_OR_NULL) return true;
|
|
}
|
|
keyuse++;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Test if we can use one of the 'usable_keys' instead of 'ref' key
|
|
for sorting.
|
|
|
|
@param order The query block's order clause.
|
|
@param tab Current JOIN_TAB.
|
|
@param ref Number of key, used for WHERE clause
|
|
@param ref_key_parts Index columns used for ref lookup.
|
|
@param usable_keys Keys for testing
|
|
|
|
@return
|
|
- MAX_KEY If we can't use other key
|
|
- the number of found key Otherwise
|
|
*/
|
|
|
|
static uint test_if_subkey(ORDER_with_src *order, JOIN_TAB *tab, uint ref,
|
|
uint ref_key_parts, const Key_map *usable_keys) {
|
|
uint nr;
|
|
uint min_length = (uint)~0;
|
|
uint best = MAX_KEY;
|
|
TABLE *table = tab->table();
|
|
KEY_PART_INFO *ref_key_part = table->key_info[ref].key_part;
|
|
KEY_PART_INFO *ref_key_part_end = ref_key_part + ref_key_parts;
|
|
|
|
for (nr = 0; nr < table->s->keys; nr++) {
|
|
bool skip_quick;
|
|
if (usable_keys->is_set(nr) &&
|
|
table->key_info[nr].key_length < min_length &&
|
|
table->key_info[nr].user_defined_key_parts >= ref_key_parts &&
|
|
is_subkey(table->key_info[nr].key_part, ref_key_part,
|
|
ref_key_part_end) &&
|
|
!is_ref_or_null_optimized(tab, nr) &&
|
|
test_if_order_by_key(order, table, nr, nullptr, &skip_quick) &&
|
|
!skip_quick) {
|
|
min_length = table->key_info[nr].key_length;
|
|
best = nr;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/**
|
|
It is not obvious to see that test_if_skip_sort_order() never changes the
|
|
plan if no_changes is true. So we double-check: creating an instance of this
|
|
class saves some important access-path-related information of the current
|
|
table; when the instance is destroyed, the latest access-path information is
|
|
compared with saved data.
|
|
*/
|
|
|
|
class Plan_change_watchdog {
|
|
#ifndef NDEBUG
|
|
public:
|
|
/**
|
|
@param tab_arg table whose access path is being determined
|
|
@param no_changes_arg whether a change to the access path is allowed
|
|
*/
|
|
Plan_change_watchdog(const JOIN_TAB *tab_arg, const bool no_changes_arg) {
|
|
if (no_changes_arg) {
|
|
tab = tab_arg;
|
|
type = tab->type();
|
|
if ((quick = tab->range_scan())) quick_index = used_index(quick);
|
|
use_quick = tab->use_quick;
|
|
ref_key = tab->ref().key;
|
|
ref_key_parts = tab->ref().key_parts;
|
|
index = tab->index();
|
|
} else {
|
|
tab = nullptr;
|
|
type = JT_UNKNOWN;
|
|
quick = nullptr;
|
|
ref_key = ref_key_parts = index = 0;
|
|
use_quick = QS_NONE;
|
|
}
|
|
}
|
|
~Plan_change_watchdog() {
|
|
if (tab == nullptr) return;
|
|
// changes are not allowed, we verify:
|
|
assert(tab->type() == type);
|
|
assert(tab->range_scan() == quick);
|
|
assert(quick == nullptr || used_index(tab->range_scan()) == quick_index);
|
|
assert(tab->use_quick == use_quick);
|
|
assert(tab->ref().key == ref_key);
|
|
assert(tab->ref().key_parts == ref_key_parts);
|
|
assert(tab->index() == index);
|
|
}
|
|
|
|
private:
|
|
const JOIN_TAB *tab; ///< table, or NULL if changes are allowed
|
|
enum join_type type; ///< copy of tab->type()
|
|
// "Range / index merge" info
|
|
const AccessPath *quick{nullptr}; ///< copy of tab->select->quick
|
|
uint quick_index{0}; ///< copy of tab->select->quick->index
|
|
enum quick_type use_quick; ///< copy of tab->use_quick
|
|
// "ref access" info
|
|
int ref_key; ///< copy of tab->ref().key
|
|
uint ref_key_parts; /// copy of tab->ref().key_parts
|
|
// Other index-related info
|
|
uint index; ///< copy of tab->index
|
|
#else // in non-debug build, empty class
|
|
public:
|
|
Plan_change_watchdog(const JOIN_TAB *, const bool) {}
|
|
#endif
|
|
};
|
|
|
|
/**
|
|
Test if we can skip ordering by using an index.
|
|
|
|
If the current plan is to use an index that provides ordering, the
|
|
plan will not be changed. Otherwise, if an index can be used, the
|
|
JOIN_TAB / tab->select struct is changed to use the index.
|
|
|
|
The index must cover all fields in @<order@>, or it will not be considered.
|
|
|
|
@param tab NULL or JOIN_TAB of the accessed table
|
|
@param order Linked list of ORDER BY arguments
|
|
@param select_limit LIMIT value, or HA_POS_ERROR if no limit
|
|
@param no_changes No changes will be made to the query plan.
|
|
@param map Key_map of applicable indexes.
|
|
@param [out] order_idx Number of index selected, -1 if no applicable index
|
|
found
|
|
|
|
@todo
|
|
- sergeyp: Results of all index merge selects actually are ordered
|
|
by clustered PK values.
|
|
|
|
@note
|
|
This function may change tmp_table_param.precomputed_group_by. This
|
|
affects how create_tmp_table() treats aggregation functions, so
|
|
count_field_types() must be called again to make sure this is taken
|
|
into consideration.
|
|
|
|
@retval
|
|
0 We have to use filesort to do the sorting
|
|
@retval
|
|
1 We can use an index.
|
|
*/
|
|
|
|
static bool test_if_skip_sort_order(JOIN_TAB *tab, ORDER_with_src &order,
|
|
ha_rows select_limit, const bool no_changes,
|
|
const Key_map *map, int *order_idx) {
|
|
DBUG_TRACE;
|
|
int ref_key;
|
|
uint ref_key_parts = 0;
|
|
int order_direction = 0;
|
|
uint used_key_parts = 0;
|
|
TABLE *const table = tab->table();
|
|
JOIN *const join = tab->join();
|
|
THD *const thd = join->thd;
|
|
AccessPath *const save_range_scan = tab->range_scan();
|
|
int best_key = -1;
|
|
double best_read_time = 0;
|
|
bool set_up_ref_access_to_key = false;
|
|
bool can_skip_sorting = false; // used as return value
|
|
int changed_key = -1;
|
|
|
|
/* Check that we are always called with first non-const table */
|
|
assert((uint)tab->idx() == join->const_tables);
|
|
|
|
const Plan_change_watchdog watchdog(tab, no_changes);
|
|
*order_idx = -1;
|
|
/* Sorting a single row can always be skipped */
|
|
if (tab->type() == JT_EQ_REF || tab->type() == JT_CONST ||
|
|
tab->type() == JT_SYSTEM) {
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
Check if FT index can be used to retrieve result in the required order.
|
|
It is possible if ordering is on the first non-constant table.
|
|
*/
|
|
if (!join->order.empty() && join->simple_order) {
|
|
/*
|
|
Check if ORDER is DESC, ORDER BY is a single MATCH function.
|
|
*/
|
|
Item_func_match *ft_func = test_if_ft_index_order(order.order);
|
|
/*
|
|
Two possible cases when we can skip sort order:
|
|
1. FT_SORTED must be set(Natural mode, no ORDER BY).
|
|
2. If FT_SORTED flag is not set then
|
|
the engine should support deferred sorting. Deferred sorting means
|
|
that sorting is postponed utill the start of index reading(InnoDB).
|
|
In this case we set FT_SORTED flag here to let the engine know that
|
|
internal sorting is needed.
|
|
*/
|
|
if (ft_func && ft_func->ft_handler && ft_func->ordered_result()) {
|
|
/*
|
|
FT index scan is used, so the only additional requirement is
|
|
that ORDER BY MATCH function is the same as the function that
|
|
is used for FT index.
|
|
*/
|
|
if (tab->type() == JT_FT &&
|
|
ft_func->eq(tab->position()->key->val, true)) {
|
|
ft_func->set_hints(join, FT_SORTED, select_limit, false);
|
|
return true;
|
|
}
|
|
/*
|
|
No index is used, it's possible to use FT index for ORDER BY if
|
|
LIMIT is present and does not exceed count of the records in FT index
|
|
and there is no WHERE condition since a condition may potentially
|
|
require more rows to be fetch from FT index.
|
|
*/
|
|
if (!tab->condition() && select_limit != HA_POS_ERROR &&
|
|
select_limit <= ft_func->get_count()) {
|
|
/* test_if_ft_index_order() always returns master MATCH function. */
|
|
assert(!ft_func->master);
|
|
/* ref is not set since there is no WHERE condition */
|
|
assert(tab->ref().key == -1);
|
|
|
|
/*Make EXPLAIN happy */
|
|
tab->set_type(JT_FT);
|
|
tab->ref().key = ft_func->key;
|
|
tab->ref().key_parts = 0;
|
|
tab->set_index(ft_func->key);
|
|
tab->set_ft_func(ft_func);
|
|
|
|
/* Setup FT handler */
|
|
ft_func->set_hints(join, FT_SORTED, select_limit, true);
|
|
ft_func->score_from_index_scan = true;
|
|
table->file->ft_handler = ft_func->ft_handler;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Keys disabled by ALTER TABLE ... DISABLE KEYS should have already
|
|
been taken into account.
|
|
*/
|
|
Key_map usable_keys = *map;
|
|
|
|
for (ORDER *tmp_order = order.order; tmp_order; tmp_order = tmp_order->next) {
|
|
const Item *item = (*tmp_order->item)->real_item();
|
|
if (item->type() != Item::FIELD_ITEM) {
|
|
usable_keys.clear_all();
|
|
return false;
|
|
}
|
|
usable_keys.intersect(
|
|
down_cast<const Item_field *>(item)->field->part_of_sortkey);
|
|
if (usable_keys.is_clear_all()) return false; // No usable keys
|
|
}
|
|
if (tab->type() == JT_REF_OR_NULL || tab->type() == JT_FT) return false;
|
|
|
|
ref_key = -1;
|
|
/* Test if constant range in WHERE */
|
|
if (tab->type() == JT_REF) {
|
|
assert(tab->ref().key >= 0 && tab->ref().key_parts);
|
|
ref_key = tab->ref().key;
|
|
ref_key_parts = tab->ref().key_parts;
|
|
} else if (tab->type() == JT_RANGE || tab->type() == JT_INDEX_MERGE) {
|
|
// Range found by opt_range
|
|
/*
|
|
assume results are not ordered when index merge is used
|
|
TODO: sergeyp: Results of all index merge selects actually are ordered
|
|
by clustered PK values.
|
|
*/
|
|
|
|
if (tab->range_scan()->type == AccessPath::INDEX_MERGE ||
|
|
tab->range_scan()->type == AccessPath::ROWID_UNION ||
|
|
tab->range_scan()->type == AccessPath::ROWID_INTERSECTION)
|
|
return false;
|
|
ref_key = used_index(tab->range_scan());
|
|
ref_key_parts = get_used_key_parts(tab->range_scan());
|
|
} else if (tab->type() == JT_INDEX_SCAN) {
|
|
// The optimizer has decided to use an index scan.
|
|
ref_key = tab->index();
|
|
ref_key_parts = actual_key_parts(&table->key_info[tab->index()]);
|
|
}
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper_1(trace);
|
|
Opt_trace_object trace_skip_sort_order(
|
|
trace, "reconsidering_access_paths_for_index_ordering");
|
|
trace_skip_sort_order.add_alnum(
|
|
"clause", (order.src == ESC_ORDER_BY ? "ORDER BY" : "GROUP BY"));
|
|
Opt_trace_array trace_steps(trace, "steps");
|
|
|
|
if (ref_key >= 0) {
|
|
/*
|
|
We come here when ref/index scan/range scan access has been set
|
|
up for this table. Do not change access method if ordering is
|
|
provided already.
|
|
*/
|
|
if (!usable_keys.is_set(ref_key)) {
|
|
/*
|
|
We come here when ref_key is not among usable_keys, try to find a
|
|
usable prefix key of that key.
|
|
*/
|
|
uint new_ref_key;
|
|
/*
|
|
If using index only read, only consider other possible index only
|
|
keys
|
|
*/
|
|
if (table->covering_keys.is_set(ref_key))
|
|
usable_keys.intersect(table->covering_keys);
|
|
|
|
if ((new_ref_key = test_if_subkey(&order, tab, ref_key, ref_key_parts,
|
|
&usable_keys)) < MAX_KEY) {
|
|
/* Found key that can be used to retrieve data in sorted order */
|
|
if (tab->ref().key >= 0) {
|
|
/*
|
|
We'll use ref access method on key new_ref_key. The actual change
|
|
is done further down in this function where we update the plan.
|
|
*/
|
|
set_up_ref_access_to_key = true;
|
|
} else if (!no_changes) {
|
|
/*
|
|
The range optimizer constructed QUICK_RANGE for ref_key, and
|
|
we want to use instead new_ref_key as the index. We can't
|
|
just change the index of the quick select, because this may
|
|
result in an inconsistent RowIterator object. Below we
|
|
create a new RowIterator from scratch so that all its
|
|
parameres are set correctly by the range optimizer.
|
|
|
|
Note that the range optimizer is NOT called if
|
|
no_changes==true. This reason is that the range optimizer
|
|
cannot find a QUICK that can return ordered result unless
|
|
index access (ref or index scan) is also able to do so
|
|
(which test_if_order_by_key () will tell).
|
|
Admittedly, range access may be much more efficient than
|
|
e.g. index scan, but the only thing that matters when
|
|
no_change==true is the answer to the question: "Is it
|
|
possible to avoid sorting if an index is used to access
|
|
this table?". The answer does not depend on the outcome of
|
|
the range optimizer.
|
|
*/
|
|
Key_map new_ref_key_map; // Force the creation of quick select
|
|
new_ref_key_map.set_bit(new_ref_key); // only for new_ref_key.
|
|
|
|
const Opt_trace_object trace_wrapper_2(trace);
|
|
Opt_trace_object trace_recest(trace, "rows_estimation");
|
|
trace_recest.add_utf8_table(tab->table_ref)
|
|
.add_utf8("index", table->key_info[new_ref_key].name);
|
|
AccessPath *range_scan;
|
|
MEM_ROOT temp_mem_root(key_memory_test_quick_select_exec,
|
|
thd->variables.range_alloc_block_size);
|
|
const bool no_quick =
|
|
test_quick_select(
|
|
thd, thd->mem_root, &temp_mem_root, new_ref_key_map, 0,
|
|
0, // empty table_map
|
|
join->calc_found_rows
|
|
? HA_POS_ERROR
|
|
: join->query_expression()->select_limit_cnt,
|
|
false, // don't force quick range
|
|
order.order->direction, tab->table(),
|
|
tab->skip_records_in_range(),
|
|
// we are after make_join_query_block():
|
|
tab->condition(), &tab->needed_reg, tab->table()->force_index,
|
|
join->query_block, &range_scan) <= 0;
|
|
assert(tab->range_scan() == save_range_scan);
|
|
tab->set_range_scan(range_scan);
|
|
if (no_quick) {
|
|
can_skip_sorting = false;
|
|
goto fix_ICP;
|
|
}
|
|
}
|
|
ref_key = new_ref_key;
|
|
changed_key = new_ref_key;
|
|
}
|
|
}
|
|
bool dummy;
|
|
/* Check if we get the rows in requested sorted order by using the key */
|
|
if (usable_keys.is_set(ref_key))
|
|
// Last parameter can be ignored as it'll be checked later, if needed
|
|
order_direction =
|
|
test_if_order_by_key(&order, table, ref_key, &used_key_parts, &dummy);
|
|
}
|
|
if (ref_key < 0 || order_direction <= 0) {
|
|
/*
|
|
There is no ref/index scan/range scan access set up for this
|
|
table, or it does not provide the requested ordering, or it uses
|
|
backward scan. Do a cost-based search on all keys.
|
|
*/
|
|
uint best_key_parts = 0;
|
|
uint saved_best_key_parts = 0;
|
|
int best_key_direction = 0;
|
|
const ha_rows table_records = table->file->stats.records;
|
|
|
|
/*
|
|
If an index scan that cannot provide ordering has been selected
|
|
then do not use the index scan key as starting hint to
|
|
test_if_cheaper_ordering()
|
|
*/
|
|
const int ref_key_hint =
|
|
(order_direction == 0 && tab->type() == JT_INDEX_SCAN) ? -1 : ref_key;
|
|
|
|
// Does the query have a "FORCE INDEX [FOR GROUP BY] (idx)" (if clause is
|
|
// group by) or a "FORCE INDEX [FOR ORDER BY] (idx)" (if clause is order
|
|
// by)?
|
|
const bool is_group_by =
|
|
join && join->grouped && order.order == join->group_list.order;
|
|
const bool is_force_index =
|
|
table->force_index ||
|
|
(is_group_by ? table->force_index_group : table->force_index_order);
|
|
|
|
// We try to find an ordering_index alternative over the chosen plan, if:
|
|
// 1. "prefer_ordering_index" switch is on or
|
|
// 2. Force index for order/group is specified or
|
|
// 3. Optimizer has chosen to do table scan currently.
|
|
if (thd->optimizer_switch_flag(OPTIMIZER_SWITCH_PREFER_ORDERING_INDEX) ||
|
|
is_force_index || ref_key == -1)
|
|
test_if_cheaper_ordering(tab, &order, table, usable_keys, ref_key_hint,
|
|
select_limit, &best_key, &best_key_direction,
|
|
&select_limit, &best_key_parts,
|
|
&saved_best_key_parts, &best_read_time);
|
|
|
|
// Try backward scan for previously found key
|
|
if (best_key < 0 && order_direction < 0) goto check_reverse_order;
|
|
|
|
if (best_key < 0) {
|
|
// No usable key has been found
|
|
can_skip_sorting = false;
|
|
goto fix_ICP;
|
|
}
|
|
/*
|
|
If found index will use backward index scan, ref_key uses backward
|
|
range/ref, pick the latter as it has better selectivity.
|
|
*/
|
|
if (order_direction < 0 && order_direction == best_key_direction) {
|
|
best_key = -1; // reset found best key
|
|
goto check_reverse_order;
|
|
}
|
|
|
|
/*
|
|
filesort() and join cache are usually faster than reading in
|
|
index order and not using join cache. Don't use index scan
|
|
unless:
|
|
- the user specified FORCE INDEX [FOR {GROUP|ORDER} BY] (have to assume
|
|
the user knows what's best)
|
|
- the chosen index is clustered primary key (table scan is not cheaper)
|
|
*/
|
|
if (!is_force_index && (select_limit >= table_records) &&
|
|
(tab->type() == JT_ALL &&
|
|
join->primary_tables > join->const_tables + 1) &&
|
|
((unsigned)best_key != table->s->primary_key ||
|
|
!table->file->primary_key_is_clustered())) {
|
|
can_skip_sorting = false;
|
|
goto fix_ICP;
|
|
}
|
|
|
|
if (table->quick_keys.is_set(best_key) &&
|
|
!tab->quick_order_tested.is_set(best_key) && best_key != ref_key) {
|
|
tab->quick_order_tested.set_bit(best_key);
|
|
const Opt_trace_object trace_wrapper_3(trace);
|
|
Opt_trace_object trace_recest(trace, "rows_estimation");
|
|
trace_recest.add_utf8_table(tab->table_ref)
|
|
.add_utf8("index", table->key_info[best_key].name);
|
|
|
|
Key_map keys_to_use; // Force the creation of quick select
|
|
keys_to_use.set_bit(best_key); // only best_key.
|
|
MEM_ROOT temp_mem_root(key_memory_test_quick_select_exec,
|
|
thd->variables.range_alloc_block_size);
|
|
AccessPath *range_scan;
|
|
test_quick_select(
|
|
thd, thd->mem_root, &temp_mem_root, keys_to_use, 0,
|
|
0, // empty table_map
|
|
join->calc_found_rows ? HA_POS_ERROR
|
|
: join->query_expression()->select_limit_cnt,
|
|
true, // force quick range
|
|
order.order->direction, tab->table(), tab->skip_records_in_range(),
|
|
tab->condition(), &tab->needed_reg, tab->table()->force_index,
|
|
join->query_block, &range_scan);
|
|
if (order_direction < 0 && tab->range_scan() != nullptr &&
|
|
tab->range_scan() != save_range_scan) {
|
|
/*
|
|
We came here in case when 3 indexes are available for quick
|
|
select:
|
|
1 - found by join order optimizer (greedy search) and saved in
|
|
save_range_scan
|
|
2 - constructed far above, as better suited for order by, but it was
|
|
found that it requires backward scan.
|
|
3 - constructed right above
|
|
In this case we drop quick #2 as #3 is expected to be better.
|
|
*/
|
|
::destroy_at(tab->range_scan());
|
|
tab->set_range_scan(nullptr);
|
|
}
|
|
/*
|
|
If tab->range_scan() pointed to another quick than save_range_scan, we
|
|
would lose access to it and leak memory.
|
|
*/
|
|
assert(tab->range_scan() == save_range_scan ||
|
|
tab->range_scan() == nullptr);
|
|
tab->set_range_scan(range_scan);
|
|
}
|
|
order_direction = best_key_direction;
|
|
/*
|
|
saved_best_key_parts is actual number of used keyparts found by the
|
|
test_if_order_by_key function. It could differ from keyinfo->key_parts,
|
|
thus we have to restore it in case of desc order as it affects
|
|
ReverseIndexRangeScanIterator behaviour.
|
|
*/
|
|
used_key_parts =
|
|
(order_direction == -1) ? saved_best_key_parts : best_key_parts;
|
|
changed_key = best_key;
|
|
// We will use index scan or range scan:
|
|
set_up_ref_access_to_key = false;
|
|
}
|
|
|
|
check_reverse_order:
|
|
assert(order_direction != 0);
|
|
|
|
if (order_direction == -1) // If ORDER BY ... DESC
|
|
{
|
|
if (tab->range_scan()) {
|
|
/*
|
|
Don't reverse the sort order, if it's already done.
|
|
(In some cases test_if_order_by_key() can be called multiple times
|
|
*/
|
|
if (is_reverse_sorted_range(tab->range_scan())) {
|
|
can_skip_sorting = true;
|
|
goto fix_ICP;
|
|
}
|
|
// test_if_cheaper_ordering() might disable range scan on current index
|
|
if (tab->table()->quick_keys.is_set(used_index(tab->range_scan())) &&
|
|
reverse_sort_possible(tab->range_scan()))
|
|
can_skip_sorting = true;
|
|
else {
|
|
can_skip_sorting = false;
|
|
goto fix_ICP;
|
|
}
|
|
} else {
|
|
// Other index access (ref or scan) poses no problem
|
|
can_skip_sorting = true;
|
|
}
|
|
} else {
|
|
// ORDER BY ASC poses no problem
|
|
can_skip_sorting = true;
|
|
}
|
|
|
|
assert(can_skip_sorting);
|
|
|
|
/*
|
|
Update query plan with access pattern for doing
|
|
ordered access according to what we have decided
|
|
above.
|
|
*/
|
|
if (!no_changes) // We are allowed to update QEP
|
|
{
|
|
if (set_up_ref_access_to_key) {
|
|
/*
|
|
We'll use ref access method on key changed_key. In general case
|
|
the index search tuple for changed_ref_key will be different (e.g.
|
|
when one index is defined as (part1, part2, ...) and another as
|
|
(part1, part2(N), ...) and the WHERE clause contains
|
|
"part1 = const1 AND part2=const2".
|
|
So we build tab->ref() from scratch here.
|
|
*/
|
|
Key_use *keyuse = tab->keyuse();
|
|
while (keyuse->key != (uint)changed_key &&
|
|
keyuse->table_ref == tab->table_ref)
|
|
keyuse++;
|
|
|
|
if (create_ref_for_key(join, tab, keyuse, tab->prefix_tables())) {
|
|
can_skip_sorting = false;
|
|
goto fix_ICP;
|
|
}
|
|
|
|
assert(tab->type() != JT_REF_OR_NULL && tab->type() != JT_FT);
|
|
|
|
// Changing the key makes filter_effect obsolete
|
|
tab->position()->filter_effect = COND_FILTER_STALE;
|
|
|
|
/*
|
|
Check if it is possible to shift from ref to range. The index chosen
|
|
for 'ref' has changed since the last time this function was called.
|
|
*/
|
|
if (can_switch_from_ref_to_range(thd, tab, order.order->direction,
|
|
true)) {
|
|
// Allow the code to fall-through to the next if condition.
|
|
set_up_ref_access_to_key = false;
|
|
best_key = changed_key;
|
|
}
|
|
}
|
|
if (!set_up_ref_access_to_key && best_key >= 0) {
|
|
// Cancel any ref-access previously set up
|
|
tab->ref().key = -1;
|
|
tab->ref().key_parts = 0;
|
|
|
|
/*
|
|
If ref_key used index tree reading only ('Using index' in EXPLAIN),
|
|
and best_key doesn't, then revert the decision.
|
|
*/
|
|
if (!table->covering_keys.is_set(best_key)) table->set_keyread(false);
|
|
// Create an index scan if the table is not a temporary table that uses
|
|
// Temptable engine (Does not support index_first() and index_last()) and
|
|
// if there was no new range scan created.
|
|
if (!(is_temporary_table(tab->table_ref) &&
|
|
tab->table_ref->table->s->db_type() == temptable_hton) &&
|
|
((!tab->range_scan() || tab->range_scan() == save_range_scan))) {
|
|
// Avoid memory leak:
|
|
assert(tab->range_scan() == save_range_scan ||
|
|
tab->range_scan() == nullptr);
|
|
tab->set_range_scan(nullptr);
|
|
tab->set_index(best_key);
|
|
tab->set_type(JT_INDEX_SCAN); // Read with index_first(), index_next()
|
|
/*
|
|
There is a bug. When we change here, e.g. from group_min_max to
|
|
index scan: loose index scan expected to read a small number of rows
|
|
(jumping through the index), this small number was in
|
|
position()->rows_fetched; index scan will read much more, so
|
|
rows_fetched should be updated. So should the filtering effect.
|
|
It is visible in main.distinct in trunk:
|
|
explain SELECT distinct a from t3 order by a desc limit 2;
|
|
id select_type table partitions type
|
|
possible_keys key key_len ref rows filtered Extra 1
|
|
SIMPLE t3 NULL index a a 5 NULL
|
|
40 25.00 Using index "rows=40" should be ~200 i.e. # of records
|
|
in table. Filter should be 100.00 (no WHERE).
|
|
*/
|
|
table->file->ha_index_or_rnd_end();
|
|
tab->position()->filter_effect = COND_FILTER_STALE;
|
|
} else if (tab->type() != JT_ALL) {
|
|
/*
|
|
We're about to use a quick access to the table.
|
|
We need to change the access method so as the quick access
|
|
method is actually used.
|
|
*/
|
|
assert(tab->range_scan());
|
|
assert(used_index(tab->range_scan()) == (uint)best_key);
|
|
tab->set_type(calc_join_type(tab->range_scan()));
|
|
tab->use_quick = QS_RANGE;
|
|
if (is_loose_index_scan(tab->range_scan()))
|
|
join->tmp_table_param.precomputed_group_by = true;
|
|
tab->position()->filter_effect = COND_FILTER_STALE;
|
|
}
|
|
} // best_key >= 0
|
|
|
|
if (order_direction == -1) // If ORDER BY ... DESC
|
|
{
|
|
if (tab->range_scan()) {
|
|
/* ORDER BY range_key DESC */
|
|
if (make_reverse(used_key_parts, tab->range_scan())) {
|
|
/* purecov: begin inspected */
|
|
can_skip_sorting = false; // Reverse sort failed -> filesort
|
|
goto fix_ICP;
|
|
/* purecov: end */
|
|
}
|
|
tab->set_type(calc_join_type(tab->range_scan()));
|
|
tab->position()->filter_effect = COND_FILTER_STALE;
|
|
} else if (tab->type() == JT_REF &&
|
|
tab->ref().key_parts <= used_key_parts) {
|
|
/*
|
|
SELECT * FROM t1 WHERE a=1 ORDER BY a DESC,b DESC
|
|
|
|
Use a traversal function that starts by reading the last row
|
|
with key part (A) and then traverse the index backwards.
|
|
*/
|
|
tab->reversed_access = true;
|
|
|
|
/*
|
|
The current implementation of the reverse RefIterator does not
|
|
work well in combination with ICP and can lead to increased
|
|
execution time. Setting changed_key to the current key
|
|
(based on that we change the access order for the key) will
|
|
ensure that a pushed index condition will be cancelled.
|
|
*/
|
|
changed_key = tab->ref().key;
|
|
} else if (tab->type() == JT_INDEX_SCAN)
|
|
tab->reversed_access = true;
|
|
} else if (tab->range_scan())
|
|
set_need_sorted_output(tab->range_scan());
|
|
|
|
} // QEP has been modified
|
|
|
|
fix_ICP:
|
|
/*
|
|
Cleanup:
|
|
We may have both a 'tab->range_scan()' and 'save_range_scan' (original)
|
|
at this point. Delete the one that we won't use.
|
|
*/
|
|
if (can_skip_sorting && !no_changes) {
|
|
if (tab->type() == JT_INDEX_SCAN &&
|
|
select_limit < table->file->stats.records) {
|
|
assert(select_limit > 0);
|
|
tab->position()->rows_fetched = select_limit;
|
|
/*
|
|
Update the cost data if secondary engine is active as it is needed to
|
|
make the query offload decision later.
|
|
*/
|
|
if (best_read_time > 0 && join->primary_tables == 1 &&
|
|
table->s->has_secondary_engine()) {
|
|
tab->position()->read_cost = best_read_time;
|
|
/*
|
|
Assume no filter at this point to calculate the access cost. This
|
|
will be updated later to proper values when/if filter_effect is
|
|
updated. The logic is to ensure the cost covers accessing at least
|
|
LIMIT number of rows using the access method. If there exists a WHERE
|
|
clause, then more than LIMIT number of rows needs to be accessed.
|
|
Ideally we should calculate proper filtering effect and update
|
|
rows_fetched to include the filtering effect as well. Eg:
|
|
tab->position()->rows_fetched = select_limit / filter_effect;
|
|
*/
|
|
tab->position()->filter_effect = COND_FILTER_ALLPASS;
|
|
// Update the cost values accordingly.
|
|
tab->position()->set_prefix_join_cost(tab->idx(), join->cost_model());
|
|
}
|
|
// Update filter effect to reflect the access change.
|
|
tab->position()->filter_effect = COND_FILTER_STALE_NO_CONST;
|
|
}
|
|
|
|
// Keep current (ordered) tab->range_scan()
|
|
if (save_range_scan != nullptr && save_range_scan != tab->range_scan())
|
|
::destroy_at(save_range_scan);
|
|
} else {
|
|
// Restore original save_range_scan
|
|
if (tab->range_scan() != save_range_scan) {
|
|
if (tab->range_scan() != nullptr) ::destroy_at(tab->range_scan());
|
|
tab->set_range_scan(save_range_scan);
|
|
}
|
|
}
|
|
|
|
trace_steps.end();
|
|
Opt_trace_object trace_change_index(trace, "index_order_summary");
|
|
trace_change_index.add_utf8_table(tab->table_ref)
|
|
.add("index_provides_order", can_skip_sorting)
|
|
.add_alnum("order_direction",
|
|
order_direction == 1
|
|
? "asc"
|
|
: ((order_direction == -1) ? "desc" : "undefined"));
|
|
|
|
if (changed_key >= 0) {
|
|
// switching to another index
|
|
// Should be no pushed index conditions at this point
|
|
assert(!table->file->pushed_idx_cond);
|
|
if (unlikely(trace->is_started())) {
|
|
trace_change_index.add_utf8("index", table->key_info[changed_key].name);
|
|
trace_change_index.add("plan_changed", !no_changes);
|
|
if (!no_changes)
|
|
trace_change_index.add_alnum("access_type", join_type_str[tab->type()]);
|
|
}
|
|
} else if (unlikely(trace->is_started())) {
|
|
trace_change_index.add_utf8(
|
|
"index", ref_key >= 0 ? table->key_info[ref_key].name : "unknown");
|
|
trace_change_index.add("plan_changed", false);
|
|
}
|
|
*order_idx = best_key < 0 ? ref_key : best_key;
|
|
return can_skip_sorting;
|
|
}
|
|
|
|
/**
|
|
Prune partitions for all tables of a join (query block).
|
|
|
|
Requires that tables have been locked.
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
bool JOIN::prune_table_partitions() {
|
|
assert(query_block->partitioned_table_count);
|
|
|
|
for (Table_ref *tbl = query_block->leaf_tables; tbl; tbl = tbl->next_leaf) {
|
|
// This will try to prune non-static conditions, which can be probed after
|
|
// the tables are locked.
|
|
|
|
// Predicates for pruning of this table must be placed in the outer-most
|
|
// join nest (Predicates in other join nests, or in the WHERE clause,
|
|
// would have caused an outer join to be converted to an inner join,
|
|
// and thus there would be no join nest graph to traverse)
|
|
// Look up the join nest hierarchy for the outermost condition:
|
|
Item *cond = where_cond;
|
|
const table_map tbl_map = tbl->map();
|
|
for (Table_ref *nest = tbl; nest != nullptr; nest = nest->embedding) {
|
|
if (nest->join_cond_optim() != nullptr &&
|
|
Overlaps(tbl_map, nest->join_cond_optim()->used_tables())) {
|
|
cond = nest->join_cond_optim();
|
|
// For an anti-join operation, a synthetic left join nest is added above
|
|
// the anti-join nest. Make sure that we skip this when searching for
|
|
// the predicate to prune.
|
|
if (nest->is_aj_nest()) break;
|
|
}
|
|
}
|
|
if (prune_partitions(thd, tbl->table, query_block, cond)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
A helper function to check whether it's better to use range than ref.
|
|
|
|
@details
|
|
Heuristic: Switch from 'ref' to 'range' access if 'range'
|
|
access can utilize more keyparts than 'ref' access. Conditions
|
|
for doing switching:
|
|
|
|
1) Range access is possible
|
|
2) 'ref' access and 'range' access uses the same index
|
|
3) Used parts of key shouldn't have nullable parts & ref_or_null isn't used.
|
|
4) 'ref' access depends on a constant, not a value read from a
|
|
table earlier in the join sequence.
|
|
|
|
Rationale: if 'ref' depends on a value from another table,
|
|
the join condition is not used to limit the rows read by
|
|
'range' access (that would require dynamic range - 'Range
|
|
checked for each record'). In other words, if 'ref' depends
|
|
on a value from another table, we have a query with
|
|
conditions of the form
|
|
|
|
this_table.idx_col1 = other_table.col AND <<- used by 'ref'
|
|
this_table.idx_col1 OP @<const@> AND <<- used by 'range'
|
|
this_table.idx_col2 OP @<const@> AND ... <<- used by 'range'
|
|
|
|
and an index on (idx_col1,idx_col2,...). But the fact that
|
|
'range' access uses more keyparts does not mean that it is
|
|
more selective than 'ref' access because these access types
|
|
utilize different parts of the query condition. We
|
|
therefore trust the cost based choice made by
|
|
best_access_path() instead of forcing a heuristic choice
|
|
here.
|
|
5) 'range' access uses more keyparts than 'ref' access
|
|
6) ORDER BY might make range better than table scan:
|
|
Check possibility of range scan even if it was previously deemed unviable
|
|
(for example when table scan was estimated to be cheaper). If yes,
|
|
range-access should be chosen only for larger key length.
|
|
|
|
@param thd To re-run range optimizer.
|
|
@param tab JOIN_TAB to check
|
|
@param ordering Used as a parameter to call test_quick_select.
|
|
@param recheck_range Check possibility of range scan even if it is currently
|
|
unviable.
|
|
|
|
@return true Range is better than ref
|
|
@return false Ref is better or switch isn't possible
|
|
|
|
@todo: This decision should rather be made in best_access_path()
|
|
*/
|
|
|
|
static bool can_switch_from_ref_to_range(THD *thd, JOIN_TAB *tab,
|
|
enum_order ordering,
|
|
bool recheck_range) {
|
|
if ((tab->range_scan() && // 1)
|
|
tab->position()->key->key == used_index(tab->range_scan())) || // 2)
|
|
recheck_range) {
|
|
uint keyparts = 0, length = 0;
|
|
table_map dep_map = 0;
|
|
bool maybe_null = false;
|
|
|
|
calc_length_and_keyparts(tab->position()->key, tab,
|
|
tab->position()->key->key, tab->prefix_tables(),
|
|
nullptr, &length, &keyparts, &dep_map,
|
|
&maybe_null);
|
|
|
|
if (thd->is_error()) {
|
|
return true;
|
|
}
|
|
if (!maybe_null && // 3)
|
|
!dep_map) // 4)
|
|
{
|
|
if (recheck_range) // 6)
|
|
{
|
|
Key_map new_ref_key_map;
|
|
new_ref_key_map.set_bit(tab->ref().key);
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
const Opt_trace_object can_switch(
|
|
trace, "check_if_range_uses_more_keyparts_than_ref");
|
|
const Opt_trace_object trace_cond(
|
|
trace, "rerunning_range_optimizer_for_single_index");
|
|
|
|
AccessPath *range_scan;
|
|
MEM_ROOT temp_mem_root(key_memory_test_quick_select_exec,
|
|
thd->variables.range_alloc_block_size);
|
|
if (test_quick_select(
|
|
thd, thd->mem_root, &temp_mem_root, new_ref_key_map, 0,
|
|
0, // empty table_map
|
|
tab->join()->row_limit, false, ordering, tab->table(),
|
|
tab->skip_records_in_range(),
|
|
tab->join_cond() ? tab->join_cond() : tab->join()->where_cond,
|
|
&tab->needed_reg, recheck_range, tab->join()->query_block,
|
|
&range_scan) > 0) {
|
|
if (length < get_max_used_key_length(range_scan)) {
|
|
if (tab->range_scan() != nullptr) ::destroy_at(tab->range_scan());
|
|
tab->set_range_scan(range_scan);
|
|
return true;
|
|
}
|
|
Opt_trace_object(trace, "access_type_unchanged")
|
|
.add("ref_key_length", length)
|
|
.add("range_key_length", get_max_used_key_length(range_scan));
|
|
::destroy_at(range_scan);
|
|
}
|
|
} else
|
|
return length < get_max_used_key_length(tab->range_scan()); // 5)
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
An utility function - apply heuristics and optimize access methods to tables.
|
|
Currently this function can change REF to RANGE and ALL to INDEX scan if
|
|
latter is considered to be better (not cost-based) than the former.
|
|
@note Side effect - this function could set 'Impossible WHERE' zero
|
|
result.
|
|
*/
|
|
|
|
void JOIN::adjust_access_methods() {
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
for (uint i = const_tables; i < tables; i++) {
|
|
JOIN_TAB *const tab = best_ref[i];
|
|
Table_ref *const tl = tab->table_ref;
|
|
|
|
if (tab->type() == JT_ALL) {
|
|
/*
|
|
It's possible to speedup query by switching from full table scan to
|
|
the scan of covering index, due to less data being read.
|
|
Prerequisites for this are:
|
|
1) Keyread (i.e index only scan) is allowed (table isn't updated/deleted
|
|
from)
|
|
2) Covering indexes are available
|
|
3) This isn't a derived table/materialized view
|
|
*/
|
|
if (!tab->table()->no_keyread && // 1
|
|
!tab->table()->covering_keys.is_clear_all() && // 2
|
|
!tl->uses_materialization()) // 3
|
|
{
|
|
/*
|
|
It has turned out that the change commented out below, while speeding
|
|
things up for disk-bound loads, slows them down for cases when the data
|
|
is in disk cache (see BUG#35850):
|
|
// See bug #26447: "Using the clustered index for a table scan
|
|
// is always faster than using a secondary index".
|
|
if (table->s->primary_key != MAX_KEY &&
|
|
table->file->primary_key_is_clustered())
|
|
tab->index= table->s->primary_key;
|
|
else
|
|
tab->index=find_shortest_key(table, & table->covering_keys);
|
|
*/
|
|
if (tab->position()->sj_strategy != SJ_OPT_LOOSE_SCAN)
|
|
tab->set_index(
|
|
find_shortest_key(tab->table(), &tab->table()->covering_keys));
|
|
tab->set_type(JT_INDEX_SCAN); // Read with index_first / index_next
|
|
// From table scan to index scan, thus filter effect needs no recalc.
|
|
}
|
|
} else if (tab->type() == JT_REF) {
|
|
if (can_switch_from_ref_to_range(thd, tab, ORDER_NOT_RELEVANT, false)) {
|
|
tab->set_type(JT_RANGE);
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object wrapper(trace);
|
|
Opt_trace_object(trace, "access_type_changed")
|
|
.add_utf8_table(tl)
|
|
.add_utf8("index",
|
|
tab->table()->key_info[tab->position()->key->key].name)
|
|
.add_alnum("old_type", "ref")
|
|
.add_alnum("new_type", join_type_str[tab->type()])
|
|
.add_alnum("cause", "uses_more_keyparts");
|
|
|
|
tab->use_quick = QS_RANGE;
|
|
tab->position()->filter_effect = COND_FILTER_STALE;
|
|
} else {
|
|
// Cleanup quick, REF/REF_OR_NULL/EQ_REF, will be clarified later
|
|
if (tab->range_scan() != nullptr) {
|
|
::destroy_at(tab->range_scan());
|
|
tab->set_range_scan(nullptr);
|
|
}
|
|
}
|
|
}
|
|
// Ensure AM consistency
|
|
assert(!(tab->range_scan() &&
|
|
(tab->type() == JT_REF || tab->type() == JT_ALL)));
|
|
assert((tab->type() != JT_RANGE && tab->type() != JT_INDEX_MERGE) ||
|
|
tab->range_scan());
|
|
if (!tab->const_keys.is_clear_all() &&
|
|
tab->table()->reginfo.impossible_range &&
|
|
((i == const_tables && tab->type() == JT_REF) ||
|
|
((tab->type() == JT_ALL || tab->type() == JT_RANGE ||
|
|
tab->type() == JT_INDEX_MERGE || tab->type() == JT_INDEX_SCAN) &&
|
|
tab->use_quick != QS_RANGE)) &&
|
|
!tab->table_ref->is_inner_table_of_outer_join())
|
|
zero_result_cause = "Impossible WHERE noticed after reading const tables";
|
|
}
|
|
}
|
|
|
|
static JOIN_TAB *alloc_jtab_array(THD *thd, uint table_count) {
|
|
JOIN_TAB *t = new (thd->mem_root) JOIN_TAB[table_count];
|
|
if (!t) return nullptr; /* purecov: inspected */
|
|
|
|
QEP_shared *qs = new (thd->mem_root) QEP_shared[table_count];
|
|
if (!qs) return nullptr; /* purecov: inspected */
|
|
|
|
for (uint i = 0; i < table_count; ++i) t[i].set_qs(qs++);
|
|
|
|
return t;
|
|
}
|
|
|
|
/**
|
|
Set up JOIN_TAB structs according to the picked join order in best_positions.
|
|
This allocates execution structures so may be called only after we have the
|
|
very final plan. It must be called after
|
|
Optimize_table_order::fix_semijoin_strategies().
|
|
|
|
@return False if success, True if error
|
|
|
|
@details
|
|
- create join->join_tab array and copy from existing JOIN_TABs in join order
|
|
- create helper structs for materialized semi-join handling
|
|
- finalize semi-join strategy choices
|
|
- Number of intermediate tables "tmp_tables" is calculated.
|
|
- "tables" and "primary_tables" are recalculated.
|
|
- for full and index scans info of estimated # of records is updated.
|
|
- in a helper function:
|
|
- all heuristics are applied and the final access method type is picked
|
|
for each join_tab (only test_if_skip_sort_order() could override it)
|
|
- AM consistency is ensured (e.g only range and index merge are allowed
|
|
to have quick select set).
|
|
- if "Impossible WHERE" is detected - appropriate zero_result_cause is
|
|
set.
|
|
|
|
Notice that intermediate tables will not have a POSITION reference; and they
|
|
will not have a TABLE reference before the final stages of code generation.
|
|
|
|
@todo the block which sets tab->type should move to adjust_access_methods
|
|
for unification.
|
|
*/
|
|
|
|
bool JOIN::get_best_combination() {
|
|
DBUG_TRACE;
|
|
|
|
// At this point "tables" and "primary"tables" represent the same:
|
|
assert(tables == primary_tables);
|
|
|
|
/*
|
|
Allocate additional space for tmp tables.
|
|
Number of plan nodes:
|
|
# of regular input tables (including semi-joined ones) +
|
|
# of semi-join nests for materialization +
|
|
1? + // For GROUP BY (or implicit grouping when we have windowing)
|
|
1? + // For DISTINCT
|
|
1? + // For aggregation functions aggregated in outer query
|
|
// when used with distinct
|
|
1? + // For ORDER BY
|
|
1? + // buffer result
|
|
# of windows +
|
|
1? // The presence of windows may increase need for grouping tmp tables,
|
|
// cf. de-optimization in make_tmp_tables_info, but there are other
|
|
// cases where we may need another tmp table as well (ROLLUP, full
|
|
// text search). For details, see computation of
|
|
// JOIN::need_tmp_before_win.
|
|
|
|
Up to 2 tmp tables + N window output tmp are allocated (NOTE: windows also
|
|
have frame buffer tmp tables, but those are not relevant here).
|
|
*/
|
|
uint num_tmp_tables =
|
|
(!group_list.empty() || (implicit_grouping && m_windows.elements > 0)
|
|
? 1
|
|
: 0) +
|
|
(select_distinct ? (tmp_table_param.outer_sum_func_count ? 2 : 1) : 0) +
|
|
(order.empty() ? 0 : 1) +
|
|
(query_block->active_options() &
|
|
(SELECT_BIG_RESULT | OPTION_BUFFER_RESULT)
|
|
? 1
|
|
: 0) +
|
|
m_windows.elements + 1;
|
|
|
|
if (num_tmp_tables > (2 + m_windows.elements))
|
|
num_tmp_tables = 2 + m_windows.elements;
|
|
|
|
/*
|
|
Rearrange queries with materialized semi-join nests so that the semi-join
|
|
nest is replaced with a reference to a materialized temporary table and all
|
|
materialized subquery tables are placed after the intermediate tables.
|
|
After the following loop, "inner_target" is the position of the first
|
|
subquery table (if any). "outer_target" is the position of first outer
|
|
table, and will later be used to track the position of any materialized
|
|
temporary tables.
|
|
*/
|
|
const bool has_semijoin = !query_block->sj_nests.empty();
|
|
uint outer_target = 0;
|
|
uint inner_target = primary_tables + num_tmp_tables;
|
|
uint sjm_nests = 0;
|
|
|
|
if (has_semijoin) {
|
|
for (uint tableno = 0; tableno < primary_tables;) {
|
|
if (sj_is_materialize_strategy(best_positions[tableno].sj_strategy)) {
|
|
sjm_nests++;
|
|
inner_target -= (best_positions[tableno].n_sj_tables - 1);
|
|
tableno += best_positions[tableno].n_sj_tables;
|
|
} else
|
|
tableno++;
|
|
}
|
|
}
|
|
|
|
JOIN_TAB *tmp_join_tabs = nullptr;
|
|
if (sjm_nests + num_tmp_tables) {
|
|
// join_tab array only has "primary_tables" tables. We need those more:
|
|
if (!(tmp_join_tabs = alloc_jtab_array(thd, sjm_nests + num_tmp_tables)))
|
|
return true; /* purecov: inspected */
|
|
}
|
|
|
|
// To check that we fill the array correctly: fill it with zeros first
|
|
memset(best_ref, 0,
|
|
sizeof(JOIN_TAB *) * (primary_tables + sjm_nests + num_tmp_tables));
|
|
|
|
int sjm_index = tables; // Number assigned to materialized temporary table
|
|
int remaining_sjm_inner = 0;
|
|
bool err = false;
|
|
for (uint tableno = 0; tableno < tables; tableno++) {
|
|
POSITION *const pos = best_positions + tableno;
|
|
if (has_semijoin && sj_is_materialize_strategy(pos->sj_strategy)) {
|
|
assert(outer_target < inner_target);
|
|
|
|
Table_ref *const sj_nest = pos->table->emb_sj_nest;
|
|
|
|
// Handle this many inner tables of materialized semi-join
|
|
remaining_sjm_inner = pos->n_sj_tables;
|
|
|
|
/*
|
|
If we fail in some allocation below, we cannot bail out immediately;
|
|
that would put us in a difficult situation to clean up; imagine we
|
|
have planned this layout:
|
|
outer1 - sj_mat_tmp1 - outer2 - sj_mat_tmp2 - outer3
|
|
We have successfully filled a JOIN_TAB for sj_mat_tmp1, and are
|
|
failing to fill a JOIN_TAB for sj_mat_tmp2 (OOM). So we want to quit
|
|
this function, which will lead to cleanup functions.
|
|
But sj_mat_tmp1 is in this->best_ref only, outer3 is in this->join_tab
|
|
only: what is the array to traverse for cleaning up? What is the
|
|
number of tables to loop over?
|
|
So: if we fail in the present loop, we record the error but continue
|
|
filling best_ref; when it's fully filled, bail out, because then
|
|
best_ref can be used as reliable array for cleaning up.
|
|
*/
|
|
JOIN_TAB *const tab = tmp_join_tabs++;
|
|
best_ref[outer_target] = tab;
|
|
tab->set_join(this);
|
|
tab->set_idx(outer_target);
|
|
|
|
/*
|
|
Up to this point there cannot be a failure. JOIN_TAB has been filled
|
|
enough to be clean-able.
|
|
*/
|
|
|
|
Semijoin_mat_exec *const sjm_exec = new (thd->mem_root) Semijoin_mat_exec(
|
|
sj_nest, (pos->sj_strategy == SJ_OPT_MATERIALIZE_SCAN),
|
|
remaining_sjm_inner, outer_target, inner_target);
|
|
|
|
tab->set_sj_mat_exec(sjm_exec);
|
|
|
|
if (!sjm_exec || setup_semijoin_materialized_table(
|
|
tab, sjm_index, pos, best_positions + sjm_index))
|
|
err = true; /* purecov: inspected */
|
|
|
|
outer_target++;
|
|
sjm_index++;
|
|
}
|
|
/*
|
|
Locate join_tab target for the table we are considering.
|
|
(remaining_sjm_inner becomes negative for non-SJM tables, this can be
|
|
safely ignored).
|
|
*/
|
|
const uint target =
|
|
(remaining_sjm_inner--) > 0 ? inner_target++ : outer_target++;
|
|
JOIN_TAB *const tab = pos->table;
|
|
|
|
best_ref[target] = tab;
|
|
tab->set_idx(target);
|
|
tab->set_position(pos);
|
|
TABLE *const table = tab->table();
|
|
if (tab->type() != JT_CONST && tab->type() != JT_SYSTEM) {
|
|
if (pos->sj_strategy == SJ_OPT_LOOSE_SCAN && tab->range_scan() &&
|
|
used_index(tab->range_scan()) != pos->loosescan_key) {
|
|
/*
|
|
We must use the duplicate-eliminating index, so this QUICK is not
|
|
an option.
|
|
*/
|
|
::destroy_at(tab->range_scan());
|
|
tab->set_range_scan(nullptr);
|
|
}
|
|
if (table->is_intersect() || table->is_except()) {
|
|
tab->set_type(JT_ALL); // INTERSECT, EXCEPT can't use ref access yet
|
|
} else if (!pos->key) {
|
|
if (tab->range_scan())
|
|
tab->set_type(calc_join_type(tab->range_scan()));
|
|
else
|
|
tab->set_type(JT_ALL);
|
|
} else {
|
|
// REF or RANGE, clarify later when prefix tables are set for JOIN_TABs
|
|
tab->set_type(JT_REF);
|
|
}
|
|
}
|
|
assert(tab->type() != JT_UNKNOWN);
|
|
|
|
assert(table->reginfo.join_tab == tab);
|
|
if (!tab->join_cond())
|
|
table->reginfo.not_exists_optimize = false; // Only with LEFT JOIN
|
|
map2table[tab->table_ref->tableno()] = tab;
|
|
}
|
|
|
|
// Count the materialized semi-join tables as regular input tables
|
|
tables += sjm_nests + num_tmp_tables;
|
|
// Set the number of non-materialized tables:
|
|
primary_tables = outer_target;
|
|
|
|
/*
|
|
Between the last outer table or sj-mat tmp table, and the first sj-mat
|
|
inner table, there may be 2 slots for sort/group/etc tmp tables:
|
|
*/
|
|
for (uint i = 0; i < num_tmp_tables; ++i) {
|
|
const uint idx = outer_target + i;
|
|
tmp_join_tabs->set_join(this);
|
|
tmp_join_tabs->set_idx(idx);
|
|
assert(best_ref[idx] == nullptr); // verify that not overwriting
|
|
best_ref[idx] = tmp_join_tabs++;
|
|
/*
|
|
note that set_table() cannot be called yet. We may not even use this
|
|
JOIN_TAB in the end, it's dummy at the moment. Which can be tested with
|
|
"position()!=NULL".
|
|
*/
|
|
}
|
|
|
|
// make array unreachable: should walk JOIN_TABs by best_ref now
|
|
join_tab = nullptr;
|
|
|
|
if (err) return true; /* purecov: inspected */
|
|
|
|
if (has_semijoin) {
|
|
set_semijoin_info();
|
|
|
|
// Update equalities and keyuses after having added SJ materialization
|
|
if (update_equalities_for_sjm()) return true;
|
|
}
|
|
if (!plan_is_const()) {
|
|
// Assign map of "available" tables to all tables belonging to query block
|
|
set_prefix_tables();
|
|
adjust_access_methods();
|
|
}
|
|
// Calculate outer join info
|
|
if (query_block->outer_join) make_outerjoin_info();
|
|
|
|
// sjm is no longer needed, trash it. To reuse it, reset its members!
|
|
for (Table_ref *sj_nest : query_block->sj_nests) {
|
|
TRASH(&sj_nest->nested_join->sjm, sizeof(sj_nest->nested_join->sjm));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Finds the dependencies of the remaining lateral derived tables.
|
|
|
|
@param plan_tables map of all tables that the planner is processing
|
|
(tables already in plan and tables to be added to plan).
|
|
@param idx index of the table which the planner is currently
|
|
considering.
|
|
@return A map of the dependencies of the remaining
|
|
lateral derived tables (from best_ref[idx] and on).
|
|
*/
|
|
table_map JOIN::calculate_deps_of_remaining_lateral_derived_tables(
|
|
table_map plan_tables, uint idx) const {
|
|
assert(has_lateral);
|
|
table_map deps = 0;
|
|
auto last = best_ref + tables;
|
|
for (auto **pos = best_ref + idx; pos < last; pos++) {
|
|
if ((*pos)->table_ref && ((*pos)->table_ref->map() & plan_tables)) {
|
|
deps |= get_lateral_deps(**pos);
|
|
}
|
|
}
|
|
return deps;
|
|
}
|
|
|
|
/*
|
|
Revise usage of join buffer for the specified table and the whole nest
|
|
|
|
SYNOPSIS
|
|
revise_cache_usage()
|
|
tab join table for which join buffer usage is to be revised
|
|
|
|
DESCRIPTION
|
|
The function revise the decision to use a join buffer for the table 'tab'.
|
|
If this table happened to be among the inner tables of a nested outer join/
|
|
semi-join the functions denies usage of join buffers for all of them
|
|
|
|
RETURN
|
|
none
|
|
*/
|
|
|
|
static void revise_cache_usage(JOIN_TAB *join_tab) {
|
|
plan_idx first_inner = join_tab->first_inner();
|
|
JOIN *const join = join_tab->join();
|
|
if (first_inner != NO_PLAN_IDX) {
|
|
plan_idx end_tab = join_tab->idx();
|
|
for (first_inner = join_tab->first_inner(); first_inner != NO_PLAN_IDX;
|
|
first_inner = join->best_ref[first_inner]->first_upper()) {
|
|
for (plan_idx i = end_tab - 1; i >= first_inner; --i)
|
|
join->best_ref[i]->set_use_join_cache(JOIN_CACHE::ALG_NONE);
|
|
end_tab = first_inner;
|
|
}
|
|
} else if (join_tab->get_sj_strategy() == SJ_OPT_FIRST_MATCH) {
|
|
const plan_idx first_sj_inner = join_tab->first_sj_inner();
|
|
for (plan_idx i = join_tab->idx() - 1; i >= first_sj_inner; --i) {
|
|
JOIN_TAB *tab = join->best_ref[i];
|
|
if (tab->first_sj_inner() == first_sj_inner)
|
|
tab->set_use_join_cache(JOIN_CACHE::ALG_NONE);
|
|
}
|
|
} else
|
|
join_tab->set_use_join_cache(JOIN_CACHE::ALG_NONE);
|
|
assert(join->qep_tab == nullptr);
|
|
}
|
|
|
|
/**
|
|
Set up join buffering for a specified table, if possible.
|
|
|
|
@param tab joined table to check join buffer usage for
|
|
@param join join for which the check is performed
|
|
@param no_jbuf_after don't use join buffering after table with this number
|
|
|
|
@return false if successful, true if error.
|
|
Currently, allocation errors for join cache objects are ignored,
|
|
and regular execution is chosen silently.
|
|
|
|
@details
|
|
The function finds out whether the table 'tab' can be joined using a join
|
|
buffer. This check is performed after the best execution plan for 'join'
|
|
has been chosen. If the function decides that a join buffer can be employed
|
|
then it selects the most appropriate join cache type, which later will
|
|
be instantiated by init_join_cache().
|
|
If it has already been decided to not use join buffering for this table,
|
|
no action is taken.
|
|
|
|
Often it is already decided that join buffering will be used earlier in
|
|
the optimization process, and this will also ensure that the most correct
|
|
cost for the operation is calculated, and hence the probability of
|
|
choosing an optimal join plan is higher. However, some join buffering
|
|
decisions cannot currently be taken before this stage, hence we need this
|
|
function to decide the most accurate join buffering strategy.
|
|
|
|
@todo Long-term it is the goal that join buffering strategy is decided
|
|
when the plan is selected.
|
|
|
|
The result of the check and the type of the join buffer to be used
|
|
depend on:
|
|
- the access method to access rows of the joined table
|
|
- whether the join table is an inner table of an outer join or semi-join
|
|
- the optimizer_switch settings for join buffering
|
|
- the join 'options'.
|
|
In any case join buffer is not used if the number of the joined table is
|
|
greater than 'no_jbuf_after'.
|
|
|
|
If block_nested_loop is turned on, and if all other criteria for using
|
|
join buffering is fulfilled (see below), then join buffer is used
|
|
for any join operation (inner join, outer join, semi-join) with 'JT_ALL'
|
|
access method. In that case, a JOIN_CACHE_BNL type is always employed.
|
|
|
|
If an index is used to access rows of the joined table and
|
|
batched_key_access is on, then a JOIN_CACHE_BKA type is employed.
|
|
|
|
If the function decides that a join buffer can be used to join the table
|
|
'tab' then it sets @c tab->use_join_cache to reflect the chosen algorithm.
|
|
|
|
@note
|
|
For a nested outer join/semi-join, currently, we either use join buffers for
|
|
all inner tables or for none of them.
|
|
|
|
Join buffering is enabled for a few more cases for secondary engine.
|
|
Currently if blocked nested loop(BNL) is employed for join buffering,
|
|
it is replaced by hash joins in the executor. So the reasons for disabling
|
|
join buffering because of the way BNL works are no more valid. This gives
|
|
us an oppotunity to enable join buffering for more cases. However,
|
|
we enable it only for secondary engine (in particular for semijoins),
|
|
because of the following reasons:
|
|
Secondary engine does not care about the cost based decisions
|
|
involved in arriving at the best possible semijoin strategy;
|
|
because it can only interpret a plan using "FirstMatch" strategy
|
|
and can only do table scans. So the choices are very limited.
|
|
However, it's not the case for mysql. There are serveral semijoin
|
|
stratagies that could be picked. And these are picked based
|
|
on the assumption that a nested-loop join(NLJ) would be used because
|
|
optimizer currently generates plans only for NLJs and not
|
|
hash joins. So, when executor replaces with hash joins, the number
|
|
of rows that would be looked into for a particular semijoin strategy
|
|
will differ from what the optimizer presumed while picking that
|
|
strategy.
|
|
For mysql server, we could enable join buffering for more cases, when
|
|
a cost model for using hash joins is developed and optimizer could
|
|
generate plans for hash joins.
|
|
|
|
@todo
|
|
Support BKA inside SJ-Materialization nests. When doing this, we'll need
|
|
to only store sj-inner tables in the join buffer.
|
|
@verbatim
|
|
JOIN_TAB *first_tab= join->join_tab+join->const_tables;
|
|
uint n_tables= i-join->const_tables;
|
|
/ *
|
|
We normally put all preceding tables into the join buffer, except
|
|
for the constant tables.
|
|
If we're inside a semi-join materialization nest, e.g.
|
|
|
|
outer_tbl1 outer_tbl2 ( inner_tbl1, inner_tbl2 ) ...
|
|
^-- we're here
|
|
|
|
then we need to put into the join buffer only the tables from
|
|
within the nest.
|
|
* /
|
|
if (i >= first_sjm_table && i < last_sjm_table)
|
|
{
|
|
n_tables= i - first_sjm_table; // will be >0 if we got here
|
|
first_tab= join->join_tab + first_sjm_table;
|
|
}
|
|
@endverbatim
|
|
*/
|
|
|
|
static bool setup_join_buffering(JOIN_TAB *tab, JOIN *join,
|
|
uint no_jbuf_after) {
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(join);
|
|
Cost_estimate cost;
|
|
ha_rows rows;
|
|
uint bufsz = 4096;
|
|
uint join_cache_flags = 0;
|
|
const bool bnl_on = hint_table_state(join->thd, tab->table_ref, BNL_HINT_ENUM,
|
|
OPTIMIZER_SWITCH_BNL);
|
|
const bool bka_on = hint_table_state(join->thd, tab->table_ref, BKA_HINT_ENUM,
|
|
OPTIMIZER_SWITCH_BKA);
|
|
|
|
const uint tableno = tab->idx();
|
|
const uint tab_sj_strategy = tab->get_sj_strategy();
|
|
|
|
/*
|
|
If all key_parts are null_rejecting, the MultiRangeRowIterator will
|
|
eliminate all NULL values in the key set, such that
|
|
HA_MRR_NO_NULL_ENDPOINTS can be promised.
|
|
*/
|
|
const key_part_map keypart_map = make_prev_keypart_map(tab->ref().key_parts);
|
|
if (tab->ref().null_rejecting == keypart_map) {
|
|
join_cache_flags |= HA_MRR_NO_NULL_ENDPOINTS;
|
|
}
|
|
|
|
// Set preliminary join cache setting based on decision from greedy search
|
|
if (!join->select_count)
|
|
tab->set_use_join_cache(tab->position()->use_join_buffer
|
|
? JOIN_CACHE::ALG_BNL
|
|
: JOIN_CACHE::ALG_NONE);
|
|
|
|
if (tableno == join->const_tables) {
|
|
assert(tab->use_join_cache() == JOIN_CACHE::ALG_NONE);
|
|
return false;
|
|
}
|
|
|
|
if (!(bnl_on || bka_on)) goto no_join_cache;
|
|
|
|
/*
|
|
psergey-todo: why the below when execution code seems to handle the
|
|
"range checked for each record" case?
|
|
*/
|
|
if (tab->use_quick == QS_DYNAMIC_RANGE) goto no_join_cache;
|
|
|
|
/* No join buffering if prevented by no_jbuf_after */
|
|
if (tableno > no_jbuf_after) goto no_join_cache;
|
|
|
|
/*
|
|
An inner table of an outer join nest must not use join buffering if
|
|
the first inner table of that outer join nest does not use join buffering.
|
|
This condition is not handled by earlier optimizer stages.
|
|
*/
|
|
if (tab->first_inner() != NO_PLAN_IDX && tab->first_inner() != tab->idx() &&
|
|
!join->best_ref[tab->first_inner()]->use_join_cache())
|
|
goto no_join_cache;
|
|
/*
|
|
The first inner table of an outer join nest must not use join buffering
|
|
if the tables in the embedding outer join nest do not use join buffering.
|
|
This condition is not handled by earlier optimizer stages.
|
|
*/
|
|
if (tab->first_upper() != NO_PLAN_IDX &&
|
|
!join->best_ref[tab->first_upper()]->use_join_cache())
|
|
goto no_join_cache;
|
|
|
|
if (tab->table()->pos_in_table_list->is_table_function() && tab->dependent)
|
|
goto no_join_cache;
|
|
|
|
switch (tab_sj_strategy) {
|
|
case SJ_OPT_FIRST_MATCH:
|
|
/*
|
|
Use join cache with FirstMatch semi-join strategy only when semi-join
|
|
contains only one table.
|
|
As mentioned earlier (in comments), we lift this restriction for
|
|
secondary engine.
|
|
*/
|
|
if (!(current_thd->lex->m_sql_cmd != nullptr &&
|
|
current_thd->lex->m_sql_cmd->using_secondary_storage_engine())) {
|
|
if (!tab->is_single_inner_of_semi_join()) {
|
|
assert(tab->use_join_cache() == JOIN_CACHE::ALG_NONE);
|
|
goto no_join_cache;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SJ_OPT_LOOSE_SCAN:
|
|
/* No join buffering if this semijoin nest is handled by loosescan */
|
|
assert(tab->use_join_cache() == JOIN_CACHE::ALG_NONE);
|
|
goto no_join_cache;
|
|
|
|
case SJ_OPT_MATERIALIZE_LOOKUP:
|
|
case SJ_OPT_MATERIALIZE_SCAN:
|
|
/*
|
|
The Materialize strategies reuse the join_tab belonging to the
|
|
first table that was materialized. Neither table can use join buffering:
|
|
- The first table in a join never uses join buffering.
|
|
- The join_tab used for looking up a row in the materialized table, or
|
|
scanning the rows of a materialized table, cannot use join buffering.
|
|
We allow join buffering for the remaining tables of the materialized
|
|
semi-join nest.
|
|
*/
|
|
if (tab->first_sj_inner() == tab->idx()) {
|
|
assert(tab->use_join_cache() == JOIN_CACHE::ALG_NONE);
|
|
goto no_join_cache;
|
|
}
|
|
break;
|
|
|
|
case SJ_OPT_DUPS_WEEDOUT:
|
|
// This strategy allows the same join buffering as a regular join would.
|
|
case SJ_OPT_NONE:
|
|
break;
|
|
}
|
|
|
|
/*
|
|
The following code prevents use of join buffering when there is an
|
|
outer join operation and first match semi-join strategy is used, because:
|
|
|
|
Outer join needs a "match flag" to track that a row should be
|
|
NULL-complemented, such flag being attached to first inner table's cache
|
|
(tracks whether the cached row from outer table got a match, in which case
|
|
no NULL-complemented row is needed).
|
|
|
|
FirstMatch also needs a "match flag", such flag is attached to sj inner
|
|
table's cache (tracks whether the cached row from outer table already got
|
|
a first match in the sj-inner table, in which case we don't need to join
|
|
this cached row again)
|
|
- but a row in a cache has only one "match flag"
|
|
- so if "sj inner table"=="first inner", there is a problem.
|
|
|
|
As mentioned earlier(in comments), we lift this restriction for
|
|
secondary engine.
|
|
*/
|
|
if (!(current_thd->lex->m_sql_cmd != nullptr &&
|
|
current_thd->lex->m_sql_cmd->using_secondary_storage_engine())) {
|
|
if (tab_sj_strategy == SJ_OPT_FIRST_MATCH &&
|
|
tab->is_inner_table_of_outer_join())
|
|
goto no_join_cache;
|
|
}
|
|
|
|
if (join->deps_of_remaining_lateral_derived_tables &
|
|
(tab->prefix_tables() & ~tab->added_tables())) {
|
|
/*
|
|
Even though the planner said "no jbuf please", the switch below may
|
|
force it.
|
|
If first-dependency-of-lateral-table < table-we-plan-for <=
|
|
lateral-table, disable join buffering.
|
|
Reason for this rule:
|
|
consider a plan t1-t2-dt where dt is LATERAL and depends only on t1, and
|
|
imagine t2 could do join buffering: then we buffer many rows of t1, then
|
|
read one row of t2, fetch row#1 of t1 from cache, then materialize "dt"
|
|
(as it depends on t1) and send row to client; then fetch row#2 of t1
|
|
from cache, rematerialize "dt": it's very inefficient. So we forbid join
|
|
buffering on t2; this way, the signal "row of t1 changed" is emitted at
|
|
the level of t1's operator, i.e. much less often, as one row of t1 may
|
|
serve N rows of t2 before changing.
|
|
On the other hand, t1 can do join buffering.
|
|
A nice side-effect is to disable join buffering for "dt" itself. If
|
|
"dt" would do join buffering: "dt" buffers many rows from t1/t2, then in a
|
|
second phase we read one row from "dt" and join it with the many rows
|
|
from t1/t2; but we cannot read a row from "dt" without first choosing a
|
|
row of t1/t2 as "dt" depends on t1.
|
|
See similar code in best_access_path().
|
|
*/
|
|
goto no_join_cache;
|
|
}
|
|
|
|
switch (tab->type()) {
|
|
case JT_ALL:
|
|
case JT_INDEX_SCAN:
|
|
case JT_RANGE:
|
|
case JT_INDEX_MERGE:
|
|
if (!bnl_on) {
|
|
assert(tab->use_join_cache() == JOIN_CACHE::ALG_NONE);
|
|
goto no_join_cache;
|
|
}
|
|
|
|
if (!join->select_count) tab->set_use_join_cache(JOIN_CACHE::ALG_BNL);
|
|
return false;
|
|
case JT_SYSTEM:
|
|
case JT_CONST:
|
|
case JT_REF:
|
|
case JT_EQ_REF:
|
|
if (!bka_on) {
|
|
assert(tab->use_join_cache() == JOIN_CACHE::ALG_NONE);
|
|
goto no_join_cache;
|
|
}
|
|
|
|
/*
|
|
Disable BKA for materializable derived tables/views as they aren't
|
|
instantiated yet.
|
|
*/
|
|
if (tab->table_ref->uses_materialization()) goto no_join_cache;
|
|
|
|
/*
|
|
Can't use BKA for subquery if dealing with a subquery that can
|
|
turn a ref access into a "full scan on NULL key" table scan.
|
|
|
|
@see Item_in_optimizer::val_int()
|
|
@see subselect_iterator_engine::exec()
|
|
@see Index_lookup::cond_guards
|
|
@see push_index_cond()
|
|
|
|
@todo: This choice to not use BKA should be done before making
|
|
cost estimates, e.g. in set_join_buffer_properties(). That
|
|
happens before cond guards are set up, so instead of doing the
|
|
check below, BKA should be disabled if
|
|
- We are in an IN subquery, and
|
|
- The IN predicate is not a top_level_item, and
|
|
- The left_expr of the IN predicate may contain NULL values
|
|
(left_expr->maybe_null)
|
|
*/
|
|
if (tab->has_guarded_conds()) goto no_join_cache;
|
|
|
|
if (tab->table()->covering_keys.is_set(tab->ref().key))
|
|
join_cache_flags |= HA_MRR_INDEX_ONLY;
|
|
rows = tab->table()->file->multi_range_read_info(
|
|
tab->ref().key, 10, 20, &bufsz, &join_cache_flags, &cost);
|
|
/*
|
|
Cannot use BKA if
|
|
1. MRR scan cannot be performed, or
|
|
2. MRR default implementation is used, or
|
|
3. HA_MRR_NO_ASSOCIATION flag is set
|
|
*/
|
|
if ((rows == HA_POS_ERROR) || // 1
|
|
(join_cache_flags & HA_MRR_USE_DEFAULT_IMPL) || // 2
|
|
(join_cache_flags & HA_MRR_NO_ASSOCIATION)) // 3
|
|
goto no_join_cache;
|
|
|
|
tab->set_use_join_cache(JOIN_CACHE::ALG_BKA);
|
|
|
|
tab->join_cache_flags = join_cache_flags;
|
|
return false;
|
|
default:;
|
|
}
|
|
|
|
no_join_cache:
|
|
revise_cache_usage(tab);
|
|
tab->set_use_join_cache(JOIN_CACHE::ALG_NONE);
|
|
return false;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Make some simple condition optimization:
|
|
If there is a test 'field = const' change all refs to 'field' to 'const'
|
|
Remove all dummy tests 'item = item', 'const op const'.
|
|
Remove all 'item is NULL', when item can never be null!
|
|
Return in cond_value false if condition is impossible (1 = 2)
|
|
*****************************************************************************/
|
|
|
|
class COND_CMP : public ilink<COND_CMP> {
|
|
public:
|
|
static void *operator new(size_t size) { return (*THR_MALLOC)->Alloc(size); }
|
|
static void operator delete(void *ptr [[maybe_unused]],
|
|
size_t size [[maybe_unused]]) {
|
|
TRASH(ptr, size);
|
|
}
|
|
|
|
Item *and_level;
|
|
Item_func *cmp_func;
|
|
COND_CMP(Item *a, Item_func *b) : and_level(a), cmp_func(b) {}
|
|
};
|
|
|
|
Item_equal *find_item_equal(COND_EQUAL *cond_equal,
|
|
const Item_field *item_field, bool *inherited_fl) {
|
|
Item_equal *item = nullptr;
|
|
bool in_upper_level = false;
|
|
while (cond_equal) {
|
|
List_iterator_fast<Item_equal> li(cond_equal->current_level);
|
|
while ((item = li++)) {
|
|
if (item->contains(item_field->field)) goto finish;
|
|
}
|
|
in_upper_level = true;
|
|
cond_equal = cond_equal->upper_levels;
|
|
}
|
|
in_upper_level = false;
|
|
finish:
|
|
*inherited_fl = in_upper_level;
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
Get the best field substitution for a given field.
|
|
|
|
If the field is member of a multiple equality, look up that equality
|
|
and return the most appropriate field. Usually this is the equivalenced
|
|
field belonging to the outer-most table in the join order, but
|
|
@see Item_field::get_subst_item() for details.
|
|
Otherwise, return the same field.
|
|
|
|
@param item_field The field that we are seeking a substitution for.
|
|
@param cond_equal multiple equalities to search in
|
|
|
|
@return The substituted field.
|
|
*/
|
|
|
|
Item_field *get_best_field(Item_field *item_field, COND_EQUAL *cond_equal) {
|
|
bool dummy;
|
|
Item_equal *item_eq = find_item_equal(cond_equal, item_field, &dummy);
|
|
if (!item_eq) return item_field;
|
|
|
|
return item_eq->get_subst_item(item_field);
|
|
}
|
|
|
|
/**
|
|
Check whether an equality can be used to build multiple equalities.
|
|
|
|
This function first checks whether the equality (left_item=right_item)
|
|
is a simple equality i.e. one that equates a field with another field
|
|
or a constant (field=field_item or field=const_item).
|
|
If this is the case the function looks for a multiple equality
|
|
in the lists referenced directly or indirectly by cond_equal inferring
|
|
the given simple equality. If it doesn't find any, it builds a multiple
|
|
equality that covers the predicate, i.e. the predicate can be inferred
|
|
from this multiple equality.
|
|
The built multiple equality could be obtained in such a way:
|
|
create a binary multiple equality equivalent to the predicate, then
|
|
merge it, if possible, with one of old multiple equalities.
|
|
This guarantees that the set of multiple equalities covering equality
|
|
predicates will be minimal.
|
|
|
|
EXAMPLE:
|
|
For the where condition
|
|
@code
|
|
WHERE a=b AND b=c AND
|
|
(b=2 OR f=e)
|
|
@endcode
|
|
the check_equality will be called for the following equality
|
|
predicates a=b, b=c, b=2 and f=e.
|
|
- For a=b it will be called with *cond_equal=(0,[]) and will transform
|
|
*cond_equal into (0,[Item_equal(a,b)]).
|
|
- For b=c it will be called with *cond_equal=(0,[Item_equal(a,b)])
|
|
and will transform *cond_equal into CE=(0,[Item_equal(a,b,c)]).
|
|
- For b=2 it will be called with *cond_equal=(ptr(CE),[])
|
|
and will transform *cond_equal into (ptr(CE),[Item_equal(2,a,b,c)]).
|
|
- For f=e it will be called with *cond_equal=(ptr(CE), [])
|
|
and will transform *cond_equal into (ptr(CE),[Item_equal(f,e)]).
|
|
|
|
@note
|
|
Now only fields that have the same type definitions (verified by
|
|
the Field::eq_def method) are placed to the same multiple equalities.
|
|
Because of this some equality predicates are not eliminated and
|
|
can be used in the constant propagation procedure.
|
|
We could weaken the equality test as soon as at least one of the
|
|
equal fields is to be equal to a constant. It would require a
|
|
more complicated implementation: we would have to store, in
|
|
general case, its own constant for each fields from the multiple
|
|
equality. But at the same time it would allow us to get rid
|
|
of constant propagation completely: it would be done by the call
|
|
to build_equal_items_for_cond.
|
|
|
|
The implementation does not follow exactly the above rules to
|
|
build a new multiple equality for the equality predicate.
|
|
If it processes the equality of the form field1=field2, it
|
|
looks for multiple equalities me1 containing field1 and me2 containing
|
|
field2. If only one of them is found the function expands it with
|
|
the lacking field. If multiple equalities for both fields are
|
|
found they are merged. If both searches fail a new multiple equality
|
|
containing just field1 and field2 is added to the existing
|
|
multiple equalities.
|
|
If the function processes the predicate of the form field1=const,
|
|
it looks for a multiple equality containing field1. If found, the
|
|
function checks the constant of the multiple equality. If the value
|
|
is unknown, it is setup to const. Otherwise the value is compared with
|
|
const and the evaluation of the equality predicate is performed.
|
|
When expanding/merging equality predicates from the upper levels
|
|
the function first copies them for the current level. It looks
|
|
acceptable, as this happens rarely. The implementation without
|
|
copying would be much more complicated.
|
|
|
|
@param thd Thread handler
|
|
@param left_item left term of the equality to be checked
|
|
@param right_item right term of the equality to be checked
|
|
@param item equality item if the equality originates from a condition
|
|
predicate, 0 if the equality is the result of row
|
|
elimination
|
|
@param cond_equal multiple equalities that must hold together with the
|
|
equality
|
|
@param[out] simple_equality
|
|
true if the predicate is a simple equality predicate
|
|
to be used for building multiple equalities
|
|
false otherwise
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
static bool check_simple_equality(THD *thd, Item *left_item, Item *right_item,
|
|
Item *item, COND_EQUAL *cond_equal,
|
|
bool *simple_equality) {
|
|
*simple_equality = false;
|
|
|
|
if (left_item->type() == Item::REF_ITEM &&
|
|
down_cast<Item_ref *>(left_item)->ref_type() == Item_ref::VIEW_REF) {
|
|
if (down_cast<Item_ref *>(left_item)->is_outer_reference()) return false;
|
|
left_item = left_item->real_item();
|
|
}
|
|
if (right_item->type() == Item::REF_ITEM &&
|
|
down_cast<Item_ref *>(right_item)->ref_type() == Item_ref::VIEW_REF) {
|
|
if (down_cast<Item_ref *>(right_item)->is_outer_reference()) return false;
|
|
right_item = right_item->real_item();
|
|
}
|
|
const Item_field *left_item_field, *right_item_field;
|
|
|
|
if (left_item->type() == Item::FIELD_ITEM &&
|
|
right_item->type() == Item::FIELD_ITEM &&
|
|
(left_item_field = down_cast<const Item_field *>(left_item)) &&
|
|
(right_item_field = down_cast<const Item_field *>(right_item)) &&
|
|
!left_item_field->depended_from && !right_item_field->depended_from) {
|
|
/* The predicate the form field1=field2 is processed */
|
|
|
|
const Field *const left_field = left_item_field->field;
|
|
const Field *const right_field = right_item_field->field;
|
|
|
|
if (!left_field->eq_def(right_field)) return false;
|
|
|
|
/* Search for multiple equalities containing field1 and/or field2 */
|
|
bool left_copyfl, right_copyfl;
|
|
Item_equal *left_item_equal =
|
|
find_item_equal(cond_equal, left_item_field, &left_copyfl);
|
|
Item_equal *right_item_equal =
|
|
find_item_equal(cond_equal, right_item_field, &right_copyfl);
|
|
|
|
/* As (NULL=NULL) != TRUE we can't just remove the predicate f=f */
|
|
if (left_field->eq(right_field)) /* f = f */
|
|
{
|
|
*simple_equality =
|
|
!((left_field->is_nullable() || left_field->table->is_nullable()) &&
|
|
!left_item_equal);
|
|
return false;
|
|
}
|
|
|
|
if (left_item_equal && left_item_equal == right_item_equal) {
|
|
/*
|
|
The equality predicate is inference of one of the existing
|
|
multiple equalities, i.e the condition is already covered
|
|
by upper level equalities
|
|
*/
|
|
*simple_equality = true;
|
|
return false;
|
|
}
|
|
|
|
/* Copy the found multiple equalities at the current level if needed */
|
|
if (left_copyfl) {
|
|
/* left_item_equal of an upper level contains left_item */
|
|
left_item_equal = new Item_equal(left_item_equal);
|
|
if (left_item_equal == nullptr) return true;
|
|
cond_equal->current_level.push_back(left_item_equal);
|
|
}
|
|
if (right_copyfl) {
|
|
/* right_item_equal of an upper level contains right_item */
|
|
right_item_equal = new Item_equal(right_item_equal);
|
|
if (right_item_equal == nullptr) return true;
|
|
cond_equal->current_level.push_back(right_item_equal);
|
|
}
|
|
|
|
if (left_item_equal) {
|
|
/* left item was found in the current or one of the upper levels */
|
|
if (!right_item_equal)
|
|
left_item_equal->add(down_cast<Item_field *>(right_item));
|
|
else {
|
|
/* Merge two multiple equalities forming a new one */
|
|
if (left_item_equal->merge(thd, right_item_equal)) return true;
|
|
/* Remove the merged multiple equality from the list */
|
|
List_iterator<Item_equal> li(cond_equal->current_level);
|
|
while ((li++) != right_item_equal)
|
|
;
|
|
li.remove();
|
|
}
|
|
} else {
|
|
/* left item was not found neither the current nor in upper levels */
|
|
if (right_item_equal) {
|
|
right_item_equal->add(down_cast<Item_field *>(left_item));
|
|
} else {
|
|
/* None of the fields was found in multiple equalities */
|
|
Item_equal *item_equal =
|
|
new Item_equal(down_cast<Item_field *>(left_item),
|
|
down_cast<Item_field *>(right_item));
|
|
if (item_equal == nullptr) return true;
|
|
cond_equal->current_level.push_back(item_equal);
|
|
}
|
|
}
|
|
*simple_equality = true;
|
|
return false;
|
|
}
|
|
|
|
{
|
|
/* The predicate of the form field=const/const=field is processed */
|
|
Item *const_item = nullptr;
|
|
Item_field *field_item = nullptr;
|
|
if (left_item->type() == Item::FIELD_ITEM &&
|
|
(field_item = down_cast<Item_field *>(left_item)) &&
|
|
field_item->depended_from == nullptr &&
|
|
right_item->const_for_execution()) {
|
|
const_item = right_item;
|
|
} else if (right_item->type() == Item::FIELD_ITEM &&
|
|
(field_item = down_cast<Item_field *>(right_item)) &&
|
|
field_item->depended_from == nullptr &&
|
|
left_item->const_for_execution()) {
|
|
const_item = left_item;
|
|
}
|
|
|
|
// Don't evaluate subqueries if they are disabled during optimization.
|
|
if (const_item != nullptr &&
|
|
!evaluate_during_optimization(const_item,
|
|
thd->lex->current_query_block()))
|
|
return false;
|
|
|
|
/*
|
|
If the constant expression contains a reference to the field
|
|
(for example, a = (a IS NULL)), we don't want to replace the
|
|
field with the constant expression as it makes the predicates
|
|
more complex and may introduce cycles in the Item tree.
|
|
*/
|
|
if (const_item != nullptr &&
|
|
const_item->walk(&Item::find_field_processor, enum_walk::POSTFIX,
|
|
pointer_cast<uchar *>(field_item->field)))
|
|
return false;
|
|
|
|
if (const_item && field_item->result_type() == const_item->result_type()) {
|
|
if (field_item->result_type() == STRING_RESULT) {
|
|
const CHARSET_INFO *cs = field_item->field->charset();
|
|
if (!item) {
|
|
Item_func_eq *const eq_item = new Item_func_eq(left_item, right_item);
|
|
if (eq_item == nullptr || eq_item->set_cmp_func()) return true;
|
|
eq_item->quick_fix_field();
|
|
item = eq_item;
|
|
}
|
|
if ((cs != down_cast<Item_func *>(item)->compare_collation()) ||
|
|
!cs->coll->propagate(cs, nullptr, 0))
|
|
return false;
|
|
// Don't build multiple equalities mixing strings and JSON, not even
|
|
// when they have the same collation, since string comparison and JSON
|
|
// comparison are very different.
|
|
if ((field_item->data_type() == MYSQL_TYPE_JSON) !=
|
|
(const_item->data_type() == MYSQL_TYPE_JSON)) {
|
|
return false;
|
|
}
|
|
// Similarly, strings and temporal types have different semantics for
|
|
// equality comparison.
|
|
if (const_item->is_temporal()) {
|
|
// No multiple equality for string columns compared to temporal
|
|
// values. See also comment in comparable_in_index().
|
|
if (!field_item->is_temporal()) {
|
|
return false;
|
|
}
|
|
// No multiple equality for TIME columns compared to temporal values.
|
|
// See also comment in comparable_in_index().
|
|
if (const_item->is_temporal_with_date() &&
|
|
!field_item->is_temporal_with_date()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool copyfl;
|
|
Item_equal *item_equal = find_item_equal(cond_equal, field_item, ©fl);
|
|
if (copyfl) {
|
|
item_equal = new Item_equal(item_equal);
|
|
if (item_equal == nullptr) return true;
|
|
cond_equal->current_level.push_back(item_equal);
|
|
}
|
|
if (item_equal) {
|
|
if (item_equal->const_arg() != nullptr) {
|
|
// Make sure that the existing const and new one are of comparable
|
|
// collation.
|
|
DTCollation cmp_collation;
|
|
if (cmp_collation.set(const_item->collation,
|
|
item_equal->const_arg()->collation,
|
|
MY_COLL_CMP_CONV) ||
|
|
cmp_collation.derivation == DERIVATION_NONE) {
|
|
return false;
|
|
}
|
|
}
|
|
/*
|
|
The flag cond_false will be set to 1 after this, if item_equal
|
|
already contains a constant and its value is not equal to
|
|
the value of const_item.
|
|
*/
|
|
if (item_equal->add(thd, const_item, field_item)) return true;
|
|
} else {
|
|
item_equal = new Item_equal(const_item, field_item);
|
|
if (item_equal == nullptr) return true;
|
|
cond_equal->current_level.push_back(item_equal);
|
|
}
|
|
*simple_equality = true;
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Convert row equalities into a conjunction of regular equalities.
|
|
|
|
The function converts a row equality of the form (E1,...,En)=(E'1,...,E'n)
|
|
into a list of equalities E1=E'1,...,En=E'n. For each of these equalities
|
|
Ei=E'i the function checks whether it is a simple equality or a row
|
|
equality. If it is a simple equality it is used to expand multiple
|
|
equalities of cond_equal. If it is a row equality it converted to a
|
|
sequence of equalities between row elements. If Ei=E'i is neither a
|
|
simple equality nor a row equality the item for this predicate is added
|
|
to eq_list.
|
|
|
|
@param thd thread handle
|
|
@param left_row left term of the row equality to be processed
|
|
@param right_row right term of the row equality to be processed
|
|
@param cond_equal multiple equalities that must hold together with the
|
|
predicate
|
|
@param eq_list results of conversions of row equalities that are not
|
|
simple enough to form multiple equalities
|
|
@param[out] simple_equality
|
|
true if the row equality is composed of only
|
|
simple equalities.
|
|
|
|
@returns false if conversion succeeded, true if any error.
|
|
*/
|
|
|
|
static bool check_row_equality(THD *thd, Item *left_row, Item_row *right_row,
|
|
COND_EQUAL *cond_equal, List<Item> *eq_list,
|
|
bool *simple_equality) {
|
|
*simple_equality = false;
|
|
const uint n = left_row->cols();
|
|
for (uint i = 0; i < n; i++) {
|
|
bool is_converted;
|
|
Item *left_item = left_row->element_index(i);
|
|
Item *right_item = right_row->element_index(i);
|
|
if (left_item->type() == Item::ROW_ITEM &&
|
|
right_item->type() == Item::ROW_ITEM) {
|
|
if (check_row_equality(thd, down_cast<Item_row *>(left_item),
|
|
down_cast<Item_row *>(right_item), cond_equal,
|
|
eq_list, &is_converted))
|
|
return true;
|
|
if (!is_converted) thd->lex->current_query_block()->cond_count++;
|
|
} else {
|
|
if (check_simple_equality(thd, left_item, right_item, nullptr, cond_equal,
|
|
&is_converted))
|
|
return true;
|
|
thd->lex->current_query_block()->cond_count++;
|
|
}
|
|
|
|
if (!is_converted) {
|
|
Item_func_eq *const eq_item = new Item_func_eq(left_item, right_item);
|
|
if (eq_item == nullptr) return true;
|
|
if (eq_item->set_cmp_func()) {
|
|
// Failed to create cmp func -> not only simple equalitities
|
|
return true;
|
|
}
|
|
eq_item->quick_fix_field();
|
|
eq_list->push_back(eq_item);
|
|
}
|
|
}
|
|
*simple_equality = true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Eliminate row equalities and form multiple equalities predicates.
|
|
|
|
This function checks whether the item is a simple equality
|
|
i.e. the one that equates a field with another field or a constant
|
|
(field=field_item or field=constant_item), or, a row equality.
|
|
For a simple equality the function looks for a multiple equality
|
|
in the lists referenced directly or indirectly by cond_equal inferring
|
|
the given simple equality. If it doesn't find any, it builds/expands
|
|
multiple equality that covers the predicate.
|
|
Row equalities are eliminated substituted for conjunctive regular
|
|
equalities which are treated in the same way as original equality
|
|
predicates.
|
|
|
|
@param thd thread handle
|
|
@param item predicate to process
|
|
@param cond_equal multiple equalities that must hold together with the
|
|
predicate
|
|
@param eq_list results of conversions of row equalities that are not
|
|
simple enough to form multiple equalities
|
|
@param[out] equality
|
|
true if re-writing rules have been applied
|
|
false otherwise, i.e.
|
|
if the predicate is not an equality, or
|
|
if the equality is neither a simple nor a row equality
|
|
|
|
@returns false if success, true if error
|
|
|
|
@note If the equality was created by IN->EXISTS, it may be removed later by
|
|
subquery materialization. So we don't mix this possibly temporary equality
|
|
with others; if we let it go into a multiple-equality (Item_equal), then we
|
|
could not remove it later. There is however an exception: if the outer
|
|
expression is a constant, it is safe to leave the equality even in
|
|
materialization; all it can do is preventing NULL/FALSE distinction but if
|
|
such distinction mattered the equality would be in a triggered condition so
|
|
we would not come to this function. And injecting constants is good because
|
|
it makes the materialized table smaller.
|
|
*/
|
|
|
|
static bool check_equality(THD *thd, Item *item, COND_EQUAL *cond_equal,
|
|
List<Item> *eq_list, bool *equality) {
|
|
*equality = false;
|
|
assert(item->is_bool_func());
|
|
Item_func *item_func;
|
|
if (item->type() == Item::FUNC_ITEM &&
|
|
(item_func = down_cast<Item_func *>(item))->functype() ==
|
|
Item_func::EQ_FUNC) {
|
|
Item *left_item = item_func->arguments()[0];
|
|
Item *right_item = item_func->arguments()[1];
|
|
|
|
if (item->created_by_in2exists() && !left_item->const_item())
|
|
return false; // See note above
|
|
|
|
if (left_item->type() == Item::ROW_ITEM &&
|
|
right_item->type() == Item::ROW_ITEM) {
|
|
thd->lex->current_query_block()->cond_count--;
|
|
return check_row_equality(thd, down_cast<Item_row *>(left_item),
|
|
down_cast<Item_row *>(right_item), cond_equal,
|
|
eq_list, equality);
|
|
} else
|
|
return check_simple_equality(thd, left_item, right_item, item, cond_equal,
|
|
equality);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Replace all equality predicates in a condition by multiple equality items.
|
|
|
|
At each 'and' level the function detects items for equality predicates
|
|
and replaces them by a set of multiple equality items of class Item_equal,
|
|
taking into account inherited equalities from upper levels.
|
|
If an equality predicate is used not in a conjunction it's just
|
|
replaced by a multiple equality predicate.
|
|
For each 'and' level the function set a pointer to the inherited
|
|
multiple equalities in the cond_equal field of the associated
|
|
object of the type Item_cond_and.
|
|
The function also traverses the cond tree and for each field reference
|
|
sets a pointer to the multiple equality item containing the field, if there
|
|
is any. If this multiple equality equates fields to a constant the
|
|
function replaces the field reference by the constant in the cases
|
|
when the field is not of a string type or when the field reference is
|
|
just an argument of a comparison predicate.
|
|
The function also determines the maximum number of members in
|
|
equality lists of each Item_cond_and object assigning it to
|
|
thd->lex->current_query_block()->max_equal_elems.
|
|
|
|
@note
|
|
Multiple equality predicate =(f1,..fn) is equivalent to the conjunction of
|
|
f1=f2, .., fn-1=fn. It substitutes any inference from these
|
|
equality predicates that is equivalent to the conjunction.
|
|
Thus, =(a1,a2,a3) can substitute for ((a1=a3) AND (a2=a3) AND (a2=a1)) as
|
|
it is equivalent to ((a1=a2) AND (a2=a3)).
|
|
The function always makes a substitution of all equality predicates occurred
|
|
in a conjunction for a minimal set of multiple equality predicates.
|
|
This set can be considered as a canonical representation of the
|
|
sub-conjunction of the equality predicates.
|
|
E.g. (t1.a=t2.b AND t2.b>5 AND t1.a=t3.c) is replaced by
|
|
(=(t1.a,t2.b,t3.c) AND t2.b>5), not by
|
|
(=(t1.a,t2.b) AND =(t1.a,t3.c) AND t2.b>5);
|
|
while (t1.a=t2.b AND t2.b>5 AND t3.c=t4.d) is replaced by
|
|
(=(t1.a,t2.b) AND =(t3.c=t4.d) AND t2.b>5),
|
|
but if additionally =(t4.d,t2.b) is inherited, it
|
|
will be replaced by (=(t1.a,t2.b,t3.c,t4.d) AND t2.b>5)
|
|
|
|
The function performs the substitution in a recursive descent of
|
|
the condition tree, passing to the next AND level a chain of multiple
|
|
equality predicates which have been built at the upper levels.
|
|
The Item_equal items built at the level are attached to other
|
|
non-equality conjuncts as a sublist. The pointer to the inherited
|
|
multiple equalities is saved in the and condition object (Item_cond_and).
|
|
This chain allows us for any field reference occurrence to easily find a
|
|
multiple equality that must be held for this occurrence.
|
|
For each AND level we do the following:
|
|
- scan it for all equality predicate (=) items
|
|
- join them into disjoint Item_equal() groups
|
|
- process the included OR conditions recursively to do the same for
|
|
lower AND levels.
|
|
|
|
We need to do things in this order as lower AND levels need to know about
|
|
all possible Item_equal objects in upper levels.
|
|
|
|
@param thd thread handle
|
|
@param cond condition(expression) where to make replacement
|
|
@param[out] retcond returned condition
|
|
@param inherited path to all inherited multiple equality items
|
|
@param do_inherit whether or not to inherit equalities from other parts
|
|
of the condition
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
static bool build_equal_items_for_cond(THD *thd, Item *cond, Item **retcond,
|
|
COND_EQUAL *inherited, bool do_inherit) {
|
|
Item_equal *item_equal;
|
|
COND_EQUAL cond_equal;
|
|
cond_equal.upper_levels = inherited;
|
|
assert(cond->is_bool_func());
|
|
if (check_stack_overrun(thd, STACK_MIN_SIZE, nullptr))
|
|
return true; // Fatal error flag is set!
|
|
|
|
const enum Item::Type cond_type = cond->type();
|
|
if (cond_type == Item::COND_ITEM) {
|
|
List<Item> eq_list;
|
|
Item_cond *const item_cond = down_cast<Item_cond *>(cond);
|
|
const bool and_level = item_cond->functype() == Item_func::COND_AND_FUNC;
|
|
List<Item> *args = item_cond->argument_list();
|
|
|
|
List_iterator<Item> li(*args);
|
|
Item *item;
|
|
|
|
if (and_level) {
|
|
/*
|
|
Retrieve all conjuncts of this level detecting the equality
|
|
that are subject to substitution by multiple equality items and
|
|
removing each such predicate from the conjunction after having
|
|
found/created a multiple equality whose inference the predicate is.
|
|
*/
|
|
while ((item = li++)) {
|
|
/*
|
|
PS/SP note: we can safely remove a node from AND-OR
|
|
structure here because it's restored before each
|
|
re-execution of any prepared statement/stored procedure.
|
|
*/
|
|
bool equality;
|
|
if (check_equality(thd, item, &cond_equal, &eq_list, &equality))
|
|
return true;
|
|
if (equality) li.remove();
|
|
}
|
|
|
|
/*
|
|
Check if we eliminated all the predicates of the level, e.g.
|
|
(a=a AND b=b AND a=a).
|
|
*/
|
|
if (!args->elements && !cond_equal.current_level.elements &&
|
|
!eq_list.elements) {
|
|
*retcond = new Item_func_true();
|
|
return *retcond == nullptr;
|
|
}
|
|
|
|
List_iterator_fast<Item_equal> it(cond_equal.current_level);
|
|
while ((item_equal = it++)) {
|
|
if (item_equal->resolve_type(thd)) return true;
|
|
item_equal->update_used_tables();
|
|
thd->lex->current_query_block()->max_equal_elems =
|
|
std::max(thd->lex->current_query_block()->max_equal_elems,
|
|
item_equal->members());
|
|
}
|
|
|
|
Item_cond_and *const item_cond_and = down_cast<Item_cond_and *>(cond);
|
|
item_cond_and->cond_equal = cond_equal;
|
|
inherited = &item_cond_and->cond_equal;
|
|
}
|
|
/*
|
|
Make replacement of equality predicates for lower levels
|
|
of the condition expression.
|
|
*/
|
|
li.rewind();
|
|
while ((item = li++)) {
|
|
Item *new_item;
|
|
if (build_equal_items_for_cond(thd, item, &new_item, inherited,
|
|
do_inherit))
|
|
return true;
|
|
if (new_item != item) {
|
|
/* This replacement happens only for standalone equalities */
|
|
/*
|
|
This is ok with PS/SP as the replacement is done for
|
|
arguments of an AND/OR item, which are restored for each
|
|
execution of PS/SP.
|
|
*/
|
|
li.replace(new_item);
|
|
}
|
|
}
|
|
if (and_level) {
|
|
args->concat(&eq_list);
|
|
args->concat((List<Item> *)&cond_equal.current_level);
|
|
}
|
|
} else if (cond->type() == Item::FUNC_ITEM) {
|
|
List<Item> eq_list;
|
|
/*
|
|
If an equality predicate forms the whole and level,
|
|
we call it standalone equality and it's processed here.
|
|
E.g. in the following where condition
|
|
WHERE a=5 AND (b=5 or a=c)
|
|
(b=5) and (a=c) are standalone equalities.
|
|
In general we can't leave alone standalone eqalities:
|
|
for WHERE a=b AND c=d AND (b=c OR d=5)
|
|
b=c is replaced by =(a,b,c,d).
|
|
*/
|
|
bool equality;
|
|
if (check_equality(thd, cond, &cond_equal, &eq_list, &equality))
|
|
return true;
|
|
if (equality) {
|
|
int n = cond_equal.current_level.elements + eq_list.elements;
|
|
if (n == 0) {
|
|
*retcond = new Item_func_true();
|
|
return *retcond == nullptr;
|
|
} else if (n == 1) {
|
|
if ((item_equal = cond_equal.current_level.pop())) {
|
|
if (item_equal->resolve_type(thd)) return true;
|
|
item_equal->update_used_tables();
|
|
thd->lex->current_query_block()->max_equal_elems =
|
|
std::max(thd->lex->current_query_block()->max_equal_elems,
|
|
item_equal->members());
|
|
*retcond = item_equal;
|
|
return false;
|
|
}
|
|
|
|
*retcond = eq_list.pop();
|
|
return false;
|
|
} else {
|
|
/*
|
|
Here a new AND level must be created. It can happen only
|
|
when a row equality is processed as a standalone predicate.
|
|
*/
|
|
Item_cond_and *and_cond = new Item_cond_and(eq_list);
|
|
if (and_cond == nullptr) return true;
|
|
|
|
and_cond->quick_fix_field();
|
|
List<Item> *args = and_cond->argument_list();
|
|
List_iterator_fast<Item_equal> it(cond_equal.current_level);
|
|
while ((item_equal = it++)) {
|
|
if (item_equal->resolve_type(thd)) return true;
|
|
item_equal->update_used_tables();
|
|
thd->lex->current_query_block()->max_equal_elems =
|
|
std::max(thd->lex->current_query_block()->max_equal_elems,
|
|
item_equal->members());
|
|
}
|
|
and_cond->cond_equal = cond_equal;
|
|
args->concat((List<Item> *)&cond_equal.current_level);
|
|
|
|
*retcond = and_cond;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (do_inherit) {
|
|
/*
|
|
For each field reference in cond, not from equal item predicates,
|
|
set a pointer to the multiple equality it belongs to (if there is any)
|
|
as soon the field is not of a string type or the field reference is
|
|
an argument of a comparison predicate.
|
|
*/
|
|
uchar *is_subst_valid = (uchar *)1;
|
|
cond = cond->compile(&Item::subst_argument_checker, &is_subst_valid,
|
|
&Item::equal_fields_propagator, (uchar *)inherited);
|
|
if (cond == nullptr) return true;
|
|
}
|
|
cond->update_used_tables();
|
|
}
|
|
*retcond = cond;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Build multiple equalities for a WHERE condition and all join conditions that
|
|
inherit these multiple equalities.
|
|
|
|
The function first applies the build_equal_items_for_cond function
|
|
to build all multiple equalities for condition cond utilizing equalities
|
|
referred through the parameter inherited. The extended set of
|
|
equalities is returned in the structure referred by the cond_equal_ref
|
|
parameter. After this the function calls itself recursively for
|
|
all join conditions whose direct references can be found in join_list
|
|
and who inherit directly the multiple equalities just having built.
|
|
|
|
@note
|
|
The join condition used in an outer join operation inherits all equalities
|
|
from the join condition of the embedding join, if there is any, or
|
|
otherwise - from the where condition.
|
|
This fact is not obvious, but presumably can be proved.
|
|
Consider the following query:
|
|
@code
|
|
SELECT * FROM (t1,t2) LEFT JOIN (t3,t4) ON t1.a=t3.a AND t2.a=t4.a
|
|
WHERE t1.a=t2.a;
|
|
@endcode
|
|
If the join condition in the query inherits =(t1.a,t2.a), then we
|
|
can build the multiple equality =(t1.a,t2.a,t3.a,t4.a) that infers
|
|
the equality t3.a=t4.a. Although the join condition
|
|
t1.a=t3.a AND t2.a=t4.a AND t3.a=t4.a is not equivalent to the one
|
|
in the query the latter can be replaced by the former: the new query
|
|
will return the same result set as the original one.
|
|
|
|
Interesting that multiple equality =(t1.a,t2.a,t3.a,t4.a) allows us
|
|
to use t1.a=t3.a AND t3.a=t4.a under the join condition:
|
|
@code
|
|
SELECT * FROM (t1,t2) LEFT JOIN (t3,t4) ON t1.a=t3.a AND t3.a=t4.a
|
|
WHERE t1.a=t2.a
|
|
@endcode
|
|
This query equivalent to:
|
|
@code
|
|
SELECT * FROM (t1 LEFT JOIN (t3,t4) ON t1.a=t3.a AND t3.a=t4.a),t2
|
|
WHERE t1.a=t2.a
|
|
@endcode
|
|
Similarly the original query can be rewritten to the query:
|
|
@code
|
|
SELECT * FROM (t1,t2) LEFT JOIN (t3,t4) ON t2.a=t4.a AND t3.a=t4.a
|
|
WHERE t1.a=t2.a
|
|
@endcode
|
|
that is equivalent to:
|
|
@code
|
|
SELECT * FROM (t2 LEFT JOIN (t3,t4)ON t2.a=t4.a AND t3.a=t4.a), t1
|
|
WHERE t1.a=t2.a
|
|
@endcode
|
|
Thus, applying equalities from the where condition we basically
|
|
can get more freedom in performing join operations.
|
|
Although we don't use this property now, it probably makes sense to use
|
|
it in the future.
|
|
|
|
@param thd Thread handler
|
|
@param cond condition to build the multiple equalities for
|
|
@param[out] retcond Returned condition
|
|
@param inherited path to all inherited multiple equality items
|
|
@param do_inherit whether or not to inherit equalities from other
|
|
parts of the condition
|
|
@param join_list list of join tables that the condition refers to
|
|
@param[out] cond_equal_ref pointer to the structure to place built
|
|
equalities in
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
static bool build_equal_items(THD *thd, Item *cond, Item **retcond,
|
|
COND_EQUAL *inherited, bool do_inherit,
|
|
mem_root_deque<Table_ref *> *join_list,
|
|
COND_EQUAL **cond_equal_ref) {
|
|
COND_EQUAL *cond_equal = nullptr;
|
|
|
|
if (cond) {
|
|
if (build_equal_items_for_cond(thd, cond, &cond, inherited, do_inherit))
|
|
return true;
|
|
cond->update_used_tables();
|
|
// update_used_tables() returns void but can still fail.
|
|
if (thd->is_error()) return true;
|
|
|
|
const enum Item::Type cond_type = cond->type();
|
|
if (cond_type == Item::COND_ITEM &&
|
|
down_cast<Item_cond *>(cond)->functype() == Item_func::COND_AND_FUNC)
|
|
cond_equal = &down_cast<Item_cond_and *>(cond)->cond_equal;
|
|
else if (cond_type == Item::FUNC_ITEM &&
|
|
down_cast<Item_func *>(cond)->functype() ==
|
|
Item_func::MULT_EQUAL_FUNC) {
|
|
cond_equal = new (thd->mem_root) COND_EQUAL;
|
|
if (cond_equal == nullptr) return true;
|
|
cond_equal->current_level.push_back(down_cast<Item_equal *>(cond));
|
|
}
|
|
}
|
|
if (cond_equal) {
|
|
cond_equal->upper_levels = inherited;
|
|
inherited = cond_equal;
|
|
}
|
|
*cond_equal_ref = cond_equal;
|
|
|
|
if (join_list) {
|
|
for (Table_ref *table : *join_list) {
|
|
if (table->join_cond_optim()) {
|
|
mem_root_deque<Table_ref *> *nested_join_list =
|
|
table->nested_join ? &table->nested_join->m_tables : nullptr;
|
|
Item *join_cond;
|
|
if (build_equal_items(thd, table->join_cond_optim(), &join_cond,
|
|
inherited, do_inherit, nested_join_list,
|
|
&table->cond_equal))
|
|
return true;
|
|
table->set_join_cond_optim(join_cond);
|
|
}
|
|
}
|
|
}
|
|
|
|
*retcond = cond;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Compare field items by table order in the execution plan.
|
|
|
|
field1 considered as better than field2 if the table containing
|
|
field1 is accessed earlier than the table containing field2.
|
|
The function finds out what of two fields is better according
|
|
this criteria.
|
|
|
|
@param field1 first field item to compare
|
|
@param field2 second field item to compare
|
|
@param table_join_idx index to tables determining table order
|
|
|
|
@retval
|
|
-1 if field1 is better than field2
|
|
@retval
|
|
1 if field2 is better than field1
|
|
@retval
|
|
0 otherwise
|
|
*/
|
|
|
|
static int compare_fields_by_table_order(Item_field *field1, Item_field *field2,
|
|
JOIN_TAB **table_join_idx) {
|
|
int cmp = 0;
|
|
bool outer_ref = false;
|
|
if (field1->is_outer_reference()) {
|
|
outer_ref = true;
|
|
cmp = -1;
|
|
}
|
|
if (field2->is_outer_reference()) {
|
|
outer_ref = true;
|
|
cmp++;
|
|
}
|
|
if (outer_ref) return cmp;
|
|
|
|
/*
|
|
table_join_idx is NULL if this function was not called from JOIN::optimize()
|
|
but from e.g. mysql_delete() or mysql_update(). In these cases
|
|
there is only one table and both fields belong to it. Example
|
|
condition where this is the case: t1.fld1=t1.fld2
|
|
*/
|
|
if (!table_join_idx) return 0;
|
|
|
|
// Locate JOIN_TABs thanks to table_join_idx, then compare their index.
|
|
cmp = table_join_idx[field1->table_ref->tableno()]->idx() -
|
|
table_join_idx[field2->table_ref->tableno()]->idx();
|
|
return cmp < 0 ? -1 : (cmp ? 1 : 0);
|
|
}
|
|
|
|
/**
|
|
Generate minimal set of simple equalities equivalent to a multiple equality.
|
|
|
|
The function retrieves the fields of the multiple equality item
|
|
item_equal and for each field f:
|
|
- if item_equal contains const it generates the equality f=const_item;
|
|
- otherwise, if f is not the first field, generates the equality
|
|
f=item_equal->get_first().
|
|
All generated equality are added to the cond conjunction.
|
|
|
|
@param thd the session context
|
|
@param cond condition to add the generated equality to
|
|
@param upper_levels structure to access multiple equality of upper levels
|
|
@param item_equal multiple equality to generate simple equality from
|
|
|
|
@note
|
|
Before generating an equality function checks that it has not
|
|
been generated for multiple equalities of the upper levels.
|
|
E.g. for the following where condition
|
|
WHERE a=5 AND ((a=b AND b=c) OR c>4)
|
|
the upper level AND condition will contain =(5,a),
|
|
while the lower level AND condition will contain =(5,a,b,c).
|
|
When splitting =(5,a,b,c) into a separate equality predicates
|
|
we should omit 5=a, as we have it already in the upper level.
|
|
The following where condition gives us a more complicated case:
|
|
WHERE t1.a=t2.b AND t3.c=t4.d AND (t2.b=t3.c OR t4.e>5 ...) AND ...
|
|
Given the tables are accessed in the order t1->t2->t3->t4 for
|
|
the selected query execution plan the lower level multiple
|
|
equality =(t1.a,t2.b,t3.c,t4.d) formally should be converted to
|
|
t1.a=t2.b AND t1.a=t3.c AND t1.a=t4.d. But t1.a=t2.a will be
|
|
generated for the upper level. Also t3.c=t4.d will be generated there.
|
|
So only t1.a=t3.c should be left in the lower level.
|
|
If cond is equal to 0, then not more then one equality is generated
|
|
and a pointer to it is returned as the result of the function.
|
|
|
|
@return
|
|
- The condition with generated simple equalities or
|
|
a pointer to the simple generated equality, if success.
|
|
- 0, otherwise.
|
|
*/
|
|
|
|
static Item *eliminate_item_equal(THD *thd, Item *cond,
|
|
COND_EQUAL *upper_levels,
|
|
Item_equal *item_equal) {
|
|
List<Item> eq_list;
|
|
Item *eq_item = nullptr;
|
|
if (item_equal->const_item() && !item_equal->val_int())
|
|
return new Item_func_false();
|
|
Item *const item_const = item_equal->const_arg();
|
|
auto it = item_equal->get_fields().begin();
|
|
if (!item_const) {
|
|
/*
|
|
If there is a const item, match all field items with the const item,
|
|
otherwise match the second and subsequent field items with the first one:
|
|
*/
|
|
it++;
|
|
}
|
|
while (it != item_equal->get_fields().end()) {
|
|
/*
|
|
Generate an equality of the form:
|
|
item_field = some previous field in item_equal's list.
|
|
|
|
First see if we really need to generate it:
|
|
*/
|
|
Item_field *item_field = &*it++; // Field to generate equality for.
|
|
Item_equal *const upper = item_field->find_item_equal(upper_levels);
|
|
if (upper) // item_field is in this upper equality
|
|
{
|
|
if (item_const && upper->const_arg())
|
|
continue; // Const at both levels, no need to generate at current level
|
|
/*
|
|
If the upper-level multiple equality contains this item, there is no
|
|
need to generate the equality, unless item_field belongs to a
|
|
semi-join nest that is used for Materialization, and refers to tables
|
|
that are outside of the materialized semi-join nest,
|
|
As noted in Item_equal::get_subst_item(), subquery materialization
|
|
does not have this problem.
|
|
*/
|
|
JOIN_TAB *const tab = item_field->field->table->reginfo.join_tab;
|
|
|
|
if (!(tab && sj_is_materialize_strategy(tab->get_sj_strategy()))) {
|
|
Item_field *item_match;
|
|
auto li = item_equal->get_fields().begin();
|
|
while ((item_match = &*li++) != item_field) {
|
|
if (item_match->find_item_equal(upper_levels) == upper)
|
|
break; // (item_match, item_field) is also in upper level equality
|
|
}
|
|
if (item_match != item_field) continue;
|
|
}
|
|
} // ... if (upper).
|
|
|
|
/*
|
|
item_field should be compared with the head of the multiple equality
|
|
list.
|
|
item_field may refer to a table that is within a semijoin materialization
|
|
nest. In that case, the order of the join_tab entries may look like:
|
|
|
|
ot1 ot2 <subquery> ot5 SJM(it3 it4)
|
|
|
|
If we have a multiple equality
|
|
|
|
(ot1.c1, ot2.c2, <subquery>.c it3.c3, it4.c4, ot5.c5),
|
|
|
|
we should generate the following equalities:
|
|
1. ot1.c1 = ot2.c2
|
|
2. ot1.c1 = <subquery>.c
|
|
3. it3.c3 = it4.c4
|
|
4. ot1.c1 = ot5.c5
|
|
|
|
Equalities 1) and 4) are regular equalities between two outer tables.
|
|
Equality 2) is an equality that matches the outer query with a
|
|
materialized temporary table. It is either performed as a lookup
|
|
into the materialized table (SJM-lookup), or as a condition on the
|
|
outer table (SJM-scan).
|
|
Equality 3) is evaluated during semijoin materialization.
|
|
|
|
If there is a const item, match against this one.
|
|
Otherwise, match against the first field item in the multiple equality,
|
|
unless the item is within a materialized semijoin nest, in case it will
|
|
be matched against the first item within the SJM nest.
|
|
@see JOIN::set_prefix_tables()
|
|
@see Item_equal::get_subst_item()
|
|
*/
|
|
|
|
Item *const head =
|
|
item_const ? item_const : item_equal->get_subst_item(item_field);
|
|
if (head == item_field) continue;
|
|
|
|
// we have a pair, can generate 'item_field=head'
|
|
if (eq_item) eq_list.push_back(eq_item);
|
|
|
|
if (head->type() == Item::FIELD_ITEM) {
|
|
// Store away all fields that were considered equal, so that we are able
|
|
// to undo this operation later if we have to. See
|
|
// Item_func::ensure_multi_equality_fields_are_available for more details.
|
|
Item_field *head_field = down_cast<Item_field *>(head);
|
|
head_field->set_item_equal_all_join_nests(item_equal);
|
|
}
|
|
eq_item = new Item_func_eq(item_field, head);
|
|
|
|
if (!eq_item || down_cast<Item_func_eq *>(eq_item)->set_cmp_func())
|
|
return nullptr;
|
|
|
|
eq_item->quick_fix_field();
|
|
if (item_const != nullptr) {
|
|
eq_item->apply_is_true();
|
|
Item::cond_result res;
|
|
if (fold_condition(thd, eq_item, &eq_item, &res)) return nullptr;
|
|
if (res == Item::COND_FALSE) {
|
|
eq_item = new (thd->mem_root) Item_func_false();
|
|
if (eq_item == nullptr) return nullptr;
|
|
return eq_item; // entire AND is false
|
|
} else if (res == Item::COND_TRUE) {
|
|
eq_item = new (thd->mem_root) Item_func_true();
|
|
if (eq_item == nullptr) return nullptr;
|
|
}
|
|
}
|
|
} // ... while ((item_field= it++))
|
|
|
|
if (!cond && !eq_list.head()) {
|
|
if (!eq_item) return new Item_func_true();
|
|
return eq_item;
|
|
}
|
|
|
|
if (eq_item) eq_list.push_back(eq_item);
|
|
if (!cond)
|
|
cond = new Item_cond_and(eq_list);
|
|
else {
|
|
assert(cond->type() == Item::COND_ITEM);
|
|
if (eq_list.elements) ((Item_cond *)cond)->add_at_head(&eq_list);
|
|
}
|
|
|
|
cond->quick_fix_field();
|
|
cond->update_used_tables();
|
|
|
|
return cond;
|
|
}
|
|
|
|
/**
|
|
Substitute every field reference in a condition by the best equal field
|
|
and eliminate all multiple equality predicates.
|
|
|
|
The function retrieves the cond condition and for each encountered
|
|
multiple equality predicate it sorts the field references in it
|
|
according to the order of tables specified by the table_join_idx
|
|
parameter. Then it eliminates the multiple equality predicate by
|
|
replacing it with the conjunction of simple equality predicates
|
|
equating every field from the multiple equality to the first
|
|
field in it, or to the constant, if there is any.
|
|
After this, the function retrieves all other conjuncted
|
|
predicates and substitutes every field reference by the field reference
|
|
to the first equal field or equal constant if there are any.
|
|
|
|
@param thd the session context
|
|
@param cond condition to process
|
|
@param cond_equal multiple equalities to take into consideration
|
|
@param table_join_idx index to tables determining field preference
|
|
|
|
@note
|
|
At the first glance, a full sort of fields in multiple equality
|
|
seems to be an overkill. Yet it's not the case due to possible
|
|
new fields in multiple equality item of lower levels. We want
|
|
the order in them to comply with the order of upper levels.
|
|
|
|
@return
|
|
The transformed condition, or NULL in case of error
|
|
*/
|
|
|
|
Item *substitute_for_best_equal_field(THD *thd, Item *cond,
|
|
COND_EQUAL *cond_equal,
|
|
JOIN_TAB **table_join_idx) {
|
|
assert(cond->is_bool_func());
|
|
if (cond->type() == Item::COND_ITEM) {
|
|
List<Item> *cond_list = ((Item_cond *)cond)->argument_list();
|
|
|
|
bool and_level =
|
|
((Item_cond *)cond)->functype() == Item_func::COND_AND_FUNC;
|
|
if (and_level) {
|
|
cond_equal = &((Item_cond_and *)cond)->cond_equal;
|
|
cond_list->disjoin((List<Item> *)&cond_equal->current_level);
|
|
|
|
List_iterator_fast<Item_equal> it(cond_equal->current_level);
|
|
auto cmp = [table_join_idx](Item_field *f1, Item_field *f2) {
|
|
return compare_fields_by_table_order(f1, f2, table_join_idx);
|
|
};
|
|
Item_equal *item_equal;
|
|
while ((item_equal = it++)) {
|
|
item_equal->sort(cmp);
|
|
}
|
|
}
|
|
|
|
List_iterator<Item> li(*cond_list);
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
Item *new_item = substitute_for_best_equal_field(thd, item, cond_equal,
|
|
table_join_idx);
|
|
if (new_item == nullptr) return nullptr;
|
|
/*
|
|
This works OK with PS/SP re-execution as changes are made to
|
|
the arguments of AND/OR items only
|
|
*/
|
|
if (new_item != item) li.replace(new_item);
|
|
}
|
|
|
|
if (and_level) {
|
|
List_iterator_fast<Item_equal> it(cond_equal->current_level);
|
|
Item_equal *item_equal;
|
|
while ((item_equal = it++)) {
|
|
cond = eliminate_item_equal(thd, cond, cond_equal->upper_levels,
|
|
item_equal);
|
|
if (cond == nullptr) return nullptr;
|
|
// This occurs when eliminate_item_equal() founds that cond is
|
|
// always false and substitutes it with a false value.
|
|
// Due to this, value of item_equal will be 0, so just return it.
|
|
if (cond->type() != Item::COND_ITEM) break;
|
|
}
|
|
}
|
|
if (cond->type() == Item::COND_ITEM &&
|
|
!((Item_cond *)cond)->argument_list()->elements)
|
|
cond = cond->val_bool() ? implicit_cast<Item *>(new Item_func_true())
|
|
: implicit_cast<Item *>(new Item_func_false());
|
|
} else if (cond->type() == Item::FUNC_ITEM &&
|
|
(down_cast<Item_func *>(cond))->functype() ==
|
|
Item_func::MULT_EQUAL_FUNC) {
|
|
Item_equal *item_equal = down_cast<Item_equal *>(cond);
|
|
item_equal->sort([table_join_idx](Item_field *f1, Item_field *f2) {
|
|
return compare_fields_by_table_order(f1, f2, table_join_idx);
|
|
});
|
|
if (cond_equal && cond_equal->current_level.head() == item_equal)
|
|
cond_equal = cond_equal->upper_levels;
|
|
return eliminate_item_equal(thd, nullptr, cond_equal, item_equal);
|
|
} else {
|
|
Item::Replace_equal replace;
|
|
uchar *arg = pointer_cast<uchar *>(&replace);
|
|
if (cond->compile(&Item::replace_equal_field_checker, &arg,
|
|
&Item::replace_equal_field, arg) == nullptr)
|
|
return nullptr;
|
|
}
|
|
return cond;
|
|
}
|
|
|
|
/**
|
|
change field = field to field = const for each found field = const in the
|
|
and_level
|
|
|
|
@param thd Thread handler
|
|
@param save_list saved list of COND_CMP
|
|
@param and_father father of AND op
|
|
@param cond Condition where fields are replaced with constant values
|
|
@param field The field that will be substituted
|
|
@param value The substitution value
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
static bool change_cond_ref_to_const(THD *thd, I_List<COND_CMP> *save_list,
|
|
Item *and_father, Item *cond, Item *field,
|
|
Item *value) {
|
|
assert(cond->real_item()->is_bool_func());
|
|
if (cond->type() == Item::COND_ITEM) {
|
|
Item_cond *const item_cond = down_cast<Item_cond *>(cond);
|
|
bool and_level = item_cond->functype() == Item_func::COND_AND_FUNC;
|
|
List_iterator<Item> li(*item_cond->argument_list());
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
if (change_cond_ref_to_const(thd, save_list, and_level ? cond : item,
|
|
item, field, value))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
if (cond->eq_cmp_result() == Item::COND_OK)
|
|
return false; // Not a boolean function
|
|
|
|
Item_bool_func2 *func = down_cast<Item_bool_func2 *>(cond);
|
|
Item **args = func->arguments();
|
|
Item *left_item = args[0];
|
|
Item *right_item = args[1];
|
|
Item_func::Functype functype = func->functype();
|
|
|
|
if (right_item->eq(field, false) && left_item != value &&
|
|
right_item->cmp_context == field->cmp_context &&
|
|
(left_item->result_type() != STRING_RESULT ||
|
|
value->result_type() != STRING_RESULT ||
|
|
left_item->collation.collation == value->collation.collation)) {
|
|
Item *const clone = value->clone_item();
|
|
if (thd->is_error()) return true;
|
|
|
|
if (clone == nullptr) return false;
|
|
|
|
clone->collation.set(right_item->collation);
|
|
thd->change_item_tree(args + 1, clone);
|
|
func->update_used_tables();
|
|
if ((functype == Item_func::EQ_FUNC || functype == Item_func::EQUAL_FUNC) &&
|
|
and_father != cond && !left_item->const_item()) {
|
|
cond->marker = Item::MARKER_CONST_PROPAG;
|
|
COND_CMP *const cond_cmp = new COND_CMP(and_father, func);
|
|
if (cond_cmp == nullptr) return true;
|
|
|
|
save_list->push_back(cond_cmp);
|
|
}
|
|
if (func->set_cmp_func()) return true;
|
|
} else if (left_item->eq(field, false) && right_item != value &&
|
|
left_item->cmp_context == field->cmp_context &&
|
|
(right_item->result_type() != STRING_RESULT ||
|
|
value->result_type() != STRING_RESULT ||
|
|
right_item->collation.collation == value->collation.collation)) {
|
|
Item *const clone = value->clone_item();
|
|
if (thd->is_error()) return true;
|
|
|
|
if (clone == nullptr) return false;
|
|
|
|
clone->collation.set(left_item->collation);
|
|
thd->change_item_tree(args, clone);
|
|
value = clone;
|
|
func->update_used_tables();
|
|
if ((functype == Item_func::EQ_FUNC || functype == Item_func::EQUAL_FUNC) &&
|
|
and_father != cond && !right_item->const_item()) {
|
|
args[0] = args[1]; // For easy check
|
|
thd->change_item_tree(args + 1, value);
|
|
cond->marker = Item::MARKER_CONST_PROPAG;
|
|
COND_CMP *const cond_cmp = new COND_CMP(and_father, func);
|
|
if (cond_cmp == nullptr) return true;
|
|
|
|
save_list->push_back(cond_cmp);
|
|
}
|
|
if (func->set_cmp_func()) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Propagate constant values in a condition
|
|
|
|
@param thd Thread handler
|
|
@param save_list saved list of COND_CMP
|
|
@param and_father father of AND op
|
|
@param cond Condition for which constant values are propagated
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
static bool propagate_cond_constants(THD *thd, I_List<COND_CMP> *save_list,
|
|
Item *and_father, Item *cond) {
|
|
assert(cond->real_item()->is_bool_func());
|
|
if (cond->type() == Item::COND_ITEM) {
|
|
Item_cond *const item_cond = down_cast<Item_cond *>(cond);
|
|
bool and_level = item_cond->functype() == Item_func::COND_AND_FUNC;
|
|
List_iterator_fast<Item> li(*item_cond->argument_list());
|
|
Item *item;
|
|
I_List<COND_CMP> save;
|
|
while ((item = li++)) {
|
|
if (propagate_cond_constants(thd, &save, and_level ? cond : item, item))
|
|
return true;
|
|
}
|
|
if (and_level) { // Handle other found items
|
|
I_List_iterator<COND_CMP> cond_itr(save);
|
|
COND_CMP *cond_cmp;
|
|
while ((cond_cmp = cond_itr++)) {
|
|
Item **args = cond_cmp->cmp_func->arguments();
|
|
if (!args[0]->const_item() &&
|
|
change_cond_ref_to_const(thd, &save, cond_cmp->and_level,
|
|
cond_cmp->and_level, args[0], args[1]))
|
|
return true;
|
|
}
|
|
}
|
|
} else if (and_father != cond &&
|
|
cond->marker != Item::MARKER_CONST_PROPAG) // In a AND group
|
|
{
|
|
Item_func *func;
|
|
if (cond->type() == Item::FUNC_ITEM &&
|
|
(func = down_cast<Item_func *>(cond)) &&
|
|
(func->functype() == Item_func::EQ_FUNC ||
|
|
func->functype() == Item_func::EQUAL_FUNC)) {
|
|
Item **args = func->arguments();
|
|
const bool left_const = args[0]->const_item();
|
|
const bool right_const = args[1]->const_item();
|
|
if (!(left_const && right_const) &&
|
|
args[0]->result_type() == args[1]->result_type()) {
|
|
if (right_const) {
|
|
Item *item = args[1];
|
|
if (resolve_const_item(thd, &item, args[0])) return true;
|
|
thd->change_item_tree(&args[1], item);
|
|
func->update_used_tables();
|
|
if (change_cond_ref_to_const(thd, save_list, and_father, and_father,
|
|
args[0], args[1]))
|
|
return true;
|
|
} else if (left_const) {
|
|
Item *item = args[0];
|
|
if (resolve_const_item(thd, &item, args[1])) return true;
|
|
thd->change_item_tree(&args[0], item);
|
|
func->update_used_tables();
|
|
if (change_cond_ref_to_const(thd, save_list, and_father, and_father,
|
|
args[1], args[0]))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Assign each nested join structure a bit in nested_join_map.
|
|
|
|
@param join_list List of tables
|
|
@param first_unused Number of first unused bit in nested_join_map before the
|
|
call
|
|
|
|
@note
|
|
This function is called after simplify_joins(), when there are no
|
|
redundant nested joins.
|
|
We cannot have more nested joins in a query block than there are tables,
|
|
so as long as the number of bits in nested_join_map is not less than the
|
|
maximum number of tables in a query block, nested_join_map can never
|
|
overflow.
|
|
|
|
@return
|
|
First unused bit in nested_join_map after the call.
|
|
*/
|
|
|
|
uint build_bitmap_for_nested_joins(mem_root_deque<Table_ref *> *join_list,
|
|
uint first_unused) {
|
|
DBUG_TRACE;
|
|
for (Table_ref *table : *join_list) {
|
|
NESTED_JOIN *nested_join;
|
|
if ((nested_join = table->nested_join)) {
|
|
// We should have a join condition or a semi-join condition or both
|
|
assert((table->join_cond() != nullptr) || table->is_sj_nest());
|
|
|
|
nested_join->nj_map = 0;
|
|
nested_join->nj_total = 0;
|
|
/*
|
|
We only record nested join information for outer join nests.
|
|
Tables belonging in semi-join nests are recorded in the
|
|
embedding outer join nest, if one exists.
|
|
*/
|
|
if (table->join_cond()) {
|
|
assert(first_unused < sizeof(nested_join_map) * 8);
|
|
nested_join->nj_map = (nested_join_map)1 << first_unused++;
|
|
nested_join->nj_total = nested_join->m_tables.size();
|
|
} else if (table->is_sj_nest()) {
|
|
NESTED_JOIN *const outer_nest =
|
|
table->embedding ? table->embedding->nested_join : nullptr;
|
|
/*
|
|
The semi-join nest has already been counted into the table count
|
|
for the outer join nest as one table, so subtract 1 from the
|
|
table count.
|
|
*/
|
|
if (outer_nest)
|
|
outer_nest->nj_total += (nested_join->m_tables.size() - 1);
|
|
} else
|
|
assert(false);
|
|
|
|
first_unused =
|
|
build_bitmap_for_nested_joins(&nested_join->m_tables, first_unused);
|
|
}
|
|
}
|
|
return first_unused;
|
|
}
|
|
|
|
/** Update the dependency map for the tables. */
|
|
|
|
void JOIN::update_depend_map() {
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
for (uint tableno = 0; tableno < tables; tableno++) {
|
|
JOIN_TAB *const tab = best_ref[tableno];
|
|
Index_lookup *const ref = &tab->ref();
|
|
table_map depend_map = 0;
|
|
Item **item = ref->items;
|
|
for (uint i = 0; i < ref->key_parts; i++, item++)
|
|
depend_map |= (*item)->used_tables();
|
|
depend_map &= ~PSEUDO_TABLE_BITS;
|
|
ref->depend_map = depend_map;
|
|
for (JOIN_TAB **tab2 = map2table; depend_map; tab2++, depend_map >>= 1) {
|
|
if (depend_map & 1) ref->depend_map |= (*tab2)->ref().depend_map;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Update the dependency map for the sort order. */
|
|
|
|
void JOIN::update_depend_map(ORDER *order) {
|
|
DBUG_TRACE;
|
|
for (; order; order = order->next) {
|
|
table_map depend_map;
|
|
order->item[0]->update_used_tables();
|
|
order->depend_map = depend_map =
|
|
order->item[0]->used_tables() & ~INNER_TABLE_BIT;
|
|
order->used = 0;
|
|
// Not item_sum(), RAND() and no reference to table outside of sub select
|
|
if (!(order->depend_map & (OUTER_REF_TABLE_BIT | RAND_TABLE_BIT)) &&
|
|
!order->item[0]->has_aggregation()) {
|
|
for (JOIN_TAB **tab = map2table; depend_map; tab++, depend_map >>= 1) {
|
|
if (depend_map & 1) order->depend_map |= (*tab)->ref().depend_map;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Update equalities and keyuse references after semi-join materialization
|
|
strategy is chosen.
|
|
|
|
@details
|
|
For each multiple equality that contains a field that is selected
|
|
from a subquery, and that subquery is executed using a semi-join
|
|
materialization strategy, add the corresponding column in the materialized
|
|
temporary table to the equality.
|
|
For each injected semi-join equality that is not converted to
|
|
multiple equality, replace the reference to the expression selected
|
|
from the subquery with the corresponding column in the temporary table.
|
|
|
|
This is needed to properly reflect the equalities that involve injected
|
|
semi-join equalities when materialization strategy is chosen.
|
|
@see eliminate_item_equal() for how these equalities are used to generate
|
|
correct equality predicates.
|
|
|
|
The MaterializeScan semi-join strategy requires some additional processing:
|
|
All primary tables after the materialized temporary table must be inspected
|
|
for keyuse objects that point to expressions from the subquery tables.
|
|
These references must be replaced with references to corresponding columns
|
|
in the materialized temporary table instead. Those primary tables using
|
|
ref access will thus be made to depend on the materialized temporary table
|
|
instead of the subquery tables.
|
|
|
|
Only the injected semi-join equalities need this treatment, other predicates
|
|
will be handled correctly by the regular item substitution process.
|
|
|
|
@return False if success, true if error
|
|
*/
|
|
|
|
bool JOIN::update_equalities_for_sjm() {
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
List_iterator<Semijoin_mat_exec> sj_it(sjm_exec_list);
|
|
Semijoin_mat_exec *sjm_exec;
|
|
while ((sjm_exec = sj_it++)) {
|
|
Table_ref *const sj_nest = sjm_exec->sj_nest;
|
|
|
|
Item *cond;
|
|
/*
|
|
Conditions involving SJ-inner tables are only to be found in the closest
|
|
nest's condition, which may be an AJ nest, a LEFT JOIN nest, or the
|
|
WHERE clause.
|
|
*/
|
|
if (sj_nest->is_aj_nest())
|
|
cond = sj_nest->join_cond_optim();
|
|
else if (sj_nest->outer_join_nest())
|
|
cond = sj_nest->outer_join_nest()->join_cond_optim();
|
|
else
|
|
cond = where_cond;
|
|
if (!cond) continue;
|
|
|
|
uchar *dummy = nullptr;
|
|
cond = cond->compile(&Item::equality_substitution_analyzer, &dummy,
|
|
&Item::equality_substitution_transformer,
|
|
(uchar *)sj_nest);
|
|
if (cond == nullptr) return true;
|
|
|
|
cond->update_used_tables();
|
|
|
|
// Loop over all primary tables that follow the materialized table
|
|
for (uint j = sjm_exec->mat_table_index + 1; j < primary_tables; j++) {
|
|
JOIN_TAB *const tab = best_ref[j];
|
|
for (Key_use *keyuse = tab->position()->key;
|
|
keyuse && keyuse->table_ref == tab->table_ref &&
|
|
keyuse->key == tab->position()->key->key;
|
|
keyuse++) {
|
|
uint fieldno = 0;
|
|
for (Item *old : sj_nest->nested_join->sj_inner_exprs) {
|
|
if (old->real_item()->eq(keyuse->val->real_item(), false)) {
|
|
/*
|
|
Replace the expression selected from the subquery with the
|
|
corresponding column of the materialized temporary table.
|
|
*/
|
|
keyuse->val = sj_nest->nested_join->sjm.mat_fields[fieldno];
|
|
keyuse->used_tables = keyuse->val->used_tables();
|
|
break;
|
|
}
|
|
fieldno++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Assign set of available (prefix) tables to all tables in query block.
|
|
Also set added tables, ie the tables added in each JOIN_TAB compared to the
|
|
previous JOIN_TAB.
|
|
This function must be called for every query block after the table order
|
|
has been determined.
|
|
*/
|
|
|
|
void JOIN::set_prefix_tables() {
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
assert(!plan_is_const());
|
|
/*
|
|
The const tables are available together with the first non-const table in
|
|
the join order.
|
|
*/
|
|
table_map const initial_tables_map =
|
|
const_table_map | (allow_outer_refs ? OUTER_REF_TABLE_BIT : 0);
|
|
|
|
table_map current_tables_map = initial_tables_map;
|
|
table_map prev_tables_map = (table_map)0;
|
|
table_map saved_tables_map = (table_map)0;
|
|
|
|
JOIN_TAB *last_non_sjm_tab = nullptr; // Track the last non-sjm table
|
|
|
|
for (uint i = const_tables; i < tables; i++) {
|
|
JOIN_TAB *const tab = best_ref[i];
|
|
if (!tab->table()) continue;
|
|
/*
|
|
Tables that are within SJ-Materialization nests cannot have their
|
|
conditions referring to preceding non-const tables.
|
|
- If we're looking at the first SJM table, reset current_tables_map
|
|
to refer to only allowed tables
|
|
@see Item_equal::get_subst_item()
|
|
@see eliminate_item_equal()
|
|
*/
|
|
if (sj_is_materialize_strategy(tab->get_sj_strategy())) {
|
|
const table_map sjm_inner_tables = tab->emb_sj_nest->sj_inner_tables;
|
|
if (!(sjm_inner_tables & current_tables_map)) {
|
|
saved_tables_map = current_tables_map;
|
|
current_tables_map = initial_tables_map;
|
|
prev_tables_map = (table_map)0;
|
|
}
|
|
|
|
current_tables_map |= tab->table_ref->map();
|
|
tab->set_prefix_tables(current_tables_map, prev_tables_map);
|
|
prev_tables_map = current_tables_map;
|
|
|
|
if (!(sjm_inner_tables & ~current_tables_map)) {
|
|
/*
|
|
At the end of a semi-join materialization nest,
|
|
add non-deterministic expressions to the last table of the nest:
|
|
*/
|
|
tab->add_prefix_tables(RAND_TABLE_BIT);
|
|
|
|
// Restore the previous map:
|
|
current_tables_map = saved_tables_map;
|
|
prev_tables_map =
|
|
last_non_sjm_tab ? last_non_sjm_tab->prefix_tables() : (table_map)0;
|
|
}
|
|
} else {
|
|
last_non_sjm_tab = tab;
|
|
current_tables_map |= tab->table_ref->map();
|
|
tab->set_prefix_tables(current_tables_map, prev_tables_map);
|
|
prev_tables_map = current_tables_map;
|
|
}
|
|
}
|
|
/*
|
|
Non-deterministic expressions must be added to the last table's condition.
|
|
It solves problem with queries like SELECT * FROM t1 WHERE rand() > 0.5
|
|
*/
|
|
if (last_non_sjm_tab != nullptr)
|
|
last_non_sjm_tab->add_prefix_tables(RAND_TABLE_BIT);
|
|
}
|
|
|
|
/**
|
|
Calculate best possible join order and initialize the join structure.
|
|
|
|
@return true if success, false if error.
|
|
|
|
The JOIN object is populated with statistics about the query,
|
|
and a plan with table order and access method selection is made.
|
|
|
|
The list of tables to be optimized is taken from query_block->leaf_tables.
|
|
JOIN::where_cond is also used in the optimization.
|
|
As a side-effect, JOIN::keyuse_array is populated with key_use information.
|
|
|
|
Here is an overview of the logic of this function:
|
|
|
|
- Initialize JOIN data structures and setup basic dependencies between tables.
|
|
|
|
- Update dependencies based on join information.
|
|
|
|
- Make key descriptions (update_ref_and_keys()).
|
|
|
|
- Pull out semi-join tables based on table dependencies.
|
|
|
|
- Extract tables with zero or one rows as const tables.
|
|
|
|
- Read contents of const tables, substitute columns from these tables with
|
|
actual data. Also keep track of empty tables vs. one-row tables.
|
|
|
|
- After const table extraction based on row count, more tables may
|
|
have become functionally dependent. Extract these as const tables.
|
|
|
|
- Add new sargable predicates based on retrieved const values.
|
|
|
|
- Calculate number of rows to be retrieved from each table.
|
|
|
|
- Calculate cost of potential semi-join materializations.
|
|
|
|
- Calculate best possible join order based on available statistics.
|
|
|
|
- Fill in remaining information for the generated join order.
|
|
*/
|
|
|
|
bool JOIN::make_join_plan() {
|
|
DBUG_TRACE;
|
|
|
|
SARGABLE_PARAM *sargables = nullptr;
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
|
|
if (init_planner_arrays()) // Create and initialize the arrays
|
|
return true;
|
|
|
|
// Outer join dependencies were initialized above, now complete the analysis.
|
|
if (query_block->outer_join || query_block->is_recursive()) {
|
|
if (propagate_dependencies()) {
|
|
/*
|
|
Catch illegal join order.
|
|
SQL2011 forbids:
|
|
WITH RECURSIVE rec AS (
|
|
... UNION ALL SELECT ... FROM tbl LEFT JOIN rec ON...)c...
|
|
MySQL also forbids the same query with STRAIGHT_JOIN instead of LEFT
|
|
JOIN, because the algorithm of with-recursive imposes that "rec" be
|
|
first in plan, i.e. "tbl" depends on "rec", but STRAIGHT_JOIN imposes
|
|
the opposite dependency.
|
|
*/
|
|
assert(query_block->is_recursive());
|
|
my_error(ER_CTE_RECURSIVE_FORBIDDEN_JOIN_ORDER, MYF(0),
|
|
query_block->recursive_reference->alias);
|
|
return true;
|
|
}
|
|
init_key_dependencies();
|
|
}
|
|
|
|
if (unlikely(trace->is_started()))
|
|
trace_table_dependencies(trace, join_tab, primary_tables);
|
|
|
|
// Build the key access information, which is the basis for ref access.
|
|
if (where_cond || query_block->outer_join) {
|
|
if (update_ref_and_keys(thd, &keyuse_array, join_tab, tables, where_cond,
|
|
~query_block->outer_join, query_block, &sargables))
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
Pull out semi-join tables based on dependencies. Dependencies are valid
|
|
throughout the lifetime of a query, so this operation can be performed
|
|
on the first optimization only.
|
|
*/
|
|
if (!query_block->sj_pullout_done && !query_block->sj_nests.empty() &&
|
|
pull_out_semijoin_tables(this))
|
|
return true;
|
|
|
|
query_block->sj_pullout_done = true;
|
|
const uint sj_nests = query_block->sj_nests.size(); // Changed by pull-out
|
|
|
|
if (!(query_block->active_options() & OPTION_NO_CONST_TABLES)) {
|
|
// Detect tables that are const (0 or 1 row) and read their contents.
|
|
if (extract_const_tables()) return true;
|
|
|
|
// Detect tables that are functionally dependent on const values.
|
|
if (extract_func_dependent_tables()) return true;
|
|
}
|
|
// Possibly able to create more sargable predicates from const rows.
|
|
if (const_tables && sargables) update_sargable_from_const(sargables);
|
|
|
|
// Make a first estimate of the fanout for each table in the query block.
|
|
if (estimate_rowcount()) return true;
|
|
|
|
/*
|
|
Apply join order hints, with the exception of
|
|
JOIN_FIXED_ORDER and STRAIGHT_JOIN.
|
|
*/
|
|
if (query_block->opt_hints_qb &&
|
|
!(query_block->active_options() & SELECT_STRAIGHT_JOIN))
|
|
query_block->opt_hints_qb->apply_join_order_hints(this);
|
|
|
|
if (sj_nests) {
|
|
set_semijoin_embedding();
|
|
query_block->update_semijoin_strategies(thd);
|
|
}
|
|
|
|
if (!plan_is_const()) optimize_keyuse();
|
|
|
|
allow_outer_refs = true;
|
|
|
|
if (sj_nests && optimize_semijoin_nests_for_materialization(this))
|
|
return true;
|
|
|
|
// Choose the table order based on analysis done so far.
|
|
if (Optimize_table_order(thd, this, nullptr).choose_table_order())
|
|
return true;
|
|
|
|
DBUG_EXECUTE_IF("bug13820776_1", thd->killed = THD::KILL_QUERY;);
|
|
if (thd->killed || thd->is_error()) return true;
|
|
|
|
// If this is a subquery, decide between In-to-exists and materialization
|
|
if (query_expression()->item && decide_subquery_strategy()) return true;
|
|
|
|
refine_best_rowcount();
|
|
|
|
positions = nullptr; // But keep best_positions for get_best_combination
|
|
|
|
// Generate an execution plan from the found optimal join order.
|
|
if (get_best_combination()) return true;
|
|
|
|
// Cleanup after update_ref_and_keys has added keys for derived tables.
|
|
if (query_block->materialized_derived_table_count) finalize_derived_keys();
|
|
|
|
// No need for this struct after new JOIN_TAB array is set up.
|
|
best_positions = nullptr;
|
|
|
|
// Some called function may still set error status unnoticed
|
|
if (thd->is_error()) return true;
|
|
|
|
// There is at least one empty const table
|
|
if (const_table_map != found_const_table_map)
|
|
zero_result_cause = "no matching row in const table";
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Initialize scratch arrays for the join order optimization
|
|
|
|
@returns false if success, true if error
|
|
|
|
@note If something fails during initialization, JOIN::cleanup()
|
|
will free anything that has been partially allocated and set up.
|
|
Arrays are created in the execution mem_root, so they will be
|
|
deleted automatically when the mem_root is re-initialized.
|
|
*/
|
|
|
|
bool JOIN::init_planner_arrays() {
|
|
// Up to one extra slot per semi-join nest is needed (if materialized)
|
|
const uint sj_nests = query_block->sj_nests.size();
|
|
const uint table_count = query_block->leaf_table_count;
|
|
|
|
assert(primary_tables == 0 && tables == 0);
|
|
|
|
if (!(join_tab = alloc_jtab_array(thd, table_count))) return true;
|
|
|
|
/*
|
|
We add 2 cells:
|
|
- because planning stage uses 0-termination so needs +1
|
|
- because after get_best_combination, we don't use 0-termination but
|
|
need +2, to host at most 2 tmp sort/group/distinct tables.
|
|
*/
|
|
if (!(best_ref = (JOIN_TAB **)thd->alloc(
|
|
sizeof(JOIN_TAB *) *
|
|
(table_count + sj_nests + 2 + m_windows.elements))))
|
|
return true;
|
|
|
|
// sort/group tmp tables have no map
|
|
if (!(map2table = (JOIN_TAB **)thd->alloc(sizeof(JOIN_TAB *) *
|
|
(table_count + sj_nests))))
|
|
return true;
|
|
|
|
if (!(positions = new (thd->mem_root) POSITION[table_count])) return true;
|
|
|
|
if (!(best_positions = new (thd->mem_root) POSITION[table_count + sj_nests]))
|
|
return true;
|
|
|
|
/*
|
|
Initialize data structures for tables to be joined.
|
|
Initialize dependencies between tables.
|
|
*/
|
|
JOIN_TAB **best_ref_p = best_ref;
|
|
Table_ref *tl = query_block->leaf_tables;
|
|
|
|
for (JOIN_TAB *tab = join_tab; tl; tab++, tl = tl->next_leaf, best_ref_p++) {
|
|
*best_ref_p = tab;
|
|
TABLE *const table = tl->table;
|
|
tab->table_ref = tl;
|
|
tab->set_table(table);
|
|
const int err = tl->fetch_number_of_rows();
|
|
if (err) {
|
|
table->file->print_error(err, MYF(0));
|
|
return true;
|
|
}
|
|
// Initialize the cost model for the table.
|
|
table->init_cost_model(cost_model());
|
|
|
|
all_table_map |= tl->map();
|
|
tab->set_join(this);
|
|
|
|
if (tl->is_updated() || tl->is_deleted()) {
|
|
// As we update or delete rows, we can't read the index
|
|
table->no_keyread = true;
|
|
}
|
|
|
|
tab->dependent = tl->dep_tables; // Initialize table dependencies
|
|
if (query_block->is_recursive()) {
|
|
if (query_block->recursive_reference != tl)
|
|
// Recursive reference must go first
|
|
tab->dependent |= query_block->recursive_reference->map();
|
|
else {
|
|
// Recursive reference mustn't use any index
|
|
table->covering_keys.clear_all();
|
|
table->keys_in_use_for_group_by.clear_all();
|
|
table->keys_in_use_for_order_by.clear_all();
|
|
}
|
|
}
|
|
if (tl->schema_table) table->file->stats.records = 2;
|
|
table->quick_condition_rows = table->file->stats.records;
|
|
|
|
tab->init_join_cond_ref(tl);
|
|
|
|
if (tl->outer_join_nest()) {
|
|
// tab belongs to a nested join, maybe to several embedding joins
|
|
tab->embedding_map = 0;
|
|
for (Table_ref *embedding = tl->embedding; embedding;
|
|
embedding = embedding->embedding) {
|
|
NESTED_JOIN *const nested_join = embedding->nested_join;
|
|
tab->embedding_map |= nested_join->nj_map;
|
|
tab->dependent |= embedding->dep_tables;
|
|
}
|
|
} else if (tab->join_cond()) {
|
|
// tab is the only inner table of an outer join
|
|
tab->embedding_map = 0;
|
|
for (Table_ref *embedding = tl->embedding; embedding;
|
|
embedding = embedding->embedding)
|
|
tab->embedding_map |= embedding->nested_join->nj_map;
|
|
}
|
|
|
|
if (tl->is_derived() && tl->derived_query_expression()->m_lateral_deps)
|
|
has_lateral = true;
|
|
|
|
tables++; // Count number of initialized tables
|
|
}
|
|
|
|
primary_tables = tables;
|
|
*best_ref_p = nullptr; // Last element of array must be NULL
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Propagate dependencies between tables due to outer join relations.
|
|
|
|
@returns false if success, true if error
|
|
|
|
Build transitive closure for relation 'to be dependent on'.
|
|
This will speed up the plan search for many cases with outer joins,
|
|
as well as allow us to catch illegal cross references.
|
|
Warshall's algorithm is used to build the transitive closure.
|
|
As we may restart the outer loop up to 'table_count' times, the
|
|
complexity of the algorithm is O((number of tables)^3).
|
|
However, most of the iterations will be shortcircuited when
|
|
there are no dependencies to propagate.
|
|
*/
|
|
|
|
bool JOIN::propagate_dependencies() {
|
|
for (uint i = 0; i < tables; i++) {
|
|
if (!join_tab[i].dependent) continue;
|
|
|
|
// Add my dependencies to other tables depending on me
|
|
uint j;
|
|
JOIN_TAB *tab;
|
|
for (j = 0, tab = join_tab; j < tables; j++, tab++) {
|
|
if (tab->dependent & join_tab[i].table_ref->map()) {
|
|
const table_map was_dependent = tab->dependent;
|
|
tab->dependent |= join_tab[i].dependent;
|
|
/*
|
|
If we change dependencies for a table we already have
|
|
processed: Redo dependency propagation from this table.
|
|
*/
|
|
if (i > j && tab->dependent != was_dependent) {
|
|
i = j - 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
JOIN_TAB *const tab_end = join_tab + tables;
|
|
for (JOIN_TAB *tab = join_tab; tab < tab_end; tab++) {
|
|
if ((tab->dependent & tab->table_ref->map())) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Extract const tables based on row counts.
|
|
|
|
@returns false if success, true if error
|
|
|
|
This extraction must be done for each execution.
|
|
Tables containing exactly zero or one rows are marked as const, but
|
|
notice the additional constraints checked below.
|
|
Tables that are extracted have their rows read before actual execution
|
|
starts and are placed in the beginning of the join_tab array.
|
|
Thus, they do not take part in join order optimization process,
|
|
which can significantly reduce the optimization time.
|
|
The data read from these tables can also be regarded as "constant"
|
|
throughout query execution, hence the column values can be used for
|
|
additional constant propagation and extraction of const tables based
|
|
on eq-ref properties.
|
|
|
|
The tables are given the type JT_SYSTEM.
|
|
*/
|
|
|
|
bool JOIN::extract_const_tables() {
|
|
enum enum_const_table_extraction {
|
|
extract_no_table = 0,
|
|
extract_empty_table = 1,
|
|
extract_const_table = 2
|
|
};
|
|
|
|
JOIN_TAB *const tab_end = join_tab + tables;
|
|
for (JOIN_TAB *tab = join_tab; tab < tab_end; tab++) {
|
|
TABLE *const table = tab->table();
|
|
Table_ref *const tl = tab->table_ref;
|
|
enum enum_const_table_extraction extract_method = extract_const_table;
|
|
|
|
const bool all_partitions_pruned_away = table->all_partitions_pruned_away;
|
|
|
|
if (tl->outer_join_nest()) {
|
|
/*
|
|
Table belongs to a nested join, no candidate for const table extraction.
|
|
*/
|
|
extract_method = extract_no_table;
|
|
} else if (tl->embedding && tl->embedding->is_sj_or_aj_nest()) {
|
|
/*
|
|
Table belongs to a semi-join.
|
|
We do not currently pull out const tables from semi-join nests.
|
|
*/
|
|
extract_method = extract_no_table;
|
|
} else if (tab->join_cond()) {
|
|
// tab is the only inner table of an outer join, extract empty tables
|
|
extract_method = extract_empty_table;
|
|
}
|
|
switch (extract_method) {
|
|
case extract_no_table:
|
|
break;
|
|
|
|
case extract_empty_table:
|
|
// Extract tables with zero rows, but only if statistics are exact
|
|
if ((table->file->stats.records == 0 || all_partitions_pruned_away) &&
|
|
(table->file->ha_table_flags() & HA_STATS_RECORDS_IS_EXACT))
|
|
mark_const_table(tab, nullptr);
|
|
break;
|
|
|
|
case extract_const_table:
|
|
/*
|
|
Extract tables with zero or one rows, but do not extract tables that
|
|
1. are dependent upon other tables, or
|
|
2. have no exact statistics, or
|
|
3. are full-text searched
|
|
*/
|
|
if ((table->s->system || table->file->stats.records <= 1 ||
|
|
all_partitions_pruned_away) &&
|
|
!tab->dependent && // 1
|
|
(table->file->ha_table_flags() & HA_STATS_RECORDS_IS_EXACT) && // 2
|
|
!tl->is_fulltext_searched()) // 3
|
|
mark_const_table(tab, nullptr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Read const tables (tables matching no more than 1 rows)
|
|
if (!const_tables) return false;
|
|
|
|
for (POSITION *p_pos = positions, *p_end = p_pos + const_tables;
|
|
p_pos < p_end; p_pos++) {
|
|
JOIN_TAB *const tab = p_pos->table;
|
|
const int status = join_read_const_table(tab, p_pos);
|
|
if (status > 0)
|
|
return true;
|
|
else if (status == 0) {
|
|
found_const_table_map |= tab->table_ref->map();
|
|
tab->table_ref->optimized_away = true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Extract const tables based on functional dependencies.
|
|
|
|
@returns false if success, true if error
|
|
|
|
This extraction must be done for each execution.
|
|
|
|
Mark as const the tables that
|
|
- are functionally dependent on constant values, or
|
|
- are inner tables of an outer join and contain exactly zero or one rows
|
|
|
|
Tables that are extracted have their rows read before actual execution
|
|
starts and are placed in the beginning of the join_tab array, just as
|
|
described for JOIN::extract_const_tables().
|
|
|
|
The tables are given the type JT_CONST.
|
|
*/
|
|
|
|
bool JOIN::extract_func_dependent_tables() {
|
|
// loop until no more const tables are found
|
|
bool ref_changed;
|
|
// Tables referenced by others; if they're const the others may be too.
|
|
table_map found_ref;
|
|
do {
|
|
more_const_tables_found:
|
|
ref_changed = false;
|
|
found_ref = 0;
|
|
|
|
// Loop over all tables that are not already determined to be const
|
|
for (JOIN_TAB **pos = best_ref + const_tables; *pos; pos++) {
|
|
JOIN_TAB *const tab = *pos;
|
|
TABLE *const table = tab->table();
|
|
Table_ref *const tl = tab->table_ref;
|
|
/*
|
|
If equi-join condition by a key is null rejecting and after a
|
|
substitution of a const table the key value happens to be null
|
|
then we can state that there are no matches for this equi-join.
|
|
*/
|
|
Key_use *keyuse = tab->keyuse();
|
|
if (keyuse && tab->join_cond() && !tab->embedding_map) {
|
|
/*
|
|
When performing an outer join operation if there are no matching rows
|
|
for the single row of the outer table all the inner tables are to be
|
|
null complemented and thus considered as constant tables.
|
|
Here we apply this consideration to the case of outer join operations
|
|
with a single inner table only because the case with nested tables
|
|
would require a more thorough analysis.
|
|
TODO. Apply single row substitution to null complemented inner tables
|
|
for nested outer join operations.
|
|
*/
|
|
while (keyuse->table_ref == tl) {
|
|
if (!(keyuse->val->used_tables() & ~const_table_map) &&
|
|
keyuse->val->is_null() && keyuse->null_rejecting &&
|
|
(tl->embedding == nullptr ||
|
|
!tl->embedding->is_sj_or_aj_nest())) {
|
|
table->set_null_row();
|
|
table->const_table = true;
|
|
found_const_table_map |= tl->map();
|
|
mark_const_table(tab, keyuse);
|
|
goto more_const_tables_found;
|
|
}
|
|
keyuse++;
|
|
}
|
|
}
|
|
|
|
if (tab->dependent) // If dependent on some table
|
|
{
|
|
// All dependent tables must be const
|
|
if (tab->dependent & ~const_table_map) {
|
|
found_ref |= tab->dependent;
|
|
continue;
|
|
}
|
|
/*
|
|
Mark a dependent table as constant if
|
|
1. it has exactly zero or one rows (it is a system table), and
|
|
2. it is not within a nested outer join, and
|
|
3. it does not have an expensive outer join condition.
|
|
This is because we have to determine whether an outer-joined table
|
|
has a real row or a null-extended row in the optimizer phase.
|
|
We have no possibility to evaluate its join condition at
|
|
execution time, when it is marked as a system table.
|
|
*/
|
|
if (table->file->stats.records <= 1L && // 1
|
|
(table->file->ha_table_flags() & HA_STATS_RECORDS_IS_EXACT) && // 1
|
|
!tl->outer_join_nest() && // 2
|
|
!(tab->join_cond() && tab->join_cond()->cost().IsExpensive())) // 3
|
|
{ // system table
|
|
mark_const_table(tab, nullptr);
|
|
const int status =
|
|
join_read_const_table(tab, positions + const_tables - 1);
|
|
if (status > 0)
|
|
return true;
|
|
else if (status == 0)
|
|
found_const_table_map |= tl->map();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Check if table can be read by key or table only uses const refs
|
|
|
|
if ((keyuse = tab->keyuse())) {
|
|
while (keyuse->table_ref == tl) {
|
|
Key_use *const start_keyuse = keyuse;
|
|
const uint key = keyuse->key;
|
|
tab->keys().set_bit(key); // QQ: remove this ?
|
|
|
|
table_map refs = 0;
|
|
Key_map const_ref, eq_part;
|
|
do {
|
|
if (keyuse->val->type() != Item::NULL_ITEM && !keyuse->optimize) {
|
|
if (!((~found_const_table_map) & keyuse->used_tables))
|
|
const_ref.set_bit(keyuse->keypart);
|
|
else
|
|
refs |= keyuse->used_tables;
|
|
eq_part.set_bit(keyuse->keypart);
|
|
}
|
|
keyuse++;
|
|
} while (keyuse->table_ref == tl && keyuse->key == key);
|
|
|
|
/*
|
|
Extract const tables with proper key dependencies.
|
|
Exclude tables that
|
|
1. are full-text searched, or
|
|
2. are part of nested outer join, or
|
|
3. are part of semi-join, or
|
|
4. have an expensive outer join condition.
|
|
5. are blocked by handler for const table optimize.
|
|
6. are not going to be used, typically because they are streamed
|
|
instead of materialized
|
|
(see Query_expression::can_materialize_directly_into_result()).
|
|
*/
|
|
if (eq_part.is_prefix(table->key_info[key].user_defined_key_parts) &&
|
|
!tl->is_fulltext_searched() && // 1
|
|
!tl->outer_join_nest() && // 2
|
|
!(tl->embedding && tl->embedding->is_sj_or_aj_nest()) && // 3
|
|
!(tab->join_cond() &&
|
|
tab->join_cond()->cost().IsExpensive()) && // 4
|
|
!(table->file->ha_table_flags() & HA_BLOCK_CONST_TABLE) && // 5
|
|
table->is_created()) { // 6
|
|
if (table->key_info[key].flags & HA_NOSAME) {
|
|
if (const_ref == eq_part) { // Found everything for ref.
|
|
ref_changed = true;
|
|
mark_const_table(tab, start_keyuse);
|
|
if (create_ref_for_key(this, tab, start_keyuse,
|
|
found_const_table_map))
|
|
return true;
|
|
const int status =
|
|
join_read_const_table(tab, positions + const_tables - 1);
|
|
if (status > 0)
|
|
return true;
|
|
else if (status == 0)
|
|
found_const_table_map |= tl->map();
|
|
break;
|
|
} else
|
|
found_ref |= refs; // Table is const if all refs are const
|
|
} else if (const_ref == eq_part)
|
|
tab->const_keys.set_bit(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} while
|
|
/*
|
|
A new const table appeared, that is referenced by others, so re-check
|
|
others:
|
|
*/
|
|
((const_table_map & found_ref) && ref_changed);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Update info on indexes that can be used for search lookups as
|
|
reading const tables may has added new sargable predicates.
|
|
*/
|
|
|
|
void JOIN::update_sargable_from_const(SARGABLE_PARAM *sargables) {
|
|
for (; sargables->field; sargables++) {
|
|
Field *const field = sargables->field;
|
|
JOIN_TAB *const tab = field->table->reginfo.join_tab;
|
|
Key_map possible_keys = field->key_start;
|
|
possible_keys.intersect(field->table->keys_in_use_for_query);
|
|
bool is_const = true;
|
|
for (uint j = 0; j < sargables->num_values; j++)
|
|
is_const &= sargables->arg_value[j]->const_item();
|
|
if (is_const) {
|
|
tab->const_keys.merge(possible_keys);
|
|
tab->keys().merge(possible_keys);
|
|
}
|
|
}
|
|
}
|
|
|
|
double find_worst_seeks(const TABLE *table, double num_rows,
|
|
double table_scan_cost) {
|
|
/*
|
|
Set a max value for the cost of seek operations we can expect
|
|
when using key lookup. This can't be too high as otherwise we
|
|
are likely to use table scan.
|
|
*/
|
|
const double worst_seeks =
|
|
min(table->file->worst_seek_times(num_rows / 10), table_scan_cost * 3);
|
|
const double min_worst_seek = table->file->worst_seek_times(2.0);
|
|
return std::max(worst_seeks, min_worst_seek); // Fix for small tables
|
|
}
|
|
|
|
/**
|
|
Estimate the number of matched rows for each joined table.
|
|
Set up range scan for tables that have proper predicates.
|
|
Eliminate tables that have filter conditions that are always false based on
|
|
analysis performed in resolver phase or analysis of range scan predicates.
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
bool JOIN::estimate_rowcount() {
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
const Opt_trace_array trace_records(trace, "rows_estimation");
|
|
|
|
JOIN_TAB *const tab_end = join_tab + tables;
|
|
for (JOIN_TAB *tab = join_tab; tab < tab_end; tab++) {
|
|
Opt_trace_object trace_table(trace);
|
|
trace_table.add_utf8_table(tab->table_ref);
|
|
if (tab->type() == JT_SYSTEM || tab->type() == JT_CONST) {
|
|
trace_table.add("rows", 1)
|
|
.add("cost", 1)
|
|
.add_alnum("table_type",
|
|
(tab->type() == JT_SYSTEM) ? "system" : "const")
|
|
.add("empty", tab->table()->has_null_row());
|
|
|
|
// Only one matching row and one block to read
|
|
tab->set_records(tab->found_records = 1);
|
|
tab->worst_seeks = tab->table()->file->worst_seek_times(1.0);
|
|
tab->read_time = tab->worst_seeks;
|
|
continue;
|
|
}
|
|
// Approximate number of found rows and cost to read them
|
|
tab->set_records(tab->found_records = tab->table()->file->stats.records);
|
|
const Cost_estimate table_scan_time = tab->table()->file->table_scan_cost();
|
|
tab->read_time = table_scan_time.total_cost();
|
|
|
|
tab->worst_seeks =
|
|
find_worst_seeks(tab->table(), tab->found_records, tab->read_time);
|
|
|
|
/*
|
|
Add to tab->const_keys the indexes for which all group fields or
|
|
all select distinct fields participate in one index.
|
|
Add to tab->skip_scan_keys indexes which can be used for skip
|
|
scan access if no aggregates are present.
|
|
*/
|
|
add_loose_index_scan_and_skip_scan_keys(this, tab);
|
|
|
|
// Perform range analysis if the table has keys that can be used.
|
|
Table_ref *const tl = tab->table_ref;
|
|
Item *condition = nullptr;
|
|
/*
|
|
For an inner table of an outer join, the join condition is either
|
|
attached to the actual table, or to the embedding join nest.
|
|
For tables that are inner-joined or semi-joined, the join condition
|
|
is taken from the WHERE condition.
|
|
*/
|
|
if (tl->is_inner_table_of_outer_join()) {
|
|
for (Table_ref *t = tl; t != nullptr; t = t->embedding) {
|
|
if (t->join_cond() != nullptr) {
|
|
condition = t->join_cond();
|
|
break;
|
|
}
|
|
}
|
|
assert(condition != nullptr);
|
|
} else {
|
|
condition = where_cond;
|
|
}
|
|
bool always_false_cond = false, range_analysis_done = false;
|
|
if (!tab->const_keys.is_clear_all() ||
|
|
!tab->skip_scan_keys.is_clear_all()) {
|
|
/*
|
|
This call fills tab->range_scan() with the best range access method
|
|
possible for this table, and only if it's better than table scan.
|
|
It also fills tab->needed_reg.
|
|
*/
|
|
const ha_rows records =
|
|
get_quick_record_count(thd, tab, row_limit, condition);
|
|
|
|
if (records == 0 && thd->is_error()) return true;
|
|
if (records == 0 && tab->table()->reginfo.impossible_range)
|
|
always_false_cond = true;
|
|
if (records != HA_POS_ERROR) {
|
|
tab->found_records = records;
|
|
tab->read_time =
|
|
tab->range_scan() != nullptr ? tab->range_scan()->cost() : 0.0;
|
|
}
|
|
range_analysis_done = true;
|
|
} else if (tab->join_cond() != nullptr && tab->join_cond()->const_item() &&
|
|
tab->join_cond()->val_int() == 0) {
|
|
always_false_cond = true;
|
|
}
|
|
|
|
/*
|
|
Check for "always false" and mark table as "const".
|
|
Exclude outer-joined tables unless the table is the single outer-joined
|
|
table in the query block (this also eliminates tables inside
|
|
outer-joined derived tables).
|
|
Exclude semi-joined and anti-joined tables (only those tables that are
|
|
functionally dependent can be marked "const", and subsequently pulled
|
|
out of their semi-join nests).
|
|
*/
|
|
if (always_false_cond &&
|
|
(!tl->is_inner_table_of_outer_join() || tl->embedding == nullptr) &&
|
|
(!(tl->embedding != nullptr && tl->embedding->is_sj_or_aj_nest()))) {
|
|
/*
|
|
Always false WHERE condition or (outer) join condition.
|
|
In case of outer join, mark that one empty NULL row is matched.
|
|
In case of WHERE, don't set found_const_table_map to get the
|
|
caller to abort with a zero row result.
|
|
*/
|
|
mark_const_table(tab, nullptr);
|
|
tab->set_type(JT_CONST); // Override setting made in mark_const_table()
|
|
if (tab->join_cond() != nullptr) {
|
|
// Generate an empty row
|
|
trace_table.add("returning_empty_null_row", true)
|
|
.add_alnum("cause", "always_false_outer_join_condition");
|
|
found_const_table_map |= tl->map();
|
|
tab->table()->set_null_row(); // All fields are NULL
|
|
} else {
|
|
trace_table.add("rows", 0).add_alnum("cause",
|
|
"impossible_where_condition");
|
|
}
|
|
} else if (!range_analysis_done) {
|
|
Opt_trace_object(trace, "table_scan")
|
|
.add("rows", tab->found_records)
|
|
.add("cost", tab->read_time);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Set semi-join embedding join nest pointers.
|
|
|
|
Set pointer to embedding semi-join nest for all semi-joined tables.
|
|
This is the closest semi-join or anti-join nest.
|
|
Note that this must be done for every table inside all semi-join nests,
|
|
even for tables within outer join nests embedded in semi-join nests.
|
|
A table can never be part of multiple semi-join nests, hence no
|
|
ambiguities can ever occur.
|
|
Note also that the pointer is not set for Table_ref objects that
|
|
are outer join nests within semi-join nests.
|
|
*/
|
|
|
|
void JOIN::set_semijoin_embedding() {
|
|
assert(!query_block->sj_nests.empty());
|
|
|
|
JOIN_TAB *const tab_end = join_tab + primary_tables;
|
|
|
|
for (JOIN_TAB *tab = join_tab; tab < tab_end; tab++) {
|
|
tab->emb_sj_nest = nullptr;
|
|
for (Table_ref *tl = tab->table_ref; tl->embedding; tl = tl->embedding) {
|
|
if (tl->embedding->is_sj_or_aj_nest()) {
|
|
assert(!tab->emb_sj_nest);
|
|
tab->emb_sj_nest = tl->embedding;
|
|
// Let the up-walk continue, to assert there's no AJ/SJ nest above.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Check if semijoin's compared types allow materialization.
|
|
|
|
@param[in,out] sj_nest Semi-join nest containing information about correlated
|
|
expressions. Set nested_join->sjm.scan_allowed to true if
|
|
MaterializeScan strategy allowed. Set nested_join->sjm.lookup_allowed
|
|
to true if MaterializeLookup strategy allowed
|
|
|
|
@details
|
|
This is a temporary fix for BUG#36752.
|
|
|
|
There are two subquery materialization strategies for semijoin:
|
|
|
|
1. Materialize and do index lookups in the materialized table. See
|
|
BUG#36752 for description of restrictions we need to put on the
|
|
compared expressions.
|
|
|
|
In addition, since indexes are not supported for BLOB columns,
|
|
this strategy can not be used if any of the columns in the
|
|
materialized table will be BLOB/GEOMETRY columns. (Note that
|
|
also columns for non-BLOB values that may be greater in size
|
|
than CONVERT_IF_BIGGER_TO_BLOB, will be represented as BLOB
|
|
columns.)
|
|
|
|
2. Materialize and then do a full scan of the materialized table.
|
|
The same criteria as for MaterializeLookup are applied, except that
|
|
BLOB/GEOMETRY columns are allowed.
|
|
*/
|
|
|
|
static void semijoin_types_allow_materialization(Table_ref *sj_nest) {
|
|
DBUG_TRACE;
|
|
|
|
assert(sj_nest->nested_join->sj_outer_exprs.size() ==
|
|
sj_nest->nested_join->sj_inner_exprs.size());
|
|
|
|
if (sj_nest->nested_join->sj_outer_exprs.size() > MAX_REF_PARTS ||
|
|
sj_nest->nested_join->sj_outer_exprs.size() == 0) {
|
|
// building an index is impossible
|
|
sj_nest->nested_join->sjm.scan_allowed = false;
|
|
sj_nest->nested_join->sjm.lookup_allowed = false;
|
|
return;
|
|
}
|
|
|
|
sj_nest->nested_join->sjm.scan_allowed = true;
|
|
sj_nest->nested_join->sjm.lookup_allowed = true;
|
|
|
|
bool blobs_involved = false;
|
|
uint total_lookup_index_length = 0;
|
|
uint max_key_length, max_key_part_length, max_key_parts;
|
|
/*
|
|
Maximum lengths for keys and key parts that are supported by
|
|
the temporary table storage engine(s).
|
|
*/
|
|
get_max_key_and_part_length(&max_key_length, &max_key_part_length,
|
|
&max_key_parts);
|
|
auto it1 = sj_nest->nested_join->sj_outer_exprs.begin();
|
|
auto it2 = sj_nest->nested_join->sj_inner_exprs.begin();
|
|
while (it1 != sj_nest->nested_join->sj_outer_exprs.end() &&
|
|
it2 != sj_nest->nested_join->sj_inner_exprs.end()) {
|
|
Item *outer = *it1++;
|
|
Item *inner = *it2++;
|
|
assert(outer->real_item() && inner->real_item());
|
|
if (!types_allow_materialization(outer, inner)) {
|
|
sj_nest->nested_join->sjm.scan_allowed = false;
|
|
sj_nest->nested_join->sjm.lookup_allowed = false;
|
|
return;
|
|
}
|
|
blobs_involved |= inner->is_blob_field();
|
|
|
|
// Calculate the index length of materialized table
|
|
const uint lookup_index_length = get_key_length_tmp_table(inner);
|
|
if (lookup_index_length > max_key_part_length)
|
|
sj_nest->nested_join->sjm.lookup_allowed = false;
|
|
total_lookup_index_length += lookup_index_length;
|
|
}
|
|
if (total_lookup_index_length > max_key_length)
|
|
sj_nest->nested_join->sjm.lookup_allowed = false;
|
|
|
|
if (blobs_involved) sj_nest->nested_join->sjm.lookup_allowed = false;
|
|
|
|
DBUG_PRINT("info", ("semijoin_types_allow_materialization: ok, allowed"));
|
|
}
|
|
|
|
/**
|
|
Index dive can be skipped if the following conditions are satisfied:
|
|
F1) For a single table query:
|
|
a) FORCE INDEX applies to a single index.
|
|
b) No subquery is present.
|
|
c) Fulltext Index is not involved.
|
|
d) No GROUP-BY or DISTINCT clause.
|
|
e.I) No ORDER-BY clause or
|
|
e.II) The given index can provide the order.
|
|
|
|
F2) Not applicable to multi-table query.
|
|
|
|
@param tab JOIN_TAB object.
|
|
@param thd THD object.
|
|
*/
|
|
|
|
static bool check_skip_records_in_range_qualification(JOIN_TAB *tab, THD *thd) {
|
|
Query_block *select = thd->lex->current_query_block();
|
|
TABLE *table = tab->table();
|
|
|
|
if ((!table->force_index ||
|
|
table->keys_in_use_for_query.bits_set() != 1) || // F1.a
|
|
!select->parent_lex->is_single_level_stmt() || // F1.b
|
|
select->has_ft_funcs() || // F1.c
|
|
(select->is_grouped() || select->is_distinct()) || // F1.d
|
|
select->m_current_table_nest->size() != 1) // F2
|
|
return false;
|
|
|
|
/*
|
|
Index dive is needed to get accurate cost from storage engine. When all
|
|
above criteria is met, there are 2 use for the cost. Row access and sort.
|
|
|
|
F1.e.I) If there is no ORDER BY then getting accurate cost is not needed as
|
|
row access is enforced by force index.
|
|
|
|
F1.e.II) If there is an ORDER BY and the chosen index (enforced by FORCE
|
|
INDEX) for row access can provide order then the cost is not really used.
|
|
Hence accurate cost calculation is not needed.
|
|
*/
|
|
|
|
// F1.e.I
|
|
if (!select->is_ordered()) return true;
|
|
|
|
int idx = table->keys_in_use_for_query.get_first_set();
|
|
uint used_key_parts;
|
|
bool skip_quick;
|
|
ORDER_with_src order_src(select->order_list.first, ESC_ORDER_BY);
|
|
int key_order = test_if_order_by_key(&order_src, table, idx, &used_key_parts,
|
|
&skip_quick);
|
|
// Condition F1.e.II
|
|
return key_order != 0;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Create JOIN_TABS, make a guess about the table types,
|
|
Approximate how many records will be used in each table
|
|
*****************************************************************************/
|
|
|
|
/**
|
|
Returns estimated number of rows that could be fetched by given
|
|
access method.
|
|
|
|
The function calls the range optimizer to estimate the cost of the
|
|
cheapest QUICK_* index access method to scan one or several of the
|
|
'keys' using the conditions 'select->cond'. The range optimizer
|
|
compares several different types of 'quick select' methods (range
|
|
scan, index merge, loose index scan) and selects the cheapest one.
|
|
|
|
If the best index access method is cheaper than a table- and an index
|
|
scan, then the range optimizer also constructs the corresponding
|
|
QUICK_* object and assigns it to select->quick. In most cases this
|
|
is the QUICK_* object used at later (optimization and execution)
|
|
phases.
|
|
|
|
@param thd Session that runs the query.
|
|
@param tab JOIN_TAB of source table.
|
|
@param limit maximum number of rows to select.
|
|
@param condition the condition to be used for the range check,
|
|
|
|
@note
|
|
In case of valid range, a RowIterator object will be constructed and
|
|
saved in select->quick.
|
|
|
|
@return Estimated number of result rows selected from 'tab'.
|
|
|
|
@retval HA_POS_ERROR For derived tables/views or if an error occur.
|
|
@retval 0 If impossible query (i.e. certainly no rows will be
|
|
selected.)
|
|
*/
|
|
static ha_rows get_quick_record_count(THD *thd, JOIN_TAB *tab, ha_rows limit,
|
|
Item *condition) {
|
|
DBUG_TRACE;
|
|
uchar buff[STACK_BUFF_ALLOC];
|
|
if (check_stack_overrun(thd, STACK_MIN_SIZE, buff))
|
|
return 0; // Fatal error flag is set
|
|
Table_ref *const tl = tab->table_ref;
|
|
tab->set_skip_records_in_range(
|
|
check_skip_records_in_range_qualification(tab, thd));
|
|
|
|
// Derived tables aren't filled yet, so no stats are available.
|
|
if (!tl->uses_materialization()) {
|
|
AccessPath *range_scan;
|
|
Key_map keys_to_use = tab->const_keys;
|
|
keys_to_use.merge(tab->skip_scan_keys);
|
|
MEM_ROOT temp_mem_root(key_memory_test_quick_select_exec,
|
|
thd->variables.range_alloc_block_size);
|
|
const int error = test_quick_select(
|
|
thd, thd->mem_root, &temp_mem_root, keys_to_use, 0,
|
|
0, // empty table_map
|
|
limit,
|
|
false, // don't force quick range
|
|
ORDER_NOT_RELEVANT, tab->table(), tab->skip_records_in_range(),
|
|
condition, &tab->needed_reg, tab->table()->force_index,
|
|
tab->join()->query_block, &range_scan);
|
|
tab->set_range_scan(range_scan);
|
|
|
|
if (error == 1) return range_scan->num_output_rows();
|
|
if (error == -1) {
|
|
tl->table->reginfo.impossible_range = true;
|
|
return 0;
|
|
}
|
|
DBUG_PRINT("warning", ("Couldn't use record count on const keypart"));
|
|
} else if (tl->is_table_function() || tl->materializable_is_const()) {
|
|
tl->fetch_number_of_rows();
|
|
return tl->table->file->stats.records;
|
|
}
|
|
return HA_POS_ERROR;
|
|
}
|
|
|
|
/*
|
|
Get estimated record length for semi-join materialization temptable
|
|
|
|
SYNOPSIS
|
|
get_tmp_table_rec_length()
|
|
items IN subquery's select list.
|
|
|
|
DESCRIPTION
|
|
Calculate estimated record length for semi-join materialization
|
|
temptable. It's an estimate because we don't follow every bit of
|
|
create_tmp_table()'s logic. This isn't necessary as the return value of
|
|
this function is used only for cost calculations.
|
|
|
|
RETURN
|
|
Length of the temptable record, in bytes
|
|
*/
|
|
|
|
static uint get_tmp_table_rec_length(const mem_root_deque<Item *> &items) {
|
|
uint len = 0;
|
|
for (Item *item : VisibleFields(items)) {
|
|
switch (item->result_type()) {
|
|
case REAL_RESULT:
|
|
len += sizeof(double);
|
|
break;
|
|
case INT_RESULT:
|
|
if (item->max_length >= (MY_INT32_NUM_DECIMAL_DIGITS - 1))
|
|
len += 8;
|
|
else
|
|
len += 4;
|
|
break;
|
|
case STRING_RESULT:
|
|
/* DATE/TIME and GEOMETRY fields have STRING_RESULT result type. */
|
|
if (item->is_temporal() || item->data_type() == MYSQL_TYPE_GEOMETRY)
|
|
len += 8;
|
|
else
|
|
len += item->max_length;
|
|
break;
|
|
case DECIMAL_RESULT:
|
|
len += 10;
|
|
break;
|
|
case ROW_RESULT:
|
|
default:
|
|
assert(0); /* purecov: deadcode */
|
|
break;
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
/**
|
|
Writes to the optimizer trace information about dependencies between
|
|
tables.
|
|
@param trace optimizer trace
|
|
@param join_tabs all JOIN_TABs of the join
|
|
@param table_count how many JOIN_TABs in the 'join_tabs' array
|
|
*/
|
|
static void trace_table_dependencies(Opt_trace_context *trace,
|
|
JOIN_TAB *join_tabs, uint table_count) {
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
const Opt_trace_array trace_dep(trace, "table_dependencies");
|
|
for (uint i = 0; i < table_count; i++) {
|
|
Table_ref *table_ref = join_tabs[i].table_ref;
|
|
Opt_trace_object trace_one_table(trace);
|
|
trace_one_table.add_utf8_table(table_ref).add(
|
|
"row_may_be_null", table_ref->table->is_nullable());
|
|
const table_map map = table_ref->map();
|
|
assert(map < (1ULL << table_count));
|
|
for (uint j = 0; j < table_count; j++) {
|
|
if (map & (1ULL << j)) {
|
|
trace_one_table.add("map_bit", j);
|
|
break;
|
|
}
|
|
}
|
|
Opt_trace_array depends_on(trace, "depends_on_map_bits");
|
|
static_assert(sizeof(table_ref->map()) <= 64,
|
|
"RAND_TABLE_BIT may be in join_tabs[i].dependent, so we test "
|
|
"all 64 bits.");
|
|
for (uint j = 0; j < 64; j++) {
|
|
if (join_tabs[i].dependent & (1ULL << j)) depends_on.add(j);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Add to join_tab[i]->condition() "table.field IS NOT NULL" conditions
|
|
we've inferred from ref/eq_ref access performed.
|
|
|
|
This function is a part of "Early NULL-values filtering for ref access"
|
|
optimization.
|
|
|
|
Example of this optimization:
|
|
For query SELECT * FROM t1,t2 WHERE t2.key=t1.field @n
|
|
and plan " any-access(t1), ref(t2.key=t1.field) " @n
|
|
add "t1.field IS NOT NULL" to t1's table condition. @n
|
|
|
|
Description of the optimization:
|
|
|
|
We look through equalities chosen to perform ref/eq_ref access,
|
|
pick equalities that have form "tbl.part_of_key = othertbl.field"
|
|
(where othertbl is a non-const table and othertbl.field may be NULL)
|
|
and add them to conditions on corresponding tables (othertbl in this
|
|
example).
|
|
|
|
Exception from that is the case when referred_tab->join != join.
|
|
I.e. don't add NOT NULL constraints from any embedded subquery.
|
|
Consider this query:
|
|
@code
|
|
SELECT A.f2 FROM t1 LEFT JOIN t2 A ON A.f2 = f1
|
|
WHERE A.f3=(SELECT MIN(f3) FROM t2 C WHERE A.f4 = C.f4) OR A.f3 IS NULL;
|
|
@endcode
|
|
Here condition A.f3 IS NOT NULL is going to be added to the WHERE
|
|
condition of the embedding query.
|
|
Another example:
|
|
SELECT * FROM t10, t11 WHERE (t10.a < 10 OR t10.a IS NULL)
|
|
AND t11.b <=> t10.b AND (t11.a = (SELECT MAX(a) FROM t12
|
|
WHERE t12.b = t10.a ));
|
|
Here condition t10.a IS NOT NULL is going to be added.
|
|
In both cases addition of NOT NULL condition will erroneously reject
|
|
some rows of the result set.
|
|
referred_tab->join != join constraint would disallow such additions.
|
|
|
|
This optimization doesn't affect the choices that ref, range, or join
|
|
optimizer make. This was intentional because this was added after 4.1
|
|
was GA.
|
|
|
|
Implementation overview
|
|
1. update_ref_and_keys() accumulates info about null-rejecting
|
|
predicates in in Key_field::null_rejecting
|
|
1.1 add_key_part saves these to Key_use.
|
|
2. create_ref_for_key copies them to Index_lookup.
|
|
3. add_not_null_conds adds "x IS NOT NULL" to join_tab->m_condition of
|
|
appropriate JOIN_TAB members.
|
|
|
|
@returns false on success, true on error
|
|
*/
|
|
|
|
static bool add_not_null_conds(JOIN *join) {
|
|
DBUG_TRACE;
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(join);
|
|
for (uint i = join->const_tables; i < join->tables; i++) {
|
|
JOIN_TAB *const tab = join->best_ref[i];
|
|
if ((tab->type() != JT_REF && tab->type() != JT_EQ_REF &&
|
|
tab->type() != JT_REF_OR_NULL) ||
|
|
tab->table()->is_nullable()) {
|
|
continue;
|
|
}
|
|
for (uint keypart = 0; keypart < tab->ref().key_parts; keypart++) {
|
|
if ((tab->ref().null_rejecting & ((key_part_map)1 << keypart)) == 0) {
|
|
continue;
|
|
}
|
|
Item *const item = tab->ref().items[keypart]->real_item();
|
|
if (item->type() != Item::FIELD_ITEM || !item->is_nullable()) continue;
|
|
Item_field *const not_null_item = down_cast<Item_field *>(item);
|
|
JOIN_TAB *referred_tab = not_null_item->field->table->reginfo.join_tab;
|
|
/*
|
|
For UPDATE queries such as:
|
|
UPDATE t1 SET t1.f2=(SELECT MAX(t2.f4) FROM t2 WHERE t2.f3=t1.f1);
|
|
not_null_item is the t1.f1, but it's referred_tab is 0.
|
|
*/
|
|
if (referred_tab == nullptr || referred_tab->join() != join) continue;
|
|
/* Skip if we already have a 'not null' predicate for 'item' */
|
|
if (has_not_null_predicate(referred_tab->condition(), not_null_item))
|
|
continue;
|
|
Item *notnull = new Item_func_isnotnull(not_null_item);
|
|
if (notnull == nullptr) return true;
|
|
/*
|
|
We need to do full fix_fields() call here in order to have correct
|
|
notnull->const_item(). This is needed e.g. by test_quick_select
|
|
when it is called from make_join_query_block after this function is
|
|
called.
|
|
*/
|
|
if (notnull->fix_fields(join->thd, ¬null)) return true;
|
|
DBUG_EXECUTE("where",
|
|
print_where(join->thd, notnull, referred_tab->table()->alias,
|
|
QT_ORDINARY););
|
|
referred_tab->and_with_condition(notnull);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Check all existing AND'ed predicates in 'cond' for an existing
|
|
'is not null 'not_null_item''-predicate.
|
|
|
|
A condition consisting of multiple AND'ed terms is recursively
|
|
decomposed in the search for the specified not null predicate.
|
|
|
|
@param cond Condition to be checked.
|
|
@param not_null_item The item in: 'is not null 'item'' to search for
|
|
|
|
@return true if 'is not null 'not_null_item'' is a predicate
|
|
in the specified 'cond'.
|
|
*/
|
|
static bool has_not_null_predicate(Item *cond, Item_field *not_null_item) {
|
|
if (cond == nullptr) return false;
|
|
if (cond->type() == Item::FUNC_ITEM) {
|
|
Item_func *item_func = down_cast<Item_func *>(cond);
|
|
const Item_func::Functype func_type = item_func->functype();
|
|
return (func_type == Item_func::ISNOTNULL_FUNC &&
|
|
item_func->key_item() == not_null_item);
|
|
} else if (cond->type() == Item::COND_ITEM) {
|
|
Item_cond *item_cond = down_cast<Item_cond *>(cond);
|
|
if (item_cond->functype() == Item_func::COND_AND_FUNC) {
|
|
List_iterator<Item> li(*item_cond->argument_list());
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
if (has_not_null_predicate(item, not_null_item)) return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Check if given expression only uses fields covered by index @a keyno in the
|
|
table tbl. The expression can use any fields in any other tables.
|
|
|
|
The expression is guaranteed not to be AND or OR - those constructs are
|
|
handled outside of this function.
|
|
|
|
Restrict some function types from being pushed down to storage engine:
|
|
a) Don't push down the triggered conditions with exception for
|
|
IS_NOT_NULL_COMPL trigger condition since the NULL-complemented rows are added
|
|
at a later stage in the iterators, so we won't see NULL-complemented rows when
|
|
evaluating it as an index condition. Nested outer joins execution code may
|
|
need to evaluate a condition several times (both triggered and untriggered).
|
|
TODO: Consider cloning the triggered condition and using the copies for:
|
|
1. push the first copy down, to have most restrictive index condition
|
|
possible.
|
|
2. Put the second copy into tab->m_condition.
|
|
b) Stored functions contain a statement that might start new operations (like
|
|
DML statements) from within the storage engine. This does not work against
|
|
all SEs.
|
|
c) Subqueries might contain nested subqueries and involve more tables.
|
|
TODO: ROY: CHECK THIS
|
|
d) Do not push down internal functions of type DD_INTERNAL_FUNC. When ICP is
|
|
enabled, pushing internal functions to storage engine for evaluation will
|
|
open data-dictionary tables. In InnoDB storage engine this will result in
|
|
situation like recursive latching of same page by the same thread. To avoid
|
|
such situation, internal functions of type DD_INTERNAL_FUNC are not pushed
|
|
to storage engine for evaluation.
|
|
|
|
@param item Expression to check
|
|
@param tbl The table having the index
|
|
@param keyno The index number
|
|
@param other_tbls_ok true <=> Fields of other non-const tables are allowed
|
|
|
|
@return false if No, true if Yes
|
|
*/
|
|
|
|
bool uses_index_fields_only(Item *item, TABLE *tbl, uint keyno,
|
|
bool other_tbls_ok) {
|
|
// Restrictions b and c.
|
|
if (item->has_stored_program() || item->has_subquery()) return false;
|
|
|
|
// No table fields in const items
|
|
if (item->const_for_execution()) return true;
|
|
|
|
const Item::Type item_type = item->type();
|
|
|
|
switch (item_type) {
|
|
case Item::FUNC_ITEM: {
|
|
Item_func *item_func = (Item_func *)item;
|
|
const Item_func::Functype func_type = item_func->functype();
|
|
|
|
if (func_type == Item_func::DD_INTERNAL_FUNC) // Restriction d.
|
|
return false;
|
|
|
|
// Restriction a.
|
|
if (func_type == Item_func::TRIG_COND_FUNC &&
|
|
down_cast<Item_func_trig_cond *>(item_func)->get_trig_type() !=
|
|
Item_func_trig_cond::IS_NOT_NULL_COMPL) {
|
|
return false;
|
|
}
|
|
|
|
/* This is a function, apply condition recursively to arguments */
|
|
if (item_func->argument_count() > 0) {
|
|
Item **item_end =
|
|
(item_func->arguments()) + item_func->argument_count();
|
|
for (Item **child = item_func->arguments(); child != item_end;
|
|
child++) {
|
|
if (!uses_index_fields_only(*child, tbl, keyno, other_tbls_ok))
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case Item::COND_ITEM: {
|
|
/*
|
|
This is a AND/OR condition. Regular AND/OR clauses are handled by
|
|
make_cond_for_index() which will chop off the part that can be
|
|
checked with index. This code is for handling non-top-level AND/ORs,
|
|
e.g. func(x AND y).
|
|
*/
|
|
List_iterator<Item> li(*((Item_cond *)item)->argument_list());
|
|
Item *cond_item;
|
|
while ((cond_item = li++)) {
|
|
if (!uses_index_fields_only(cond_item, tbl, keyno, other_tbls_ok))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
case Item::FIELD_ITEM: {
|
|
const Item_field *item_field = down_cast<const Item_field *>(item);
|
|
if (item_field->field->table != tbl) return other_tbls_ok;
|
|
/*
|
|
The below is probably a repetition - the first part checks the
|
|
other two, but let's play it safe:
|
|
*/
|
|
return item_field->field->part_of_key.is_set(keyno) &&
|
|
item_field->field->type() != MYSQL_TYPE_GEOMETRY &&
|
|
item_field->field->type() != MYSQL_TYPE_BLOB;
|
|
}
|
|
case Item::REF_ITEM:
|
|
return uses_index_fields_only(item->real_item(), tbl, keyno,
|
|
other_tbls_ok);
|
|
default:
|
|
return false; /* Play it safe, don't push unknown non-const items */
|
|
}
|
|
}
|
|
|
|
/**
|
|
Optimize semi-join nests that could be run with sj-materialization
|
|
|
|
@param join The join to optimize semi-join nests for
|
|
|
|
@details
|
|
Optimize each of the semi-join nests that can be run with
|
|
materialization. For each of the nests, we
|
|
- Generate the best join order for this "sub-join" and remember it;
|
|
- Remember the sub-join execution cost (it's part of materialization
|
|
cost);
|
|
- Calculate other costs that will be incurred if we decide
|
|
to use materialization strategy for this semi-join nest.
|
|
|
|
All obtained information is saved and will be used by the main join
|
|
optimization pass.
|
|
|
|
@return false if successful, true if error
|
|
*/
|
|
|
|
static bool optimize_semijoin_nests_for_materialization(JOIN *join) {
|
|
DBUG_TRACE;
|
|
Opt_trace_context *const trace = &join->thd->opt_trace;
|
|
|
|
for (Table_ref *sj_nest : join->query_block->sj_nests) {
|
|
/* As a precaution, reset pointers that were used in prior execution */
|
|
sj_nest->nested_join->sjm.positions = nullptr;
|
|
|
|
/* Calculate the cost of materialization if materialization is allowed. */
|
|
if (sj_nest->nested_join->sj_enabled_strategies &
|
|
OPTIMIZER_SWITCH_MATERIALIZATION) {
|
|
/* A semi-join nest should not contain tables marked as const */
|
|
assert(!(sj_nest->sj_inner_tables & join->const_table_map));
|
|
|
|
Opt_trace_object trace_wrapper(trace);
|
|
Opt_trace_object trace_sjmat(
|
|
trace, "execution_plan_for_potential_materialization");
|
|
Opt_trace_array trace_sjmat_steps(trace, "steps");
|
|
/*
|
|
Try semijoin materialization if the semijoin is classified as
|
|
non-trivially-correlated.
|
|
*/
|
|
if (sj_nest->nested_join->sj_corr_tables) continue;
|
|
/*
|
|
Check whether data types allow execution with materialization.
|
|
*/
|
|
semijoin_types_allow_materialization(sj_nest);
|
|
|
|
if (!sj_nest->nested_join->sjm.scan_allowed &&
|
|
!sj_nest->nested_join->sjm.lookup_allowed)
|
|
continue;
|
|
|
|
if (Optimize_table_order(join->thd, join, sj_nest).choose_table_order())
|
|
return true;
|
|
const uint n_tables = std::popcount(sj_nest->sj_inner_tables);
|
|
calculate_materialization_costs(join, sj_nest, n_tables,
|
|
&sj_nest->nested_join->sjm);
|
|
/*
|
|
Cost data is in sj_nest->nested_join->sjm. We also need to save the
|
|
plan:
|
|
*/
|
|
if (!(sj_nest->nested_join->sjm.positions =
|
|
(POSITION *)join->thd->alloc(sizeof(POSITION) * n_tables)))
|
|
return true;
|
|
memcpy(sj_nest->nested_join->sjm.positions,
|
|
join->best_positions + join->const_tables,
|
|
sizeof(POSITION) * n_tables);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Check if table's Key_use elements have an eq_ref(outer_tables) candidate
|
|
|
|
SYNOPSIS
|
|
find_eq_ref_candidate()
|
|
tl Table to be checked
|
|
sj_inner_tables Bitmap of inner tables. eq_ref(inner_table) doesn't
|
|
count.
|
|
|
|
DESCRIPTION
|
|
Check if table's Key_use elements have an eq_ref(outer_tables) candidate
|
|
|
|
TODO
|
|
Check again if it is feasible to factor common parts with constant table
|
|
search
|
|
|
|
RETURN
|
|
true - There exists an eq_ref(outer-tables) candidate
|
|
false - Otherwise
|
|
*/
|
|
|
|
static bool find_eq_ref_candidate(Table_ref *tl, table_map sj_inner_tables) {
|
|
Key_use *keyuse = tl->table->reginfo.join_tab->keyuse();
|
|
|
|
if (keyuse) {
|
|
while (true) /* For each key */
|
|
{
|
|
const uint key = keyuse->key;
|
|
KEY *const keyinfo = tl->table->key_info + key;
|
|
key_part_map bound_parts = 0;
|
|
if ((keyinfo->flags & (HA_NOSAME)) == HA_NOSAME) {
|
|
do /* For all equalities on all key parts */
|
|
{
|
|
/* Check if this is "t.keypart = expr(outer_tables) */
|
|
if (!(keyuse->used_tables & sj_inner_tables) &&
|
|
!(keyuse->optimize & KEY_OPTIMIZE_REF_OR_NULL)) {
|
|
/*
|
|
Consider only if the resulting condition does not pass a NULL
|
|
value through. Especially needed for a UNIQUE index on NULLable
|
|
columns where a duplicate row is possible with NULL values.
|
|
*/
|
|
if (keyuse->null_rejecting || !keyuse->val->is_nullable() ||
|
|
!keyinfo->key_part[keyuse->keypart].field->is_nullable())
|
|
bound_parts |= (key_part_map)1 << keyuse->keypart;
|
|
}
|
|
keyuse++;
|
|
} while (keyuse->key == key && keyuse->table_ref == tl);
|
|
|
|
if (bound_parts == LOWER_BITS(uint, keyinfo->user_defined_key_parts))
|
|
return true;
|
|
if (keyuse->table_ref != tl) return false;
|
|
} else {
|
|
do {
|
|
keyuse++;
|
|
if (keyuse->table_ref != tl) return false;
|
|
} while (keyuse->key == key);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Pull tables out of semi-join nests based on functional dependencies
|
|
|
|
@param join The join where to do the semi-join table pullout
|
|
|
|
@return False if successful, true if error (Out of memory)
|
|
|
|
@details
|
|
Pull tables out of semi-join nests based on functional dependencies,
|
|
ie. if a table is accessed via eq_ref(outer_tables).
|
|
The function may be called several times, the caller is responsible
|
|
for setting up proper key information that this function acts upon.
|
|
|
|
PRECONDITIONS
|
|
When this function is called, the join may have several semi-join nests
|
|
but it is guaranteed that one semi-join nest does not contain another.
|
|
For functionally dependent tables to be pulled out, key information must
|
|
have been calculated (see update_ref_and_keys()).
|
|
|
|
POSTCONDITIONS
|
|
* Tables that were pulled out are removed from the semi-join nest they
|
|
belonged to and added to the parent join nest.
|
|
* For these tables, the used_tables and not_null_tables fields of
|
|
the semi-join nest they belonged to will be adjusted.
|
|
The semi-join nest is also marked as correlated, and
|
|
sj_corr_tables and sj_depends_on are adjusted if necessary.
|
|
* Semi-join nests' sj_inner_tables is set equal to used_tables
|
|
|
|
NOTE
|
|
Table pullout may make uncorrelated subquery correlated. Consider this
|
|
example:
|
|
|
|
... WHERE oe IN (SELECT it1.primary_key WHERE p(it1, it2) ... )
|
|
|
|
here table it1 can be pulled out (we have it1.primary_key=oe which gives
|
|
us functional dependency). Once it1 is pulled out, all references to it1
|
|
from p(it1, it2) become references to outside of the subquery and thus
|
|
make the subquery (i.e. its semi-join nest) correlated.
|
|
Making the subquery (i.e. its semi-join nest) correlated prevents us from
|
|
using Materialization or LooseScan to execute it.
|
|
*/
|
|
|
|
static bool pull_out_semijoin_tables(JOIN *join) {
|
|
DBUG_TRACE;
|
|
|
|
assert(!join->query_block->sj_nests.empty());
|
|
|
|
Opt_trace_context *const trace = &join->thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
const Opt_trace_array trace_pullout(trace, "pulled_out_semijoin_tables");
|
|
|
|
/* Try pulling out tables from each semi-join nest */
|
|
for (auto sj_list_it = join->query_block->sj_nests.begin();
|
|
sj_list_it != join->query_block->sj_nests.end();) {
|
|
Table_ref *sj_nest = *sj_list_it;
|
|
if (sj_nest->is_aj_nest()) {
|
|
++sj_list_it;
|
|
continue;
|
|
}
|
|
table_map pulled_tables = 0;
|
|
/*
|
|
Calculate set of tables within this semi-join nest that have
|
|
other dependent tables. They cannot be pulled out. For example, with
|
|
t1 SEMIJOIN (t2 LEFT JOIN t3 ON ...) ON t1.a=t2.pk,
|
|
t2 cannot be pulled out because t3 depends on it.
|
|
*/
|
|
table_map dep_tables = 0;
|
|
for (Table_ref *tbl : sj_nest->nested_join->m_tables) {
|
|
if (tbl->dep_tables & sj_nest->nested_join->used_tables)
|
|
dep_tables |= tbl->dep_tables;
|
|
}
|
|
/*
|
|
Find which tables we can pull out based on key dependency data.
|
|
Note that pulling one table out can allow us to pull out some
|
|
other tables too.
|
|
*/
|
|
bool pulled_a_table;
|
|
do {
|
|
pulled_a_table = false;
|
|
for (Table_ref *tbl : sj_nest->nested_join->m_tables) {
|
|
if (tbl->table && !(pulled_tables & tbl->map()) &&
|
|
!(dep_tables & tbl->map())) {
|
|
if (find_eq_ref_candidate(
|
|
tbl, sj_nest->nested_join->used_tables & ~pulled_tables)) {
|
|
pulled_a_table = true;
|
|
pulled_tables |= tbl->map();
|
|
Opt_trace_object(trace).add_utf8_table(tbl).add(
|
|
"functionally_dependent", true);
|
|
/*
|
|
Pulling a table out of uncorrelated subquery in general makes
|
|
it correlated. See the NOTE to this function.
|
|
*/
|
|
sj_nest->nested_join->sj_corr_tables |= tbl->map();
|
|
sj_nest->nested_join->sj_depends_on |= tbl->map();
|
|
}
|
|
}
|
|
}
|
|
} while (pulled_a_table);
|
|
|
|
/*
|
|
Move the pulled out Table_ref elements to the parents.
|
|
*/
|
|
sj_nest->nested_join->used_tables &= ~pulled_tables;
|
|
sj_nest->nested_join->not_null_tables &= ~pulled_tables;
|
|
|
|
/* sj_inner_tables is a copy of nested_join->used_tables */
|
|
sj_nest->sj_inner_tables = sj_nest->nested_join->used_tables;
|
|
|
|
bool remove = false;
|
|
if (pulled_tables) {
|
|
mem_root_deque<Table_ref *> *upper_join_list =
|
|
(sj_nest->embedding != nullptr)
|
|
? &sj_nest->embedding->nested_join->m_tables
|
|
: &join->query_block->m_table_nest;
|
|
|
|
const Prepared_stmt_arena_holder ps_arena_holder(join->thd);
|
|
|
|
for (auto child_li = sj_nest->nested_join->m_tables.begin();
|
|
child_li != sj_nest->nested_join->m_tables.end();) {
|
|
Table_ref *tbl = *child_li;
|
|
if (tbl->table && !(sj_nest->nested_join->used_tables & tbl->map())) {
|
|
/*
|
|
Pull the table up in the same way as simplify_joins() does:
|
|
update join_list and embedding pointers but keep next[_local]
|
|
pointers.
|
|
*/
|
|
child_li = sj_nest->nested_join->m_tables.erase(child_li);
|
|
|
|
upper_join_list->push_back(tbl);
|
|
|
|
tbl->join_list = upper_join_list;
|
|
tbl->embedding = sj_nest->embedding;
|
|
} else {
|
|
++child_li;
|
|
}
|
|
}
|
|
|
|
/* Remove the sj-nest itself if we've removed everything from it */
|
|
if (!sj_nest->nested_join->used_tables) {
|
|
upper_join_list->erase(std::find(upper_join_list->begin(),
|
|
upper_join_list->end(), sj_nest));
|
|
/* Also remove it from the list of SJ-nests: */
|
|
remove = true;
|
|
}
|
|
}
|
|
|
|
if (remove) {
|
|
sj_list_it = join->query_block->sj_nests.erase(sj_list_it);
|
|
} else {
|
|
++sj_list_it;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Values in optimize */
|
|
#define KEY_OPTIMIZE_EXISTS 1
|
|
#define KEY_OPTIMIZE_REF_OR_NULL 2
|
|
|
|
/**
|
|
Merge new key definitions to old ones, remove those not used in both.
|
|
|
|
This is called for OR between different levels.
|
|
|
|
To be able to do 'ref_or_null' we merge a comparison of a column
|
|
and 'column IS NULL' to one test. This is useful for sub select queries
|
|
that are internally transformed to something like:.
|
|
|
|
@code
|
|
SELECT * FROM t1 WHERE t1.key=outer_ref_field or t1.key IS NULL
|
|
@endcode
|
|
|
|
Key_field::null_rejecting is processed as follows: @n
|
|
result has null_rejecting=true if it is set for both ORed references.
|
|
for example:
|
|
- (t2.key = t1.field OR t2.key = t1.field) -> null_rejecting=true
|
|
- (t2.key = t1.field OR t2.key <=> t1.field) -> null_rejecting=false
|
|
|
|
@todo
|
|
The result of this is that we're missing some 'ref' accesses.
|
|
OptimizerTeam: Fix this
|
|
*/
|
|
|
|
static Key_field *merge_key_fields(Key_field *start, Key_field *new_fields,
|
|
Key_field *end, uint and_level) {
|
|
if (start == new_fields) return start; // Impossible or
|
|
if (new_fields == end) return start; // No new fields, skip all
|
|
|
|
Key_field *first_free = new_fields;
|
|
|
|
/* Mark all found fields in old array */
|
|
for (; new_fields != end; new_fields++) {
|
|
const Field *const new_field = new_fields->item_field->field;
|
|
|
|
for (Key_field *old = start; old != first_free; old++) {
|
|
const Field *const old_field = old->item_field->field;
|
|
|
|
/*
|
|
Check that the Field objects are the same, as we may have several
|
|
Item_field objects pointing to the same Field:
|
|
*/
|
|
if (old_field == new_field) {
|
|
/*
|
|
NOTE: below const_item() call really works as "!used_tables()", i.e.
|
|
it can return false where it is feasible to make it return true.
|
|
|
|
The cause is as follows: Some of the tables are already known to be
|
|
const tables (the detection code is in JOIN::make_join_plan(),
|
|
above the update_ref_and_keys() call), but we didn't propagate
|
|
information about this: TABLE::const_table is not set to true, and
|
|
Item::update_used_tables() hasn't been called for each item.
|
|
The result of this is that we're missing some 'ref' accesses.
|
|
TODO: OptimizerTeam: Fix this
|
|
*/
|
|
if (!new_fields->val->const_item()) {
|
|
/*
|
|
If the value matches, we can use the key reference.
|
|
If not, we keep it until we have examined all new values
|
|
*/
|
|
if (old->val->eq(new_fields->val, old_field->binary())) {
|
|
old->level = and_level;
|
|
old->optimize =
|
|
((old->optimize & new_fields->optimize & KEY_OPTIMIZE_EXISTS) |
|
|
((old->optimize | new_fields->optimize) &
|
|
KEY_OPTIMIZE_REF_OR_NULL));
|
|
old->null_rejecting =
|
|
(old->null_rejecting && new_fields->null_rejecting);
|
|
}
|
|
} else if (old->eq_func && new_fields->eq_func &&
|
|
old->val->eq_by_collation(new_fields->val,
|
|
old_field->binary(),
|
|
old_field->charset())) {
|
|
old->level = and_level;
|
|
old->optimize =
|
|
((old->optimize & new_fields->optimize & KEY_OPTIMIZE_EXISTS) |
|
|
((old->optimize | new_fields->optimize) &
|
|
KEY_OPTIMIZE_REF_OR_NULL));
|
|
old->null_rejecting =
|
|
(old->null_rejecting && new_fields->null_rejecting);
|
|
} else if (old->eq_func && new_fields->eq_func &&
|
|
((old->val->const_item() && old->val->is_null()) ||
|
|
new_fields->val->is_null())) {
|
|
/* field = expression OR field IS NULL */
|
|
old->level = and_level;
|
|
old->optimize = KEY_OPTIMIZE_REF_OR_NULL;
|
|
/*
|
|
Remember the NOT NULL value unless the value does not depend
|
|
on other tables.
|
|
*/
|
|
if (!old->val->used_tables() && old->val->is_null())
|
|
old->val = new_fields->val;
|
|
/* The referred expression can be NULL: */
|
|
old->null_rejecting = false;
|
|
} else {
|
|
/*
|
|
We are comparing two different const. In this case we can't
|
|
use a key-lookup on this so it's better to remove the value
|
|
and let the range optimizer handle it
|
|
*/
|
|
if (old == --first_free) // If last item
|
|
break;
|
|
*old = *first_free; // Remove old value
|
|
old--; // Retry this value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Remove all not used items */
|
|
for (Key_field *old = start; old != first_free;) {
|
|
if (old->level != and_level) { // Not used in all levels
|
|
if (old == --first_free) break;
|
|
*old = *first_free; // Remove old value
|
|
continue;
|
|
}
|
|
old++;
|
|
}
|
|
return first_free;
|
|
}
|
|
|
|
/**
|
|
Given a field, return its index in semi-join's select list, or UINT_MAX
|
|
|
|
@param item_field Field to be looked up in select list
|
|
|
|
@retval =UINT_MAX Field is not from a semijoin-transformed subquery
|
|
@retval <UINT_MAX Index in select list of subquery
|
|
|
|
@details
|
|
Given a field, find its table; then see if the table is within a
|
|
semi-join nest and if the field was in select list of the subquery
|
|
(if subquery was part of a quantified comparison predicate), or
|
|
the field was a result of subquery decorrelation.
|
|
If it was, then return the field's index in the select list.
|
|
The value is used by LooseScan strategy.
|
|
*/
|
|
|
|
static uint get_semi_join_select_list_index(Item_field *item_field) {
|
|
Table_ref *emb_sj_nest = item_field->table_ref->embedding;
|
|
if (emb_sj_nest && emb_sj_nest->is_sj_or_aj_nest()) {
|
|
const mem_root_deque<Item *> &items =
|
|
emb_sj_nest->nested_join->sj_inner_exprs;
|
|
for (size_t i = 0; i < items.size(); i++) {
|
|
const Item *sel_item = items[i];
|
|
if (sel_item->type() == Item::FIELD_ITEM &&
|
|
down_cast<const Item_field *>(sel_item)->field->eq(item_field->field))
|
|
return i;
|
|
}
|
|
}
|
|
return UINT_MAX;
|
|
}
|
|
|
|
/**
|
|
@brief
|
|
If EXPLAIN or if the --safe-updates option is enabled, add a warning that an
|
|
index cannot be used for ref access.
|
|
|
|
@details
|
|
If EXPLAIN or if the --safe-updates option is enabled, add a warning for each
|
|
index that cannot be used for ref access due to either type conversion or
|
|
different collations on the field used for comparison
|
|
|
|
Example type conversion (char compared to int):
|
|
|
|
CREATE TABLE t1 (url char(1) PRIMARY KEY);
|
|
SELECT * FROM t1 WHERE url=1;
|
|
|
|
Example different collations (danish vs german2):
|
|
|
|
CREATE TABLE t1 (url char(1) PRIMARY KEY) collate latin1_danish_ci;
|
|
SELECT * FROM t1 WHERE url='1' collate latin1_german2_ci;
|
|
|
|
@param thd Thread for the connection that submitted the query
|
|
@param field Field used in comparison
|
|
@param cant_use_index Indexes that cannot be used for lookup
|
|
*/
|
|
static void warn_index_not_applicable(THD *thd, const Field *field,
|
|
const Key_map cant_use_index) {
|
|
const Functional_index_error_handler functional_index_error_handler(field,
|
|
thd);
|
|
|
|
if (thd->lex->is_explain() ||
|
|
thd->variables.option_bits & OPTION_SAFE_UPDATES)
|
|
for (uint j = 0; j < field->table->s->keys; j++)
|
|
if (cant_use_index.is_set(j))
|
|
push_warning_printf(thd, Sql_condition::SL_WARNING,
|
|
ER_WARN_INDEX_NOT_APPLICABLE,
|
|
ER_THD(thd, ER_WARN_INDEX_NOT_APPLICABLE), "ref",
|
|
field->table->key_info[j].name, field->field_name);
|
|
}
|
|
|
|
/**
|
|
Add a possible key to array of possible keys if it's usable as a key
|
|
|
|
@param [in,out] key_fields Used as an input parameter in the sense that it is
|
|
a pointer to a pointer to a memory area where an array of Key_field objects
|
|
will stored. It is used as an out parameter in the sense that the pointer will
|
|
be updated to point beyond the last Key_field written.
|
|
|
|
@param thd session context
|
|
@param and_level And level, to be stored in Key_field
|
|
@param cond Condition predicate
|
|
@param item_field Field used in comparison
|
|
@param eq_func True if we used =, <=> or IS NULL
|
|
@param value Array of values used for comparison with field
|
|
@param num_values Number of elements in the array of values
|
|
@param usable_tables Tables which can be used for key optimization
|
|
@param sargables IN/OUT Array of found sargable candidates.
|
|
Will be ignored in case eq_func is true.
|
|
|
|
@note
|
|
If we are doing a NOT NULL comparison on a NOT NULL field in a outer join
|
|
table, we store this to be able to do not exists optimization later.
|
|
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
static bool add_key_field(THD *thd, Key_field **key_fields, uint and_level,
|
|
Item_func *cond, Item_field *item_field, bool eq_func,
|
|
Item **value, uint num_values,
|
|
table_map usable_tables, SARGABLE_PARAM **sargables) {
|
|
assert(cond->is_bool_func());
|
|
assert(eq_func || sargables);
|
|
assert(cond->functype() == Item_func::EQ_FUNC ||
|
|
cond->functype() == Item_func::NE_FUNC ||
|
|
cond->functype() == Item_func::GT_FUNC ||
|
|
cond->functype() == Item_func::LT_FUNC ||
|
|
cond->functype() == Item_func::GE_FUNC ||
|
|
cond->functype() == Item_func::LE_FUNC ||
|
|
cond->functype() == Item_func::MULT_EQUAL_FUNC ||
|
|
cond->functype() == Item_func::EQUAL_FUNC ||
|
|
cond->functype() == Item_func::LIKE_FUNC ||
|
|
cond->functype() == Item_func::ISNULL_FUNC ||
|
|
cond->functype() == Item_func::ISNOTNULL_FUNC ||
|
|
cond->functype() == Item_func::BETWEEN ||
|
|
cond->functype() == Item_func::IN_FUNC ||
|
|
cond->functype() == Item_func::MEMBER_OF_FUNC ||
|
|
cond->functype() == Item_func::SP_EQUALS_FUNC ||
|
|
cond->functype() == Item_func::SP_WITHIN_FUNC ||
|
|
cond->functype() == Item_func::SP_CONTAINS_FUNC ||
|
|
cond->functype() == Item_func::SP_INTERSECTS_FUNC ||
|
|
cond->functype() == Item_func::SP_DISJOINT_FUNC ||
|
|
cond->functype() == Item_func::SP_COVERS_FUNC ||
|
|
cond->functype() == Item_func::SP_COVEREDBY_FUNC ||
|
|
cond->functype() == Item_func::SP_OVERLAPS_FUNC ||
|
|
cond->functype() == Item_func::SP_TOUCHES_FUNC ||
|
|
cond->functype() == Item_func::SP_CROSSES_FUNC);
|
|
|
|
Field *const field = item_field->field;
|
|
Table_ref *const tl = item_field->table_ref;
|
|
|
|
if (tl->table->reginfo.join_tab == nullptr) {
|
|
/*
|
|
Due to a bug in IN-to-EXISTS (grep for real_item() in item_subselect.cc
|
|
for more info), an index over a field from an outer query might be
|
|
considered here, which is incorrect. Their query has been fully
|
|
optimized already so their reginfo.join_tab is NULL and we reject them.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
DBUG_PRINT("info", ("add_key_field for field %s", field->field_name));
|
|
uint exists_optimize = 0;
|
|
if (!tl->derived_keys_ready && tl->uses_materialization() &&
|
|
!tl->table->is_created()) {
|
|
bool allocated;
|
|
if (tl->update_derived_keys(thd, field, value, num_values, &allocated))
|
|
return true;
|
|
if (!allocated) return false;
|
|
}
|
|
if (!field->is_flag_set(PART_KEY_FLAG)) {
|
|
// Don't remove column IS NULL on a LEFT JOIN table
|
|
if (!eq_func || (*value)->type() != Item::NULL_ITEM ||
|
|
!tl->table->is_nullable() || field->is_nullable())
|
|
return false; // Not a key. Skip it
|
|
exists_optimize = KEY_OPTIMIZE_EXISTS;
|
|
assert(num_values == 1);
|
|
} else {
|
|
table_map used_tables = 0;
|
|
bool optimizable = false;
|
|
for (uint i = 0; i < num_values; i++) {
|
|
used_tables |= (value[i])->used_tables();
|
|
if (!((value[i])->used_tables() & (tl->map() | RAND_TABLE_BIT)))
|
|
optimizable = true;
|
|
}
|
|
if (!optimizable) return false;
|
|
if (!(usable_tables & tl->map())) {
|
|
if (!eq_func || (*value)->type() != Item::NULL_ITEM ||
|
|
!tl->table->is_nullable() || field->is_nullable())
|
|
return false; // Can't use left join optimize
|
|
exists_optimize = KEY_OPTIMIZE_EXISTS;
|
|
} else {
|
|
JOIN_TAB *stat = tl->table->reginfo.join_tab;
|
|
Key_map possible_keys = field->key_start;
|
|
possible_keys.intersect(tl->table->keys_in_use_for_query);
|
|
stat[0].keys().merge(possible_keys); // Add possible keys
|
|
|
|
/*
|
|
Save the following cases:
|
|
Field op constant
|
|
Field LIKE constant where constant doesn't start with a wildcard
|
|
Field = field2 where field2 is in a different table
|
|
Field op formula
|
|
Field IS NULL
|
|
Field IS NOT NULL
|
|
Field BETWEEN ...
|
|
Field IN ...
|
|
*/
|
|
stat[0].key_dependent |= used_tables;
|
|
|
|
bool is_const = true;
|
|
for (uint i = 0; i < num_values; i++) {
|
|
if (!(is_const &= value[i]->const_for_execution())) break;
|
|
}
|
|
if (is_const)
|
|
stat[0].const_keys.merge(possible_keys);
|
|
else if (!eq_func) {
|
|
/*
|
|
Save info to be able check whether this predicate can be
|
|
considered as sargable for range analysis after reading const tables.
|
|
We do not save info about equalities as update_const_equal_items
|
|
will take care of updating info on keys from sargable equalities.
|
|
*/
|
|
assert(sargables);
|
|
(*sargables)--;
|
|
/*
|
|
The sargables and key_fields arrays share the same memory
|
|
buffer, and grow from opposite directions, so make sure they
|
|
don't cross.
|
|
*/
|
|
assert(*sargables > reinterpret_cast<SARGABLE_PARAM *>(*key_fields));
|
|
(*sargables)->field = field;
|
|
(*sargables)->arg_value = value;
|
|
(*sargables)->num_values = num_values;
|
|
}
|
|
/*
|
|
We can't always use indexes when comparing a string index to a
|
|
number. cmp_type() is checked to allow compare of dates to numbers.
|
|
eq_func is NEVER true when num_values > 1
|
|
*/
|
|
if (!eq_func) return false;
|
|
|
|
/*
|
|
Check if the field and value are comparable in the index.
|
|
*/
|
|
if (!comparable_in_index(cond, field, Field::itRAW, cond->functype(),
|
|
*value) ||
|
|
(field->cmp_type() == STRING_RESULT &&
|
|
field->match_collation_to_optimize_range() &&
|
|
field->charset() != cond->compare_collation())) {
|
|
warn_index_not_applicable(stat->join()->thd, field, possible_keys);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
For the moment eq_func is always true. This slot is reserved for future
|
|
extensions where we want to remembers other things than just eq comparisons
|
|
*/
|
|
assert(eq_func);
|
|
/*
|
|
Calculate the "null rejecting" property based on the type of predicate.
|
|
Only the <=> operator and the IS NULL and IS NOT NULL clauses may return
|
|
true on nullable operands that have the NULL value - assuming that all
|
|
other predicates are augmented with IS TRUE or IS FALSE truth clause,
|
|
so that all UNKNOWN results are converted to TRUE or FALSE.
|
|
|
|
The "null rejecting" property can be combined with the left and right
|
|
operands to perform certain optimizations.
|
|
|
|
If the condition has form "left.field = right.keypart" and left.field can
|
|
be NULL, there will be no matches if left.field is NULL.
|
|
We use null_rejecting in add_not_null_conds() to add
|
|
'left.field IS NOT NULL' to tab->m_condition, if this is not an outer
|
|
join. We also use it to shortcut reading rows from table "right" when
|
|
left.field is found to be a NULL value (in RefIterator and BKA).
|
|
|
|
It is also possible to apply optimizations to the indexed table.
|
|
If the operation is null rejecting and there is a unique index over
|
|
the key field, an eq_ref operation can be performed on the index, since
|
|
we have no interest in the NULL values.
|
|
|
|
Notice however that the null rejecting property may be cancelled out
|
|
by the KEY_OPTIMIZE_REF_OR_NULL property: this can be set when having:
|
|
|
|
left.field = right.keypart OR right.keypart IS NULL.
|
|
*/
|
|
const bool null_rejecting = cond->functype() != Item_func::EQUAL_FUNC &&
|
|
cond->functype() != Item_func::ISNULL_FUNC &&
|
|
cond->functype() != Item_func::ISNOTNULL_FUNC;
|
|
|
|
/* Store possible eq field */
|
|
new (*key_fields) Key_field(item_field, *value, and_level, exists_optimize,
|
|
eq_func, null_rejecting, nullptr,
|
|
get_semi_join_select_list_index(item_field));
|
|
(*key_fields)++;
|
|
/*
|
|
The sargables and key_fields arrays share the same memory buffer,
|
|
and grow from opposite directions, so make sure they don't
|
|
cross. But if sargables was NULL, eq_func had to be true and we
|
|
don't write any sargables.
|
|
*/
|
|
assert(sargables == nullptr ||
|
|
*key_fields < reinterpret_cast<Key_field *>(*sargables));
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Add possible keys to array of possible keys originated from a simple
|
|
predicate.
|
|
|
|
@param thd session context
|
|
@param[in,out] key_fields Pointer to add key, if usable
|
|
is incremented if key was stored in the array
|
|
@param and_level And level, to be stored in Key_field
|
|
@param cond Condition predicate
|
|
@param field_item Field used in comparison
|
|
@param eq_func True if we used =, <=> or IS NULL
|
|
@param val Value used for comparison with field
|
|
Is NULL for BETWEEN and IN
|
|
@param num_values Number of elements in the array of values
|
|
@param usable_tables Tables which can be used for key optimization
|
|
@param sargables IN/OUT Array of found sargable candidates
|
|
|
|
@note
|
|
If field items f1 and f2 belong to the same multiple equality and
|
|
a key is added for f1, the the same key is added for f2.
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
static bool add_key_equal_fields(THD *thd, Key_field **key_fields,
|
|
uint and_level, Item_func *cond,
|
|
Item_field *field_item, bool eq_func,
|
|
Item **val, uint num_values,
|
|
table_map usable_tables,
|
|
SARGABLE_PARAM **sargables) {
|
|
assert(cond->is_bool_func());
|
|
|
|
if (add_key_field(thd, key_fields, and_level, cond, field_item, eq_func, val,
|
|
num_values, usable_tables, sargables))
|
|
return true;
|
|
Item_equal *item_equal = field_item->multi_equality();
|
|
if (item_equal == nullptr) return false;
|
|
/*
|
|
Add to the set of possible key values every substitution of
|
|
the field for an equal field included into item_equal
|
|
*/
|
|
for (Item_field &item : item_equal->get_fields()) {
|
|
if (!field_item->field->eq(item.field)) {
|
|
if (add_key_field(thd, key_fields, and_level, cond, &item, eq_func, val,
|
|
num_values, usable_tables, sargables))
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Check if an expression is a non-outer field.
|
|
|
|
Checks if an expression is a field and belongs to the current select.
|
|
|
|
@param field Item expression to check
|
|
|
|
@return boolean
|
|
@retval true the expression is a local field
|
|
@retval false it's something else
|
|
*/
|
|
|
|
static bool is_local_field(Item *field) {
|
|
return field->real_item()->type() == Item::FIELD_ITEM &&
|
|
!field->is_outer_reference() &&
|
|
!down_cast<Item_ident *>(field)->depended_from &&
|
|
!down_cast<Item_ident *>(field->real_item())->depended_from;
|
|
}
|
|
|
|
/**
|
|
Check if a row constructor expression is over columns in the same query block.
|
|
|
|
@param item_row Row expression to check.
|
|
|
|
@return boolean
|
|
@retval true The expression is a local column reference.
|
|
@retval false It's something else.
|
|
*/
|
|
static bool is_row_of_local_columns(Item_row *item_row) {
|
|
for (uint i = 0; i < item_row->cols(); ++i)
|
|
if (!is_local_field(item_row->element_index(i))) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
The guts of the ref optimizer. This function, along with the other
|
|
add_key_* functions, make up a recursive procedure that analyzes a
|
|
condition expression (a tree of AND and OR predicates) and does
|
|
many things.
|
|
|
|
@param thd session context
|
|
@param join The query block involving the condition.
|
|
@param [in,out] key_fields Start of memory buffer, see below.
|
|
@param [in,out] and_level Current 'and level', see below.
|
|
@param cond The conditional expression to analyze.
|
|
@param usable_tables Tables not in this bitmap will not be examined.
|
|
@param [in,out] sargables End of memory buffer, see below.
|
|
|
|
@returns false if success, true if error
|
|
|
|
This documentation is the result of reverse engineering and may
|
|
therefore not capture the full gist of the procedure, but it is
|
|
known to do the following:
|
|
|
|
- Populate a raw memory buffer from two directions at the same time. An
|
|
'array' of Key_field objects fill the buffer from low to high addresses
|
|
whilst an 'array' of SARGABLE_PARAM's fills the buffer from high to low
|
|
addresses. At the first call to this function, it is assumed that
|
|
key_fields points to the beginning of the buffer and sargables point to the
|
|
end (except for a poor-mans 'null element' at the very end).
|
|
|
|
- Update a number of properties in the JOIN_TAB's that can be used
|
|
to find search keys (sargables).
|
|
|
|
- JOIN_TAB::keys
|
|
- JOIN_TAB::key_dependent
|
|
- JOIN_TAB::const_keys (dictates if the range optimizer will be run
|
|
later.)
|
|
|
|
The Key_field objects are marked with something called an 'and_level', which
|
|
does @b not correspond to their nesting depth within the expression tree. It
|
|
is rather a tag to group conjunctions together. For instance, in the
|
|
conditional expression
|
|
|
|
@code
|
|
a = 0 AND b = 0
|
|
@endcode
|
|
|
|
two Key_field's are produced, both having an and_level of 0.
|
|
|
|
In an expression such as
|
|
|
|
@code
|
|
a = 0 AND b = 0 OR a = 1
|
|
@endcode
|
|
|
|
three Key_field's are produced, the first two corresponding to 'a = 0' and
|
|
'b = 0', respectively, both with and_level 0. The third one corresponds to
|
|
'a = 1' and has an and_level of 1.
|
|
|
|
A separate function, merge_key_fields() performs ref access validation on
|
|
the Key_field array on the recursice ascent. If some Key_field's cannot be
|
|
used for ref access, the key_fields pointer is rolled back. All other
|
|
modifications to the query plan remain.
|
|
*/
|
|
bool add_key_fields(THD *thd, JOIN *join, Key_field **key_fields,
|
|
uint *and_level, Item *cond, table_map usable_tables,
|
|
SARGABLE_PARAM **sargables) {
|
|
assert(cond->is_bool_func());
|
|
|
|
if (cond->type() == Item_func::COND_ITEM) {
|
|
List_iterator_fast<Item> li(*((Item_cond *)cond)->argument_list());
|
|
Key_field *org_key_fields = *key_fields;
|
|
|
|
if (down_cast<Item_cond *>(cond)->functype() == Item_func::COND_AND_FUNC) {
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
if (add_key_fields(thd, join, key_fields, and_level, item,
|
|
usable_tables, sargables))
|
|
return true;
|
|
}
|
|
for (; org_key_fields != *key_fields; org_key_fields++)
|
|
org_key_fields->level = *and_level;
|
|
} else {
|
|
(*and_level)++;
|
|
if (add_key_fields(thd, join, key_fields, and_level, li++, usable_tables,
|
|
sargables))
|
|
return true;
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
Key_field *start_key_fields = *key_fields;
|
|
(*and_level)++;
|
|
if (add_key_fields(thd, join, key_fields, and_level, item,
|
|
usable_tables, sargables))
|
|
return true;
|
|
*key_fields = merge_key_fields(org_key_fields, start_key_fields,
|
|
*key_fields, ++(*and_level));
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Subquery optimization: Conditions that are pushed down into subqueries
|
|
are wrapped into Item_func_trig_cond. We process the wrapped condition
|
|
but need to set cond_guard for Key_use elements generated from it.
|
|
*/
|
|
if (cond->type() == Item::FUNC_ITEM &&
|
|
down_cast<Item_func *>(cond)->functype() == Item_func::TRIG_COND_FUNC) {
|
|
Item *const cond_arg = down_cast<Item_func *>(cond)->arguments()[0];
|
|
Query_expression *qe = join->query_expression();
|
|
if (join->group_list.empty() && join->order.empty() &&
|
|
qe->item != nullptr &&
|
|
qe->item->subquery_type() == Item_subselect::IN_SUBQUERY &&
|
|
!qe->is_set_operation()) {
|
|
Key_field *save = *key_fields;
|
|
if (add_key_fields(thd, join, key_fields, and_level, cond_arg,
|
|
usable_tables, sargables))
|
|
return true;
|
|
// Indicate that this ref access candidate is for subquery lookup:
|
|
for (; save != *key_fields; save++)
|
|
save->cond_guard = ((Item_func_trig_cond *)cond)->get_trig_var();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* If item is of type 'field op field/constant' add it to key_fields */
|
|
if (cond->type() != Item::FUNC_ITEM) return false;
|
|
Item_func *const cond_func = down_cast<Item_func *>(cond);
|
|
auto optimize = cond_func->select_optimize(thd);
|
|
// Catch errors that might be thrown during select_optimize()
|
|
if (thd->is_error()) return true;
|
|
switch (optimize) {
|
|
case Item_func::OPTIMIZE_NONE:
|
|
break;
|
|
case Item_func::OPTIMIZE_KEY: {
|
|
Item **values;
|
|
/*
|
|
Build list of possible keys for 'a BETWEEN low AND high'.
|
|
It is handled similar to the equivalent condition
|
|
'a >= low AND a <= high':
|
|
*/
|
|
if (cond_func->functype() == Item_func::BETWEEN) {
|
|
Item_field *field_item;
|
|
bool equal_func = false;
|
|
uint num_values = 2;
|
|
values = cond_func->arguments();
|
|
|
|
const bool binary_cmp =
|
|
(values[0]->real_item()->type() == Item::FIELD_ITEM)
|
|
? ((Item_field *)values[0]->real_item())->field->binary()
|
|
: true;
|
|
|
|
/*
|
|
Additional optimization: If 'low = high':
|
|
Handle as if the condition was "t.key = low".
|
|
*/
|
|
if (!((Item_func_between *)cond_func)->negated &&
|
|
values[1]->eq(values[2], binary_cmp)) {
|
|
equal_func = true;
|
|
num_values = 1;
|
|
}
|
|
|
|
/*
|
|
Append keys for 'field <cmp> value[]' if the
|
|
condition is of the form::
|
|
'<field> BETWEEN value[1] AND value[2]'
|
|
*/
|
|
if (is_local_field(values[0])) {
|
|
field_item = (Item_field *)(values[0]->real_item());
|
|
if (add_key_equal_fields(thd, key_fields, *and_level, cond_func,
|
|
field_item, equal_func, &values[1],
|
|
num_values, usable_tables, sargables))
|
|
return true;
|
|
}
|
|
/*
|
|
Append keys for 'value[0] <cmp> field' if the
|
|
condition is of the form:
|
|
'value[0] BETWEEN field1 AND field2'
|
|
*/
|
|
for (uint i = 1; i <= num_values; i++) {
|
|
if (is_local_field(values[i])) {
|
|
field_item = (Item_field *)(values[i]->real_item());
|
|
if (add_key_equal_fields(thd, key_fields, *and_level, cond_func,
|
|
field_item, equal_func, values, 1,
|
|
usable_tables, sargables))
|
|
return true;
|
|
}
|
|
}
|
|
} // if ( ... Item_func::BETWEEN)
|
|
else if (cond_func->functype() == Item_func::MEMBER_OF_FUNC &&
|
|
is_local_field(cond_func->key_item())) {
|
|
// The predicate is <val> IN (<typed array>)
|
|
add_key_equal_fields(thd, key_fields, *and_level, cond_func,
|
|
(Item_field *)(cond_func->key_item()->real_item()),
|
|
true, cond_func->arguments(), 1, usable_tables,
|
|
sargables);
|
|
} else if (cond_func->functype() == Item_func::JSON_CONTAINS ||
|
|
cond_func->functype() == Item_func::JSON_OVERLAPS) {
|
|
/*
|
|
Applicability analysis was done during substitute_gc().
|
|
Check here that a typed array field is used and there's a key over
|
|
it.
|
|
1) func has a key item
|
|
2) key item is a local field
|
|
3) key item is a typed array field
|
|
If so, mark appropriate index as available for range optimizer
|
|
*/
|
|
if (!cond_func->key_item() || // 1
|
|
!is_local_field(cond_func->key_item()) || // 2
|
|
!cond_func->key_item()->returns_array()) // 3
|
|
break;
|
|
const Field *field =
|
|
(down_cast<const Item_field *>(cond_func->key_item()))->field;
|
|
JOIN_TAB *tab = field->table->reginfo.join_tab;
|
|
Key_map possible_keys = field->key_start;
|
|
|
|
possible_keys.intersect(field->table->keys_in_use_for_query);
|
|
tab->keys().merge(possible_keys); // Add possible keys
|
|
tab->const_keys.merge(possible_keys); // Add possible keys
|
|
} // if (... Item_func::CONTAINS)
|
|
// The predicate is IN or <>
|
|
else if (is_local_field(cond_func->key_item()) &&
|
|
!cond_func->is_outer_reference()) {
|
|
values = cond_func->arguments() + 1;
|
|
if (cond_func->functype() == Item_func::NE_FUNC &&
|
|
is_local_field(cond_func->arguments()[1]))
|
|
values--;
|
|
assert(cond_func->functype() != Item_func::IN_FUNC ||
|
|
cond_func->argument_count() != 2);
|
|
if (add_key_equal_fields(
|
|
thd, key_fields, *and_level, cond_func,
|
|
(Item_field *)(cond_func->key_item()->real_item()), false,
|
|
values, cond_func->argument_count() - 1, usable_tables,
|
|
sargables))
|
|
return true;
|
|
} else if (cond_func->functype() == Item_func::IN_FUNC &&
|
|
cond_func->key_item()->type() == Item::ROW_ITEM) {
|
|
/*
|
|
The condition is (column1, column2, ... ) IN ((const1_1, const1_2),
|
|
...) and there is an index on (column1, column2, ...)
|
|
|
|
The code below makes sure that the row constructor on the lhs indeed
|
|
contains only column references before calling add_key_field on them.
|
|
|
|
We can't do a ref access on IN, yet here we are. Why? We need
|
|
to run add_key_field() only because it verifies that there are
|
|
only constant expressions in the rows on the IN's rhs, see
|
|
comment above the call to add_key_field() below.
|
|
|
|
Actually, We could in theory do a ref access if the IN rhs
|
|
contained just a single row, but there is a hack in the parser
|
|
causing such IN predicates be parsed as row equalities.
|
|
*/
|
|
Item_row *lhs_row = static_cast<Item_row *>(cond_func->key_item());
|
|
if (is_row_of_local_columns(lhs_row)) {
|
|
for (uint i = 0; i < lhs_row->cols(); ++i) {
|
|
Item *const lhs_item = lhs_row->element_index(i)->real_item();
|
|
assert(lhs_item->type() == Item::FIELD_ITEM);
|
|
Item_field *const lhs_column = static_cast<Item_field *>(lhs_item);
|
|
// j goes from 1 since arguments()[0] is the lhs of IN.
|
|
for (uint j = 1; j < cond_func->argument_count(); ++j) {
|
|
// Here we pick out the i:th column in the j:th row.
|
|
Item *rhs_item = cond_func->arguments()[j];
|
|
assert(rhs_item->type() == Item::ROW_ITEM);
|
|
Item_row *rhs_row = static_cast<Item_row *>(rhs_item);
|
|
assert(rhs_row->cols() == lhs_row->cols());
|
|
Item **rhs_expr_ptr = rhs_row->addr(i);
|
|
/*
|
|
add_key_field() will write a Key_field on each call
|
|
here, but we don't care, it will never be used. We only
|
|
call it for the side effect: update JOIN_TAB::const_keys
|
|
so the range optimizer can be invoked. We pass a
|
|
scrap buffer and pointer here.
|
|
*/
|
|
Key_field scrap_key_field = **key_fields;
|
|
Key_field *scrap_key_field_ptr = &scrap_key_field;
|
|
if (add_key_field(thd, &scrap_key_field_ptr, *and_level,
|
|
cond_func, lhs_column,
|
|
true, // eq_func
|
|
rhs_expr_ptr,
|
|
1, // Number of expressions: one
|
|
usable_tables,
|
|
nullptr)) // sargables
|
|
return true;
|
|
// The pointer is not supposed to increase by more than one.
|
|
assert(scrap_key_field_ptr <= &scrap_key_field + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case Item_func::OPTIMIZE_OP: {
|
|
bool equal_func = (cond_func->functype() == Item_func::EQ_FUNC ||
|
|
cond_func->functype() == Item_func::EQUAL_FUNC);
|
|
|
|
if (is_local_field(cond_func->arguments()[0])) {
|
|
if (add_key_equal_fields(
|
|
thd, key_fields, *and_level, cond_func,
|
|
(Item_field *)(cond_func->arguments()[0])->real_item(),
|
|
equal_func, cond_func->arguments() + 1, 1, usable_tables,
|
|
sargables))
|
|
return true;
|
|
} else {
|
|
Item *real_item = cond_func->arguments()[0]->real_item();
|
|
if (real_item->type() == Item::FUNC_ITEM) {
|
|
Item_func *func_item = down_cast<Item_func *>(real_item);
|
|
if (func_item->functype() == Item_func::COLLATE_FUNC) {
|
|
Item *key_item = func_item->key_item();
|
|
if (key_item->type() == Item::FIELD_ITEM) {
|
|
if (add_key_equal_fields(thd, key_fields, *and_level, cond_func,
|
|
down_cast<Item_field *>(key_item),
|
|
equal_func, cond_func->arguments() + 1,
|
|
1, usable_tables, sargables))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (is_local_field(cond_func->arguments()[1]) &&
|
|
cond_func->functype() != Item_func::LIKE_FUNC) {
|
|
if (add_key_equal_fields(
|
|
thd, key_fields, *and_level, cond_func,
|
|
(Item_field *)(cond_func->arguments()[1])->real_item(),
|
|
equal_func, cond_func->arguments(), 1, usable_tables,
|
|
sargables))
|
|
return true;
|
|
} else {
|
|
Item *real_item = cond_func->arguments()[1]->real_item();
|
|
if (real_item->type() == Item::FUNC_ITEM) {
|
|
Item_func *func_item = down_cast<Item_func *>(real_item);
|
|
if (func_item->functype() == Item_func::COLLATE_FUNC) {
|
|
Item *key_item = func_item->key_item();
|
|
if (key_item->type() == Item::FIELD_ITEM) {
|
|
if (add_key_equal_fields(thd, key_fields, *and_level, cond_func,
|
|
down_cast<Item_field *>(key_item),
|
|
equal_func, cond_func->arguments(), 1,
|
|
usable_tables, sargables))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case Item_func::OPTIMIZE_NULL:
|
|
/* column_name IS [NOT] NULL */
|
|
if (is_local_field(cond_func->arguments()[0]) &&
|
|
!cond_func->is_outer_reference()) {
|
|
Item *tmp = new Item_null;
|
|
if (tmp == nullptr) return true;
|
|
if (add_key_equal_fields(
|
|
thd, key_fields, *and_level, cond_func,
|
|
(Item_field *)(cond_func->arguments()[0])->real_item(),
|
|
cond_func->functype() == Item_func::ISNULL_FUNC, &tmp, 1,
|
|
usable_tables, sargables))
|
|
return true;
|
|
}
|
|
break;
|
|
case Item_func::OPTIMIZE_EQUAL:
|
|
Item_equal *item_equal = (Item_equal *)cond;
|
|
Item *const_item = item_equal->const_arg();
|
|
if (const_item) {
|
|
/*
|
|
For each field field1 from item_equal consider the equality
|
|
field1=const_item as a condition allowing an index access of the table
|
|
with field1 by the keys value of field1.
|
|
*/
|
|
for (Item_field &item : item_equal->get_fields()) {
|
|
if (add_key_field(thd, key_fields, *and_level, cond_func, &item, true,
|
|
&const_item, 1, usable_tables, sargables))
|
|
return true;
|
|
}
|
|
} else {
|
|
/*
|
|
Consider all pairs of different fields included into item_equal.
|
|
For each of them (field1, field1) consider the equality
|
|
field1=field2 as a condition allowing an index access of the table
|
|
with field1 by the keys value of field2.
|
|
*/
|
|
for (Item_field &outer : item_equal->get_fields()) {
|
|
for (Item_field &inner : item_equal->get_fields()) {
|
|
if (!outer.field->eq(inner.field)) {
|
|
Item *inner_ptr = &inner;
|
|
if (add_key_field(thd, key_fields, *and_level, cond_func, &outer,
|
|
true, &inner_ptr, 1, usable_tables, sargables))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Add all keys with uses 'field' for some keypart
|
|
If field->and_level != and_level then only mark key_part as const_part
|
|
|
|
RETURN
|
|
0 - OK
|
|
1 - Out of memory.
|
|
*/
|
|
|
|
static bool add_key_part(Key_use_array *keyuse_array, Key_field *key_field) {
|
|
if (key_field->eq_func && !(key_field->optimize & KEY_OPTIMIZE_EXISTS)) {
|
|
const Field *const field = key_field->item_field->field;
|
|
Table_ref *const tl = key_field->item_field->table_ref;
|
|
TABLE *const table = tl->table;
|
|
|
|
for (uint key = 0; key < table->s->keys; key++) {
|
|
if (!(table->keys_in_use_for_query.is_set(key))) continue;
|
|
if (table->key_info[key].flags & (HA_FULLTEXT | HA_SPATIAL))
|
|
continue; // ToDo: ft-keys in non-ft queries. SerG
|
|
|
|
const uint key_parts = actual_key_parts(&table->key_info[key]);
|
|
for (uint part = 0; part < key_parts; part++) {
|
|
if (field->eq(table->key_info[key].key_part[part].field)) {
|
|
const Key_use keyuse(tl, key_field->val,
|
|
key_field->val->used_tables(), key, part,
|
|
key_field->optimize & KEY_OPTIMIZE_REF_OR_NULL,
|
|
(key_part_map)1 << part,
|
|
~(ha_rows)0, // will be set in optimize_keyuse
|
|
key_field->null_rejecting, key_field->cond_guard,
|
|
key_field->sj_pred_no);
|
|
if (keyuse_array->push_back(keyuse))
|
|
return true; /* purecov: inspected */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Function parses WHERE condition and add key_use for FT index
|
|
into key_use array if suitable MATCH function is found.
|
|
Condition should be a set of AND expression, OR is not supported.
|
|
MATCH function should be a part of simple expression.
|
|
Simple expression is MATCH only function or MATCH is a part of
|
|
comparison expression ('>=' or '>' operations are supported).
|
|
It also sets FT_HINTS values(op_type, op_value).
|
|
|
|
@param keyuse_array Key_use array
|
|
@param cond WHERE condition
|
|
@param usable_tables usable tables
|
|
@param simple_match_expr true if this is the first call false otherwise.
|
|
if MATCH function is found at first call it means
|
|
that MATCH is simple expression, otherwise, in case
|
|
of AND/OR condition this parameter will be false.
|
|
|
|
@retval
|
|
true if FT key was added to Key_use array
|
|
@retval
|
|
false if no key was added to Key_use array
|
|
|
|
*/
|
|
|
|
static bool add_ft_keys(Key_use_array *keyuse_array, Item *cond,
|
|
table_map usable_tables, bool simple_match_expr) {
|
|
Item_func_match *cond_func = nullptr;
|
|
|
|
if (!cond) return false;
|
|
|
|
assert(cond->is_bool_func());
|
|
|
|
if (cond->type() == Item::FUNC_ITEM) {
|
|
Item_func *func = down_cast<Item_func *>(cond);
|
|
Item_func::Functype functype = func->functype();
|
|
if (functype == Item_func::MATCH_FUNC) {
|
|
func = down_cast<Item_func *>(func->arguments()[0]);
|
|
functype = func->functype();
|
|
}
|
|
enum ft_operation op_type = FT_OP_NO;
|
|
double op_value = 0.0;
|
|
if (functype == Item_func::FT_FUNC) {
|
|
cond_func = down_cast<Item_func_match *>(func)->get_master();
|
|
cond_func->set_hints_op(op_type, op_value);
|
|
} else if (func->arg_count == 2) {
|
|
Item *arg0 = func->arguments()[0];
|
|
Item *arg1 = func->arguments()[1];
|
|
if (arg1->const_item() && is_function_of_type(arg0, Item_func::FT_FUNC) &&
|
|
((functype == Item_func::GE_FUNC &&
|
|
(op_value = arg1->val_real()) > 0) ||
|
|
(functype == Item_func::GT_FUNC &&
|
|
(op_value = arg1->val_real()) >= 0))) {
|
|
cond_func = down_cast<Item_func_match *>(arg0)->get_master();
|
|
if (functype == Item_func::GE_FUNC)
|
|
op_type = FT_OP_GE;
|
|
else if (functype == Item_func::GT_FUNC)
|
|
op_type = FT_OP_GT;
|
|
cond_func->set_hints_op(op_type, op_value);
|
|
} else if (arg0->const_item() &&
|
|
is_function_of_type(arg1, Item_func::FT_FUNC) &&
|
|
((functype == Item_func::LE_FUNC &&
|
|
(op_value = arg0->val_real()) > 0) ||
|
|
(functype == Item_func::LT_FUNC &&
|
|
(op_value = arg0->val_real()) >= 0))) {
|
|
cond_func = down_cast<Item_func_match *>(arg1)->get_master();
|
|
if (functype == Item_func::LE_FUNC)
|
|
op_type = FT_OP_GE;
|
|
else if (functype == Item_func::LT_FUNC)
|
|
op_type = FT_OP_GT;
|
|
cond_func->set_hints_op(op_type, op_value);
|
|
}
|
|
}
|
|
} else if (cond->type() == Item::COND_ITEM) {
|
|
List_iterator_fast<Item> li(*down_cast<Item_cond *>(cond)->argument_list());
|
|
|
|
if (down_cast<Item_cond *>(cond)->functype() == Item_func::COND_AND_FUNC) {
|
|
Item *item;
|
|
while ((item = li++))
|
|
if (add_ft_keys(keyuse_array, item, usable_tables, false)) return true;
|
|
}
|
|
}
|
|
|
|
if (!cond_func || cond_func->key == NO_SUCH_KEY ||
|
|
!(usable_tables & cond_func->table_ref->map()))
|
|
return false;
|
|
|
|
Table_ref *tbl = cond_func->table_ref;
|
|
if (!tbl->table->keys_in_use_for_query.is_set(cond_func->key)) return false;
|
|
|
|
cond_func->set_simple_expression(simple_match_expr);
|
|
|
|
const Key_use keyuse(tbl, cond_func, cond_func->key_item()->used_tables(),
|
|
cond_func->key, FT_KEYPART,
|
|
0, // optimize
|
|
0, // keypart_map
|
|
~(ha_rows)0, // ref_table_rows
|
|
false, // null_rejecting
|
|
nullptr, // cond_guard
|
|
UINT_MAX); // sj_pred_no
|
|
tbl->table->reginfo.join_tab->keys().set_bit(cond_func->key);
|
|
return keyuse_array->push_back(keyuse);
|
|
}
|
|
|
|
/**
|
|
Compares two keyuse elements.
|
|
|
|
@param a first Key_use element
|
|
@param b second Key_use element
|
|
|
|
Compare Key_use elements so that they are sorted as follows:
|
|
-# By table.
|
|
-# By key for each table.
|
|
-# By keypart for each key.
|
|
-# Const values.
|
|
-# Ref_or_null.
|
|
|
|
@retval true If a < b.
|
|
@retval false If a >= b.
|
|
*/
|
|
static bool sort_keyuse(const Key_use &a, const Key_use &b) {
|
|
if (a.table_ref->tableno() != b.table_ref->tableno())
|
|
return a.table_ref->tableno() < b.table_ref->tableno();
|
|
if (a.key != b.key) return a.key < b.key;
|
|
if (a.keypart != b.keypart) return a.keypart < b.keypart;
|
|
// Place const values before other ones
|
|
const bool a_const = a.used_tables & ~OUTER_REF_TABLE_BIT;
|
|
const bool b_const = b.used_tables & ~OUTER_REF_TABLE_BIT;
|
|
if (a_const != b_const) return b_const;
|
|
/* Place rows that are not 'OPTIMIZE_REF_OR_NULL' first */
|
|
return (a.optimize & KEY_OPTIMIZE_REF_OR_NULL) <
|
|
(b.optimize & KEY_OPTIMIZE_REF_OR_NULL);
|
|
}
|
|
|
|
/*
|
|
Add to Key_field array all 'ref' access candidates within nested join.
|
|
|
|
This function populates Key_field array with entries generated from the
|
|
ON condition of the given nested join, and does the same for nested joins
|
|
contained within this nested join.
|
|
|
|
@param thd session context
|
|
@param[in] nested_join_table Nested join pseudo-table to process
|
|
@param[in,out] end End of the key field array
|
|
@param[in,out] and_level And-level
|
|
@param[in,out] sargables Array of found sargable candidates
|
|
|
|
@returns false if success, true if error
|
|
|
|
@note
|
|
We can add accesses to the tables that are direct children of this nested
|
|
join (1), and are not inner tables w.r.t their neighbours (2).
|
|
|
|
Example for #1 (outer brackets pair denotes nested join this function is
|
|
invoked for):
|
|
@code
|
|
... LEFT JOIN (t1 LEFT JOIN (t2 ... ) ) ON cond
|
|
@endcode
|
|
Example for #2:
|
|
@code
|
|
... LEFT JOIN (t1 LEFT JOIN t2 ) ON cond
|
|
@endcode
|
|
In examples 1-2 for condition cond, we can add 'ref' access candidates to
|
|
t1 only.
|
|
Example #3:
|
|
@code
|
|
... LEFT JOIN (t1, t2 LEFT JOIN t3 ON inner_cond) ON cond
|
|
@endcode
|
|
Here we can add 'ref' access candidates for t1 and t2, but not for t3.
|
|
*/
|
|
|
|
static bool add_key_fields_for_nj(THD *thd, JOIN *join,
|
|
Table_ref *nested_join_table, Key_field **end,
|
|
uint *and_level, SARGABLE_PARAM **sargables) {
|
|
mem_root_deque<Table_ref *> &join_list =
|
|
nested_join_table->nested_join->m_tables;
|
|
auto li = join_list.begin();
|
|
auto li_end = join_list.end();
|
|
auto li2 = join_list.begin();
|
|
auto li2_end = join_list.end();
|
|
bool have_another = false;
|
|
table_map tables = 0;
|
|
Table_ref *table;
|
|
|
|
while ((table = (li != li_end) ? *li++ : nullptr) ||
|
|
(have_another && li2 != join_list.end() &&
|
|
(li = li2, li_end = li2_end, have_another = false,
|
|
(li != li_end) && (table = *li++)))) {
|
|
if (table->nested_join) {
|
|
if (!table->join_cond_optim()) {
|
|
/* It's a semi-join nest. Walk into it as if it wasn't a nest */
|
|
have_another = true;
|
|
li2 = li;
|
|
li2_end = li_end;
|
|
li = table->nested_join->m_tables.begin();
|
|
li_end = table->nested_join->m_tables.end();
|
|
} else {
|
|
if (add_key_fields_for_nj(thd, join, table, end, and_level, sargables))
|
|
return true;
|
|
}
|
|
} else if (!table->join_cond_optim())
|
|
tables |= table->map();
|
|
}
|
|
if (nested_join_table->join_cond_optim()) {
|
|
if (add_key_fields(thd, join, end, and_level,
|
|
nested_join_table->join_cond_optim(), tables, sargables))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// @} (end of group RefOptimizerModule)
|
|
|
|
/**
|
|
Check for the presence of AGGFN(DISTINCT a) queries that may be subject
|
|
to loose index scan.
|
|
|
|
|
|
Check if the query is a subject to AGGFN(DISTINCT) using loose index scan
|
|
(GroupIndexSkipScanIterator).
|
|
Optionally (if out_args is supplied) will push the arguments of
|
|
AGGFN(DISTINCT) to the list
|
|
|
|
Check for every COUNT(DISTINCT), AVG(DISTINCT) or
|
|
SUM(DISTINCT). These can be resolved by Loose Index Scan as long
|
|
as all the aggregate distinct functions refer to the same
|
|
fields. Thus:
|
|
|
|
SELECT AGGFN(DISTINCT a, b), AGGFN(DISTINCT b, a)... => can use LIS
|
|
SELECT AGGFN(DISTINCT a), AGGFN(DISTINCT a) ... => can use LIS
|
|
SELECT AGGFN(DISTINCT a, b), AGGFN(DISTINCT a) ... => cannot use LIS
|
|
SELECT AGGFN(DISTINCT a), AGGFN(DISTINCT b) ... => cannot use LIS
|
|
etc.
|
|
|
|
@param join the join to check
|
|
@param[out] out_args Collect the arguments of the aggregate functions
|
|
to a list. We don't worry about duplicates as
|
|
these will be sorted out later in
|
|
get_best_group_skip_scan.
|
|
|
|
@return does the query qualify for indexed AGGFN(DISTINCT)
|
|
@retval true it does
|
|
@retval false AGGFN(DISTINCT) must apply distinct in it.
|
|
*/
|
|
|
|
bool is_indexed_agg_distinct(JOIN *join,
|
|
mem_root_deque<Item_field *> *out_args) {
|
|
Item_sum **sum_item_ptr;
|
|
bool result = false;
|
|
Field_map first_aggdistinct_fields;
|
|
|
|
if (join->query_block->original_tables_map > 1 || /* reference more than 1
|
|
table originally */
|
|
join->select_distinct || /* or a DISTINCT */
|
|
join->query_block->is_non_primitive_grouped()) /* Check (B3) for
|
|
non-primitive grouping */
|
|
return false;
|
|
|
|
if (join->make_sum_func_list(*join->fields, true)) return false;
|
|
|
|
for (sum_item_ptr = join->sum_funcs; *sum_item_ptr; sum_item_ptr++) {
|
|
Item_sum *sum_item = *sum_item_ptr;
|
|
Field_map cur_aggdistinct_fields;
|
|
Item *expr;
|
|
/* aggregate is not AGGFN(DISTINCT) or more than 1 argument to it */
|
|
switch (sum_item->sum_func()) {
|
|
case Item_sum::MIN_FUNC:
|
|
case Item_sum::MAX_FUNC:
|
|
continue;
|
|
case Item_sum::COUNT_DISTINCT_FUNC:
|
|
break;
|
|
case Item_sum::AVG_DISTINCT_FUNC:
|
|
case Item_sum::SUM_DISTINCT_FUNC:
|
|
if (sum_item->argument_count() == 1) break;
|
|
[[fallthrough]];
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
for (uint i = 0; i < sum_item->argument_count(); i++) {
|
|
expr = sum_item->get_arg(i);
|
|
/* The AGGFN(DISTINCT) arg is not an attribute? */
|
|
if (expr->real_item()->type() != Item::FIELD_ITEM) return false;
|
|
|
|
Item_field *item = static_cast<Item_field *>(expr->real_item());
|
|
if (out_args) out_args->push_back(item);
|
|
|
|
cur_aggdistinct_fields.set_bit(item->field->field_index());
|
|
result = true;
|
|
}
|
|
/*
|
|
If there are multiple aggregate functions, make sure that they all
|
|
refer to exactly the same set of columns.
|
|
*/
|
|
if (first_aggdistinct_fields.is_clear_all())
|
|
first_aggdistinct_fields.merge(cur_aggdistinct_fields);
|
|
else if (first_aggdistinct_fields != cur_aggdistinct_fields)
|
|
return false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
Print keys that were appended to join_tab->const_keys because they
|
|
can be used for GROUP BY or DISTINCT to the optimizer trace.
|
|
|
|
@param trace The optimizer trace context we're adding info to
|
|
@param join_tab The table the indexes cover
|
|
@param new_keys The keys that are considered useful because they can
|
|
be used for GROUP BY or DISTINCT
|
|
@param cause Zero-terminated string with reason for adding indexes
|
|
to const_keys
|
|
|
|
@see add_group_and_distinct_keys()
|
|
*/
|
|
static void trace_indexes_added_group_distinct(Opt_trace_context *trace,
|
|
const JOIN_TAB *join_tab,
|
|
const Key_map new_keys,
|
|
const char *cause) {
|
|
if (likely(!trace->is_started())) return;
|
|
|
|
KEY *key_info = join_tab->table()->key_info;
|
|
const Key_map existing_keys = join_tab->const_keys;
|
|
const uint nbrkeys = join_tab->table()->s->keys;
|
|
|
|
Opt_trace_object trace_summary(trace, "const_keys_added");
|
|
{
|
|
Opt_trace_array trace_key(trace, "keys");
|
|
for (uint j = 0; j < nbrkeys; j++)
|
|
if (new_keys.is_set(j) && !existing_keys.is_set(j))
|
|
trace_key.add_utf8(key_info[j].name);
|
|
}
|
|
trace_summary.add_alnum("cause", cause);
|
|
}
|
|
|
|
/**
|
|
Discover the indexes that might be used for GROUP BY or DISTINCT queries or
|
|
indexes that might be used for SKIP SCAN.
|
|
|
|
If the query has a GROUP BY clause, find all indexes that contain
|
|
all GROUP BY fields, and add those indexes to join_tab->const_keys
|
|
and join_tab->keys.
|
|
|
|
If the query has a DISTINCT clause, find all indexes that contain
|
|
all SELECT fields, and add those indexes to join_tab->const_keys and
|
|
join_tab->keys. This allows later on such queries to be processed by
|
|
a GroupIndexSkipScanIterator.
|
|
|
|
If the query does not have GROUP BY clause or any aggregate function
|
|
the function collects possible keys to use for skip scan access.
|
|
|
|
Note that indexes that are not usable for resolving GROUP
|
|
BY/DISTINCT may also be added in some corner cases. For example, an
|
|
index covering 'a' and 'b' is not usable for the following query but
|
|
is still added: "SELECT DISTINCT a+b FROM t1". This is not a big
|
|
issue because a) although the optimizer will consider using the
|
|
index, it will not chose it (so minor calculation cost added but not
|
|
wrong result) and b) it applies only to corner cases.
|
|
|
|
@param join the current join
|
|
@param join_tab joined table
|
|
*/
|
|
|
|
static void add_loose_index_scan_and_skip_scan_keys(JOIN *join,
|
|
JOIN_TAB *join_tab) {
|
|
assert(join_tab->const_keys.is_subset(join_tab->keys()));
|
|
|
|
mem_root_deque<Item_field *> indexed_fields(join->thd->mem_root);
|
|
ORDER *cur_group;
|
|
const char *cause;
|
|
|
|
/* Find the indexes that might be used for skip scan queries. */
|
|
if (join->where_cond != nullptr &&
|
|
join->query_block->original_tables_map == 1 &&
|
|
join->query_block->original_tables_map == join_tab->table_ref->map() &&
|
|
join->group_list.empty() &&
|
|
!is_indexed_agg_distinct(join, &indexed_fields) &&
|
|
!join->select_distinct) {
|
|
const bool use_skip_scan =
|
|
hint_table_state(join->thd, join_tab->table_ref, SKIP_SCAN_HINT_ENUM,
|
|
OPTIMIZER_SKIP_SCAN);
|
|
/*
|
|
if skip_scan for a table is off, and the hint is applicable to all
|
|
indexes, skip processing for possible keys. If the hint has index
|
|
mentioned then skip_scan can be used with other indexes.
|
|
*/
|
|
if (!use_skip_scan && join_tab->table_ref->opt_hints_table != nullptr &&
|
|
join_tab->table_ref->opt_hints_table
|
|
->get_compound_key_hint(SKIP_SCAN_HINT_ENUM)
|
|
->is_key_map_clear_all())
|
|
return;
|
|
join->where_cond->walk(&Item::collect_item_field_processor,
|
|
enum_walk::POSTFIX, (uchar *)&indexed_fields);
|
|
Key_map possible_keys;
|
|
possible_keys.set_all();
|
|
join_tab->skip_scan_keys.clear_all();
|
|
for (Item_field *cur_item : indexed_fields) {
|
|
if (cur_item->used_tables() != join_tab->table_ref->map()) return;
|
|
possible_keys.intersect(cur_item->field->part_of_key);
|
|
}
|
|
join_tab->skip_scan_keys.merge(possible_keys);
|
|
cause = "skip_scan";
|
|
return;
|
|
}
|
|
|
|
if (!join->group_list.empty()) {
|
|
/* Collect all query fields referenced in the GROUP clause. */
|
|
for (cur_group = join->group_list.order; cur_group;
|
|
cur_group = cur_group->next)
|
|
(*cur_group->item)
|
|
->walk(&Item::collect_item_field_processor, enum_walk::POSTFIX,
|
|
(uchar *)&indexed_fields);
|
|
cause = "group_by";
|
|
} else if (join->select_distinct) {
|
|
/* Collect all query fields referenced in the SELECT clause. */
|
|
for (Item *item : VisibleFields(*join->fields)) {
|
|
item->walk(&Item::collect_item_field_processor, enum_walk::POSTFIX,
|
|
(uchar *)&indexed_fields);
|
|
}
|
|
cause = "distinct";
|
|
} else if (join->tmp_table_param.sum_func_count &&
|
|
is_indexed_agg_distinct(join, &indexed_fields)) {
|
|
/*
|
|
SELECT list with AGGFN(distinct col). The query qualifies for
|
|
loose index scan, and is_indexed_agg_distinct() has already
|
|
collected all referenced fields into indexed_fields.
|
|
*/
|
|
join->streaming_aggregation = true;
|
|
cause = "indexed_distinct_aggregate";
|
|
} else
|
|
return;
|
|
|
|
if (indexed_fields.empty()) return;
|
|
|
|
Key_map possible_keys = join_tab->table()->keys_in_use_for_query;
|
|
possible_keys.merge(join_tab->table()->keys_in_use_for_group_by);
|
|
|
|
/* Intersect the keys of all group fields. */
|
|
for (Item_field *cur_item : indexed_fields) {
|
|
if (cur_item->used_tables() != join_tab->table_ref->map()) {
|
|
/*
|
|
Doing GROUP BY or DISTINCT on a field in another table so no
|
|
index in this table is usable
|
|
*/
|
|
return;
|
|
} else
|
|
possible_keys.intersect(cur_item->field->part_of_key);
|
|
}
|
|
|
|
/*
|
|
At this point, possible_keys has key bits set only for usable
|
|
indexes because indexed_fields is non-empty and if any of the
|
|
fields belong to a different table the function would exit in the
|
|
loop above.
|
|
*/
|
|
|
|
if (!possible_keys.is_clear_all() &&
|
|
!possible_keys.is_subset(join_tab->const_keys)) {
|
|
trace_indexes_added_group_distinct(&join->thd->opt_trace, join_tab,
|
|
possible_keys, cause);
|
|
join_tab->const_keys.merge(possible_keys);
|
|
join_tab->keys().merge(possible_keys);
|
|
}
|
|
|
|
assert(join_tab->const_keys.is_subset(join_tab->keys()));
|
|
}
|
|
|
|
/**
|
|
Update keyuse array with all possible keys we can use to fetch rows.
|
|
|
|
@param thd session context
|
|
@param[out] keyuse Put here ordered array of Key_use structures
|
|
@param join_tab Array in table number order
|
|
@param tables Number of tables in join
|
|
@param cond WHERE condition (note that the function analyzes
|
|
join_tab[i]->join_cond() too)
|
|
@param normal_tables Tables not inner w.r.t some outer join (ones
|
|
for which we can make ref access based the WHERE
|
|
clause)
|
|
@param query_block current SELECT
|
|
@param[out] sargables Array of found sargable candidates
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
static bool update_ref_and_keys(THD *thd, Key_use_array *keyuse,
|
|
JOIN_TAB *join_tab, uint tables, Item *cond,
|
|
table_map normal_tables,
|
|
Query_block *query_block,
|
|
SARGABLE_PARAM **sargables) {
|
|
assert(cond == nullptr || cond->is_bool_func());
|
|
uint and_level, i;
|
|
Key_field *key_fields, *end, *field;
|
|
size_t sz;
|
|
const uint m = max(query_block->max_equal_elems, 1U);
|
|
JOIN *const join = query_block->join;
|
|
/*
|
|
We use the same piece of memory to store both Key_field
|
|
and SARGABLE_PARAM structure.
|
|
Key_field values are placed at the beginning this memory
|
|
while SARGABLE_PARAM values are put at the end.
|
|
All predicates that are used to fill arrays of Key_field
|
|
and SARGABLE_PARAM structures have at most 2 arguments
|
|
except BETWEEN predicates that have 3 arguments and
|
|
IN predicates.
|
|
This any predicate if it's not BETWEEN/IN can be used
|
|
directly to fill at most 2 array elements, either of Key_field
|
|
or SARGABLE_PARAM type. For a BETWEEN predicate 3 elements
|
|
can be filled as this predicate is considered as
|
|
saragable with respect to each of its argument.
|
|
An IN predicate can require at most 1 element as currently
|
|
it is considered as sargable only for its first argument.
|
|
Multiple equality can add elements that are filled after
|
|
substitution of field arguments by equal fields. There
|
|
can be not more than query_block->max_equal_elems such
|
|
substitutions.
|
|
*/
|
|
sz = max(sizeof(Key_field), sizeof(SARGABLE_PARAM)) *
|
|
(((query_block->cond_count + 1) * 2 + query_block->between_count) * m +
|
|
1);
|
|
if (!(key_fields = (Key_field *)thd->alloc(sz)))
|
|
return true; /* purecov: inspected */
|
|
and_level = 0;
|
|
field = end = key_fields;
|
|
*sargables = (SARGABLE_PARAM *)key_fields +
|
|
(sz - sizeof((*sargables)[0].field)) / sizeof(SARGABLE_PARAM);
|
|
/* set a barrier for the array of SARGABLE_PARAM */
|
|
(*sargables)[0].field = nullptr;
|
|
|
|
if (cond) {
|
|
if (add_key_fields(thd, join, &end, &and_level, cond, normal_tables,
|
|
sargables))
|
|
return true;
|
|
|
|
// The relevant secondary engines don't support antijoin, so don't enable
|
|
// this optimization for them.
|
|
if (thd->secondary_engine_optimization() !=
|
|
Secondary_engine_optimization::SECONDARY) {
|
|
for (Key_field *fld = field; fld != end; fld++) {
|
|
/* Mark that we can optimize LEFT JOIN */
|
|
if (fld->val->type() == Item::NULL_ITEM &&
|
|
!fld->item_field->field->is_nullable()) {
|
|
/*
|
|
Example:
|
|
SELECT * FROM t1 LEFT JOIN t2 ON t1.a=t2.a WHERE t2.a IS NULL;
|
|
this just wants rows of t1 where t1.a does not exist in t2.
|
|
*/
|
|
fld->item_field->field->table->reginfo.not_exists_optimize = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < tables; i++) {
|
|
/*
|
|
Block the creation of keys for inner tables of outer joins.
|
|
Here only the outer joins that can not be converted to
|
|
inner joins are left and all nests that can be eliminated
|
|
are flattened.
|
|
In the future when we introduce conditional accesses
|
|
for inner tables in outer joins these keys will be taken
|
|
into account as well.
|
|
*/
|
|
if (join_tab[i].join_cond()) {
|
|
if (add_key_fields(thd, join, &end, &and_level, join_tab[i].join_cond(),
|
|
join_tab[i].table_ref->map(), sargables))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* Process ON conditions for the nested joins */
|
|
for (Table_ref *tl : query_block->m_table_nest) {
|
|
if (tl->nested_join &&
|
|
add_key_fields_for_nj(thd, join, tl, &end, &and_level, sargables))
|
|
return true;
|
|
}
|
|
|
|
/* Generate keys descriptions for derived tables */
|
|
if (query_block->materialized_derived_table_count) {
|
|
if (join->generate_derived_keys()) return true;
|
|
}
|
|
/* fill keyuse with found key parts */
|
|
for (; field != end; field++) {
|
|
if (add_key_part(keyuse, field)) return true;
|
|
}
|
|
|
|
if (query_block->ftfunc_list->elements) {
|
|
if (add_ft_keys(keyuse, cond, normal_tables, true)) return true;
|
|
}
|
|
|
|
/*
|
|
Sort the array of possible keys and remove the following key parts:
|
|
- ref if there is a keypart which is a ref and a const.
|
|
(e.g. if there is a key(a,b) and the clause is a=3 and b=7 and b=t2.d,
|
|
then we skip the key part corresponding to b=t2.d)
|
|
- keyparts without previous keyparts
|
|
(e.g. if there is a key(a,b,c) but only b < 5 (or a=2 and c < 3) is
|
|
used in the query, we drop the partial key parts from consideration).
|
|
Special treatment for ft-keys.
|
|
*/
|
|
if (!keyuse->empty()) {
|
|
Key_use *save_pos, *use;
|
|
|
|
std::stable_sort(keyuse->begin(), keyuse->begin() + keyuse->size(),
|
|
sort_keyuse);
|
|
|
|
const Key_use key_end(nullptr, nullptr, 0, 0, 0, 0, 0, 0, false, nullptr,
|
|
0);
|
|
if (keyuse->push_back(key_end)) // added for easy testing
|
|
return true;
|
|
|
|
use = save_pos = keyuse->begin();
|
|
const Key_use *prev = &key_end;
|
|
bool found_eq_constant = false;
|
|
for (i = 0; i < keyuse->size() - 1; i++, use++) {
|
|
TABLE *const table = use->table_ref->table;
|
|
if (use->val->const_for_execution() &&
|
|
use->optimize != KEY_OPTIMIZE_REF_OR_NULL)
|
|
table->const_key_parts[use->key] |= use->keypart_map;
|
|
if (use->keypart != FT_KEYPART) {
|
|
if (use->key == prev->key && use->table_ref == prev->table_ref) {
|
|
if (prev->keypart + 1 < use->keypart ||
|
|
(prev->keypart == use->keypart && found_eq_constant))
|
|
continue; /* remove */
|
|
} else if (use->keypart != 0) // First found must be 0
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
Protect against self assignment.
|
|
The compiler *may* generate a call to memcpy() to do the assignment,
|
|
and that is undefined behaviour (memory overlap).
|
|
*/
|
|
if (save_pos != use) *save_pos = *use;
|
|
prev = use;
|
|
found_eq_constant = use->val->const_for_execution();
|
|
/* Save ptr to first use */
|
|
if (!table->reginfo.join_tab->keyuse())
|
|
table->reginfo.join_tab->set_keyuse(save_pos);
|
|
table->reginfo.join_tab->checked_keys.set_bit(use->key);
|
|
save_pos++;
|
|
}
|
|
i = (uint)(save_pos - keyuse->begin());
|
|
keyuse->at(i) = key_end;
|
|
keyuse->chop(i);
|
|
}
|
|
print_keyuse_array(thd, &thd->opt_trace, keyuse);
|
|
/*
|
|
Number of functions here call val_x() methods, which might throw an error.
|
|
Catch those errors here.
|
|
*/
|
|
return thd->is_error();
|
|
}
|
|
|
|
/**
|
|
Create a keyuse array for a table with a primary key.
|
|
To be used when creating a materialized temporary table.
|
|
|
|
@param thd THD pointer, for memory allocation
|
|
@param keyparts Number of key parts in the primary key
|
|
@param fields fields
|
|
@param outer_exprs List of items used for key lookup
|
|
|
|
@return Pointer to created keyuse array, or NULL if error
|
|
*/
|
|
Key_use_array *create_keyuse_for_table(
|
|
THD *thd, uint keyparts, Item_field **fields,
|
|
const mem_root_deque<Item *> &outer_exprs) {
|
|
void *mem = thd->alloc(sizeof(Key_use_array));
|
|
if (!mem) return nullptr;
|
|
Key_use_array *keyuses = new (mem) Key_use_array(thd->mem_root);
|
|
|
|
auto outer_expr_it = outer_exprs.begin();
|
|
|
|
for (uint keypartno = 0; keypartno < keyparts; keypartno++) {
|
|
Item *const item = *outer_expr_it++;
|
|
Key_field key_field(fields[keypartno], item, 0, 0, true,
|
|
// null_rejecting must be true for field items only,
|
|
// add_not_null_conds() is incapable of handling
|
|
// other item types.
|
|
(item->type() == Item::FIELD_ITEM), nullptr, UINT_MAX);
|
|
if (add_key_part(keyuses, &key_field)) return nullptr;
|
|
}
|
|
const Key_use key_end(nullptr, nullptr, 0, 0, 0, 0, 0, 0, false, nullptr, 0);
|
|
if (keyuses->push_back(key_end)) // added for easy testing
|
|
return nullptr;
|
|
|
|
return keyuses;
|
|
}
|
|
|
|
/**
|
|
Move const tables first in the position array.
|
|
|
|
Increment the number of const tables and set same basic properties for the
|
|
const table.
|
|
A const table looked up by a key has type JT_CONST.
|
|
A const table with a single row has type JT_SYSTEM.
|
|
|
|
@param tab Table that is designated as a const table
|
|
@param key The key definition to use for this table (NULL if table scan)
|
|
*/
|
|
|
|
void JOIN::mark_const_table(JOIN_TAB *tab, Key_use *key) {
|
|
POSITION *const position = positions + const_tables;
|
|
position->table = tab;
|
|
position->key = key;
|
|
position->rows_fetched = 1.0; // This is a const table
|
|
position->filter_effect = 1.0;
|
|
position->prefix_rowcount = 1.0;
|
|
position->read_cost = 0.0;
|
|
position->ref_depend_map = 0;
|
|
position->loosescan_key = MAX_KEY; // Not a LooseScan
|
|
position->sj_strategy = SJ_OPT_NONE;
|
|
positions->use_join_buffer = false;
|
|
|
|
// Move the const table as far down as possible in best_ref
|
|
JOIN_TAB **pos = best_ref + const_tables + 1;
|
|
for (JOIN_TAB *next = best_ref[const_tables]; next != tab; pos++) {
|
|
JOIN_TAB *const tmp = pos[0];
|
|
pos[0] = next;
|
|
next = tmp;
|
|
}
|
|
best_ref[const_tables] = tab;
|
|
|
|
tab->set_type(key ? JT_CONST : JT_SYSTEM);
|
|
|
|
const_table_map |= tab->table_ref->map();
|
|
|
|
const_tables++;
|
|
}
|
|
|
|
void JOIN::make_outerjoin_info() {
|
|
DBUG_TRACE;
|
|
|
|
assert(query_block->outer_join);
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
query_block->reset_nj_counters();
|
|
|
|
for (uint i = const_tables; i < tables; ++i) {
|
|
JOIN_TAB *const tab = best_ref[i];
|
|
TABLE *const table = tab->table();
|
|
if (!table) continue;
|
|
|
|
Table_ref *const tbl = tab->table_ref;
|
|
/*
|
|
If 'tbl' is inside a SJ/AJ nest served by materialization, we must
|
|
limit setting first_inner, last_inner and first_upper for join nests
|
|
inside the materialized table. Indeed it is the SJ-tmp table, and not
|
|
'tbl', which interacts with the nests outer to the SJ/AJ nest.
|
|
*/
|
|
const bool sj_mat_inner =
|
|
sj_is_materialize_strategy(tab->get_sj_strategy());
|
|
|
|
if (tbl->outer_join) {
|
|
/*
|
|
Table tab is the only one inner table for outer join.
|
|
(Like table t4 for the table reference t3 LEFT JOIN t4 ON t3.a=t4.a
|
|
is in the query above.)
|
|
*/
|
|
tab->set_last_inner(i);
|
|
tab->set_first_inner(i);
|
|
tab->init_join_cond_ref(tbl);
|
|
tab->cond_equal = tbl->cond_equal;
|
|
/*
|
|
If this outer join nest is embedded in another join nest,
|
|
link the join-tabs:
|
|
*/
|
|
Table_ref *const outer_join_nest = tbl->outer_join_nest();
|
|
if (outer_join_nest) {
|
|
assert(outer_join_nest->nested_join->first_nested != NO_PLAN_IDX);
|
|
if (!sj_mat_inner ||
|
|
(tab->emb_sj_nest->sj_inner_tables &
|
|
best_ref[outer_join_nest->nested_join->first_nested]
|
|
->table_ref->map()))
|
|
tab->set_first_upper(outer_join_nest->nested_join->first_nested);
|
|
}
|
|
}
|
|
for (Table_ref *embedding = tbl->embedding; embedding;
|
|
embedding = embedding->embedding) {
|
|
// When reaching the outer tables of the materialized temporary table,
|
|
// the decoration for this table is complete.
|
|
if (sj_mat_inner && embedding == tab->emb_sj_nest) break;
|
|
// Ignore join nests that are not outer join nests:
|
|
if (!embedding->join_cond_optim()) continue;
|
|
NESTED_JOIN *const nested_join = embedding->nested_join;
|
|
if (!nested_join->nj_counter) {
|
|
/*
|
|
Table tab is the first inner table for nested_join.
|
|
Save reference to it in the nested join structure.
|
|
*/
|
|
nested_join->first_nested = i;
|
|
// The table's condition is set to point to the join nest's condition
|
|
tab->init_join_cond_ref(embedding);
|
|
tab->cond_equal = tbl->cond_equal;
|
|
|
|
Table_ref *const outer_join_nest = embedding->outer_join_nest();
|
|
if (outer_join_nest) {
|
|
assert(outer_join_nest->nested_join->first_nested != NO_PLAN_IDX);
|
|
if (!sj_mat_inner ||
|
|
(tab->emb_sj_nest->sj_inner_tables &
|
|
best_ref[outer_join_nest->nested_join->first_nested]
|
|
->table_ref->map()))
|
|
tab->set_first_upper(outer_join_nest->nested_join->first_nested);
|
|
}
|
|
}
|
|
if (tab->first_inner() == NO_PLAN_IDX)
|
|
tab->set_first_inner(nested_join->first_nested);
|
|
/*
|
|
If including the sj-mat tmp table, this also implicitly
|
|
includes the inner tables of the sj-nest.
|
|
*/
|
|
nested_join->nj_counter +=
|
|
tab->sj_mat_exec() ? tab->sj_mat_exec()->table_count : 1;
|
|
if (nested_join->nj_counter < nested_join->nj_total) break;
|
|
// Table tab is the last inner table for nested join.
|
|
best_ref[nested_join->first_nested]->set_last_inner(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Build a condition guarded by match variables for embedded outer joins.
|
|
When generating a condition for a table as part of an outer join condition
|
|
or the WHERE condition, the table in question may also be part of an
|
|
embedded outer join. In such cases, the condition must be guarded by
|
|
the match variable for this embedded outer join. Such embedded outer joins
|
|
may also be recursively embedded in other joins.
|
|
|
|
The function recursively adds guards for a condition ascending from tab
|
|
to root_tab, which is the first inner table of an outer join,
|
|
or NULL if the condition being handled is the WHERE clause.
|
|
|
|
@param join the current join
|
|
@param idx index of the first inner table for the inner-most outer join
|
|
@param cond the predicate to be guarded (must be set)
|
|
@param root_idx index of the inner table to stop at
|
|
(is NO_PLAN_IDX if this is the WHERE clause)
|
|
|
|
@return
|
|
- pointer to the guarded predicate, if success
|
|
- NULL if error
|
|
*/
|
|
|
|
static Item *add_found_match_trig_cond(JOIN *join, plan_idx idx, Item *cond,
|
|
plan_idx root_idx) {
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(join);
|
|
assert(cond->is_bool_func());
|
|
|
|
for (; idx != root_idx; idx = join->best_ref[idx]->first_upper()) {
|
|
if (!(cond = new Item_func_trig_cond(cond, nullptr, join, idx,
|
|
Item_func_trig_cond::FOUND_MATCH)))
|
|
return nullptr;
|
|
|
|
cond->quick_fix_field();
|
|
cond->update_used_tables();
|
|
}
|
|
|
|
return cond;
|
|
}
|
|
|
|
/**
|
|
Helper for JOIN::attach_join_conditions().
|
|
Attaches bits of 'join_cond' to each table in the range [first_inner,
|
|
last_tab], with proper guards.
|
|
If 'sj_mat_cond' is true, we do not see first_inner (and tables on the same
|
|
level of it) as inner to anything, as they're at the top from the POV of
|
|
the materialization of the tmp table. So, if the SJ-mat nest is A LJ B,
|
|
A will get a part of condition without any guard; B will get another part
|
|
with a guard on A->found_match. It's like pushing a WHERE.
|
|
*/
|
|
bool JOIN::attach_join_condition_to_nest(plan_idx first_inner,
|
|
plan_idx last_tab, Item *join_cond,
|
|
bool is_sj_mat_cond) {
|
|
/*
|
|
Add the constant part of the join condition to the first inner table
|
|
of the outer join.
|
|
*/
|
|
Item *cond =
|
|
make_cond_for_table(thd, join_cond, const_table_map, table_map(0), false);
|
|
if (cond) {
|
|
if (!is_sj_mat_cond) {
|
|
cond = new Item_func_trig_cond(cond, nullptr, this, first_inner,
|
|
Item_func_trig_cond::IS_NOT_NULL_COMPL);
|
|
if (!cond) return true;
|
|
if (cond->fix_fields(thd, nullptr)) return true;
|
|
}
|
|
if (best_ref[first_inner]->and_with_condition(cond)) return true;
|
|
}
|
|
/*
|
|
Split the non-constant part of the join condition into parts that
|
|
can be attached to the inner tables of the outer join.
|
|
*/
|
|
for (plan_idx i = first_inner; i <= last_tab; ++i) {
|
|
table_map prefix_tables = best_ref[i]->prefix_tables();
|
|
table_map added_tables = best_ref[i]->added_tables();
|
|
|
|
/*
|
|
When handling the first inner table of an outer join, we may also
|
|
reference all tables ahead of this table:
|
|
*/
|
|
if (i == first_inner) added_tables = prefix_tables;
|
|
/*
|
|
We need RAND_TABLE_BIT on the last inner table, in case there is a
|
|
non-deterministic function in the join condition.
|
|
(RAND_TABLE_BIT is set for the last table of the join plan,
|
|
but this is not sufficient for join conditions, which may have a
|
|
last inner table that is ahead of the last table of the join plan).
|
|
*/
|
|
if (i == last_tab) {
|
|
prefix_tables |= RAND_TABLE_BIT;
|
|
added_tables |= RAND_TABLE_BIT;
|
|
}
|
|
cond =
|
|
make_cond_for_table(thd, join_cond, prefix_tables, added_tables, false);
|
|
if (cond == nullptr) continue;
|
|
/*
|
|
If the table is part of an outer join that is embedded in the
|
|
outer join currently being processed, wrap the condition in
|
|
triggered conditions for match variables of such embedded outer joins.
|
|
*/
|
|
if (!(cond = add_found_match_trig_cond(
|
|
this, best_ref[i]->first_inner(), cond,
|
|
is_sj_mat_cond ? NO_PLAN_IDX : first_inner)))
|
|
return true;
|
|
|
|
if (!is_sj_mat_cond) {
|
|
// Add the guard turning the predicate off for the null-complemented row.
|
|
cond = new Item_func_trig_cond(cond, nullptr, this, first_inner,
|
|
Item_func_trig_cond::IS_NOT_NULL_COMPL);
|
|
if (!cond) return true;
|
|
if (cond->fix_fields(thd, nullptr)) return true;
|
|
}
|
|
// Add the generated condition to the existing table condition
|
|
if (best_ref[i]->and_with_condition(cond)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Attach outer join conditions to generated table conditions in an optimal way.
|
|
|
|
@param last_tab - Last table that has been added to the current plan.
|
|
Pre-condition: If this is the last inner table of an outer
|
|
join operation, a join condition is attached to the first
|
|
inner table of that outer join operation.
|
|
|
|
@return false if success, true if error.
|
|
|
|
Outer join conditions are attached to individual tables, but we can analyze
|
|
those conditions only when reaching the last inner table of an outer join
|
|
operation. Notice also that a table can be last within several outer join
|
|
nests, hence the outer for() loop of this function.
|
|
|
|
Example:
|
|
SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.a=t3.a) ON t1.a=t2.a
|
|
|
|
Table t3 is last both in the join nest (t2 - t3) and in (t1 - (t2 - t3))
|
|
Thus, join conditions for both join nests will be evaluated when reaching
|
|
this table.
|
|
|
|
For each outer join operation processed, the join condition is split
|
|
optimally over the inner tables of the outer join. The split-out conditions
|
|
are later referred to as table conditions (but note that several table
|
|
conditions stemming from different join operations may be combined into
|
|
a composite table condition).
|
|
|
|
Example:
|
|
Consider the above query once more.
|
|
The predicate t1.a=t2.a can be evaluated when rows from t1 and t2 are ready,
|
|
ie at table t2. The predicate t2.a=t3.a can be evaluated at table t3.
|
|
|
|
Each non-constant split-out table condition is guarded by a match variable
|
|
that enables it only when a matching row is found for all the embedded
|
|
outer join operations.
|
|
|
|
Each split-out table condition is guarded by a variable that turns the
|
|
condition off just before a null-complemented row for the outer join
|
|
operation is formed. Thus, the join condition will not be checked for
|
|
the null-complemented row.
|
|
*/
|
|
|
|
bool JOIN::attach_join_conditions(plan_idx last_tab) {
|
|
DBUG_TRACE;
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
JOIN_TAB *lt = best_ref[last_tab];
|
|
|
|
for (plan_idx first_inner = lt->first_inner();
|
|
first_inner != NO_PLAN_IDX &&
|
|
best_ref[first_inner]->last_inner() == last_tab;
|
|
first_inner = best_ref[first_inner]->first_upper()) {
|
|
/*
|
|
Table last_tab is the last inner table of an outer join, locate
|
|
the corresponding join condition from the first inner table of the
|
|
same outer join:
|
|
*/
|
|
Item *const join_cond = best_ref[first_inner]->join_cond();
|
|
assert(join_cond);
|
|
if (attach_join_condition_to_nest(first_inner, last_tab, join_cond, false))
|
|
return true;
|
|
}
|
|
if (sj_is_materialize_strategy(lt->get_sj_strategy())) {
|
|
plan_idx mat_tbl = NO_PLAN_IDX;
|
|
/*
|
|
The SJ nest's condition contains both the SJ equality condition and the
|
|
WHERE of the replaced subquery. This WHERE must be pushed to SJ-inner
|
|
tables for evaluation during materialization!
|
|
*/
|
|
Semijoin_mat_exec *sjm = nullptr;
|
|
for (plan_idx j = last_tab;; j--) {
|
|
sjm = best_ref[j]->sj_mat_exec();
|
|
if (sjm && sjm->sj_nest == lt->emb_sj_nest) {
|
|
// 'j' is the sj-mat tmp table
|
|
mat_tbl = j;
|
|
break;
|
|
}
|
|
}
|
|
assert(sjm);
|
|
if (sjm->inner_table_index + sjm->table_count - 1 == (uint)last_tab) {
|
|
// we're at last table of sjmat nest
|
|
Item *join_cond = best_ref[mat_tbl]->join_cond();
|
|
Table_ref *tr = best_ref[mat_tbl]->table_ref;
|
|
while (join_cond == nullptr && tr->embedding != nullptr &&
|
|
tr->embedding->is_derived()) {
|
|
// If subquery table(s) come from a derived table
|
|
join_cond = tr->embedding->join_cond();
|
|
tr = tr->embedding;
|
|
assert(tr->is_merged());
|
|
}
|
|
if (join_cond && attach_join_condition_to_nest(sjm->inner_table_index,
|
|
last_tab, join_cond, true))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
See if 'last_tab' is the first inner of an antijoin nest,
|
|
then add a IS NULL condition on it.
|
|
By attaching the condition to the first inner table, we know that if
|
|
it is not satisfied we can just jump back to the table right before
|
|
it.
|
|
*/
|
|
if (lt->table_ref->embedding && lt->table_ref->embedding->is_aj_nest() &&
|
|
last_tab == lt->first_inner() &&
|
|
/*
|
|
Exception: in A AJ (B LJ C) where C is a single table: there is no
|
|
join nest for C as it's single; C->embedding is thus the AJ nest; but
|
|
C->first_inner() is C (as it's the first inner of the LJ operation).
|
|
In that case it's not the first inner table of the AJ.
|
|
Catch this case:
|
|
*/
|
|
!lt->table_ref->join_cond()) {
|
|
Item *cond = new Item_func_false();
|
|
if (!cond) return true;
|
|
// This is a signal for JOIN::create_access_paths
|
|
cond->item_name.set(antijoin_null_cond);
|
|
/*
|
|
For A AJ B ON COND, we need an IS NULL condition which
|
|
is tested on the result rows of A LEFT JOIN B ON COND.
|
|
It must be tested only after the "match status" of a row of B has been
|
|
decided, so is wrapped in a condition triggered by B->found_match.
|
|
To have it test IS NULL, it's wrapped in a triggered condition which is
|
|
false if B is not NULL-complemented.
|
|
We needn't wrap this condition with triggers from upper nests, hence the
|
|
last argument of the call below.
|
|
*/
|
|
cond = add_found_match_trig_cond(this, last_tab, cond, lt->first_upper());
|
|
if (!cond) return true;
|
|
cond = new Item_func_trig_cond(cond, nullptr, this, last_tab,
|
|
Item_func_trig_cond::IS_NOT_NULL_COMPL);
|
|
if (!cond) return true;
|
|
if (cond->fix_fields(thd, nullptr)) return true;
|
|
if (lt->and_with_condition(cond)) return true;
|
|
lt->table()->reginfo.not_exists_optimize = true;
|
|
|
|
// The relevant secondary engines don't support antijoin, so don't enable
|
|
// this optimization for them.
|
|
assert(thd->secondary_engine_optimization() !=
|
|
Secondary_engine_optimization::SECONDARY);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
Remove calculation with tables that aren't yet read. Remove also tests
|
|
against fields that are read through key where the table is not a
|
|
outer join table.
|
|
We can't remove tests that are made against columns which are stored
|
|
in sorted order.
|
|
*****************************************************************************/
|
|
|
|
static Item *part_of_refkey(TABLE *table, Index_lookup *ref,
|
|
const Field *field) {
|
|
const uint ref_parts = ref->key_parts;
|
|
if (ref_parts) {
|
|
if (ref->has_guarded_conds()) return nullptr;
|
|
|
|
const KEY_PART_INFO *key_part = table->key_info[ref->key].key_part;
|
|
|
|
for (uint part = 0; part < ref_parts; part++, key_part++)
|
|
if (field->eq(key_part->field) &&
|
|
!(key_part->key_part_flag & HA_PART_KEY_SEG))
|
|
return ref->items[part];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool ref_lookup_subsumes_comparison(THD *thd, Field *field, Item *right_item,
|
|
bool can_evaluate, bool *subsumes) {
|
|
*subsumes = false;
|
|
right_item = right_item->real_item();
|
|
if (right_item->type() == Item::FIELD_ITEM) {
|
|
*subsumes = field->eq_def(down_cast<Item_field *>(right_item)->field);
|
|
return false;
|
|
} else if (right_item->type() == Item::CACHE_ITEM) {
|
|
// remove equalities injected by IN->EXISTS transformation
|
|
*subsumes = down_cast<Item_cache *>(right_item)->eq_def(field);
|
|
return false;
|
|
}
|
|
bool right_is_null = true;
|
|
if (can_evaluate) {
|
|
assert(evaluate_during_optimization(right_item,
|
|
thd->lex->current_query_block()));
|
|
right_is_null = right_item->is_nullable() && right_item->is_null();
|
|
if (thd->is_error()) return true;
|
|
}
|
|
if (!right_is_null) {
|
|
/*
|
|
We can remove all fields except:
|
|
1. String data types:
|
|
- For BINARY/VARBINARY fields with equality against a
|
|
string: Ref access can return more rows than match the
|
|
string. The reason seems to be that the string constant
|
|
is not "padded" to the full length of the field when
|
|
setting up ref access. @todo Change how ref access for
|
|
BINARY/VARBINARY fields are done so that only qualifying
|
|
rows are returned from the storage engine.
|
|
2. Float data type: Comparison of float can differ
|
|
- When we search "WHERE field=value" using an index,
|
|
the "value" side is converted from double to float by
|
|
Field_float::store(), then two floats are compared.
|
|
- When we search "WHERE field=value" without indexes,
|
|
the "field" side is converted from float to double by
|
|
Field_float::val_real(), then two doubles are compared.
|
|
*/
|
|
if (field->type() == MYSQL_TYPE_STRING &&
|
|
field->charset()->pad_attribute == NO_PAD) {
|
|
/*
|
|
For "NO PAD" collations on CHAR columns, this function must return
|
|
false, because removal of trailing space in CHAR columns makes the
|
|
table value and the index value compare differently. As the column
|
|
strips trailing spaces, it can return false candidates. Further
|
|
comparison of the actual table values is required.
|
|
*/
|
|
return false;
|
|
}
|
|
if (!((field->type() == MYSQL_TYPE_STRING || // 1
|
|
field->type() == MYSQL_TYPE_VARCHAR) &&
|
|
field->binary()) &&
|
|
!(field->type() == MYSQL_TYPE_FLOAT && field->decimals() > 0)) // 2
|
|
{
|
|
*subsumes = !right_item->save_in_field_no_warnings(field, true);
|
|
if (thd->is_error()) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
@brief
|
|
Identify redundant predicates.
|
|
|
|
@details
|
|
Test if the equality predicate 'left_item = right_item' is redundant
|
|
due to a REF-access already being set up on the table, where 'left_item' is
|
|
part of the REF-key being used, and 'right_item' is equal to the key value
|
|
specified for that field in the key.
|
|
In such cases the predicate is known to be 'true' for any rows retrieved
|
|
from that table. Thus it is redundant.
|
|
|
|
@param thd session context
|
|
@param left_item The Item_field possibly being part of A ref-KEY.
|
|
@param right_item The equality value specified for 'left_item'.
|
|
@param[out] redundant true if predicate is redundant, false otherwise
|
|
|
|
@returns false if success, true if error
|
|
|
|
@note See comments in reduce_cond_for_table() about careful
|
|
usage/modifications of test_if_ref().
|
|
*/
|
|
|
|
static bool test_if_ref(THD *thd, Item_field *left_item, Item *right_item,
|
|
bool *redundant) {
|
|
*redundant = false;
|
|
if (left_item->depended_from)
|
|
return false; // don't even read join_tab of inner subquery!
|
|
Field *field = left_item->field;
|
|
JOIN_TAB *join_tab = field->table->reginfo.join_tab;
|
|
if (join_tab == nullptr) return false;
|
|
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(join_tab->join());
|
|
|
|
// No need to change const test
|
|
if (!field->table->const_table &&
|
|
/* "ref_or_null" implements "x=y or x is null", not "x=y" */
|
|
(join_tab->type() != JT_REF_OR_NULL)) {
|
|
Item *ref_item = part_of_refkey(field->table, &join_tab->ref(), field);
|
|
if (ref_item != nullptr && ref_item->eq(right_item, true)) {
|
|
const bool can_evaluate =
|
|
right_item->const_for_execution() &&
|
|
evaluate_during_optimization(right_item,
|
|
thd->lex->current_query_block());
|
|
if (ref_lookup_subsumes_comparison(thd, field, right_item, can_evaluate,
|
|
redundant)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false; // keep predicate
|
|
}
|
|
|
|
/**
|
|
@brief
|
|
Remove redundant predicates from condition, return the reduced condition.
|
|
|
|
@details
|
|
A predicate of the form 'field = value' may be redundant if the
|
|
(ref-) access chosen for the table use an index containing 'field',
|
|
where 'value' is specified as (part of) its ref-key. This method remove
|
|
such redundant predicates, thus reducing the condition, possibly
|
|
eliminating it entirely.
|
|
|
|
If comparing 'values' against outer-joined tables, these are possibly
|
|
'null-extended'. Thus the usage of these values in the ref-key, is not
|
|
sufficient anymore to guarantee that 'field = value' is 'TRUE'.
|
|
The 'null_extended' argument hold the table_map of any such possibly
|
|
null-extended tables which are excluded from the above 'reduce' logic.
|
|
|
|
Any tables referred in Item_func_trig_cond(FOUND_MATCH) conditions are
|
|
aggregated into this null_extended table_map.
|
|
|
|
@param thd thread handler
|
|
@param cond The condition to be 'reduced'.
|
|
@param null_extended table_map of possibly null-extended outer-tables.
|
|
@param[out] reduced The condition with redundant predicates removed,
|
|
possibly nullptr.
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
static bool reduce_cond_for_table(THD *thd, Item *cond, table_map null_extended,
|
|
Item **reduced) {
|
|
DBUG_TRACE;
|
|
DBUG_EXECUTE("where",
|
|
print_where(current_thd, cond, "cond term", QT_ORDINARY););
|
|
|
|
*reduced = nullptr;
|
|
|
|
if (cond->type() == Item::COND_ITEM) {
|
|
List<Item> *arguments = down_cast<Item_cond *>(cond)->argument_list();
|
|
List_iterator<Item> li(*arguments);
|
|
if (down_cast<Item_cond *>(cond)->functype() == Item_func::COND_AND_FUNC) {
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
Item *upd_item;
|
|
if (reduce_cond_for_table(thd, item, null_extended, &upd_item)) {
|
|
return true;
|
|
}
|
|
if (upd_item == nullptr) {
|
|
li.remove();
|
|
} else if (upd_item != item) {
|
|
li.replace(upd_item);
|
|
}
|
|
}
|
|
switch (arguments->elements) {
|
|
case 0:
|
|
return false; // All 'true' -> And-cond true
|
|
case 1:
|
|
*reduced = arguments->head();
|
|
return false;
|
|
}
|
|
} else { // Or list
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
Item *upd_item;
|
|
if (reduce_cond_for_table(thd, item, null_extended, &upd_item)) {
|
|
return true;
|
|
}
|
|
if (upd_item == nullptr) {
|
|
return false; // Term 'true' -> entire Or-cond true
|
|
} else if (upd_item != item) {
|
|
li.replace(upd_item);
|
|
}
|
|
}
|
|
}
|
|
} else if (cond->type() == Item::FUNC_ITEM) {
|
|
Item_func *func = down_cast<Item_func *>(cond);
|
|
if (func->functype() == Item_func::TRIG_COND_FUNC) {
|
|
Item_func_trig_cond *func_trig = down_cast<Item_func_trig_cond *>(func);
|
|
if (func_trig->get_trig_type() == Item_func_trig_cond::FOUND_MATCH) {
|
|
/*
|
|
All inner-tables are possible null-extended when evaluating
|
|
the 'FOUND_MATCH'. Thus, predicates embedded in this trig_cond,
|
|
referring these tables, should not be eliminated.
|
|
-> Add to null_extended map.
|
|
*/
|
|
null_extended |= func_trig->get_inner_tables();
|
|
}
|
|
|
|
Item *cond_arg = func->arguments()[0];
|
|
Item *upd_arg;
|
|
if (reduce_cond_for_table(thd, cond_arg, null_extended, &upd_arg)) {
|
|
return true;
|
|
}
|
|
if (upd_arg == nullptr) {
|
|
return false;
|
|
}
|
|
func->arguments()[0] = upd_arg;
|
|
} else if (func->functype() == Item_func::EQ_FUNC) {
|
|
/*
|
|
Remove equalities that are guaranteed to be true by use of 'ref' access
|
|
method.
|
|
Note that ref access implements "table1.field1 <=>
|
|
table2.indexed_field2", i.e. if it passed a NULL field1, it will return
|
|
NULL indexed_field2 if there are.
|
|
Thus the equality "table1.field1 = table2.indexed_field2",
|
|
is equivalent to "ref access AND table1.field1 IS NOT NULL"
|
|
i.e. "ref access and proper setting/testing of ref->null_rejecting".
|
|
Thus, we must be careful, that when we remove equalities below we also
|
|
set ref->null_rejecting, and test it at execution; otherwise wrong NULL
|
|
matches appear.
|
|
So:
|
|
- for the optimization phase, the code which is below, and the code in
|
|
test_if_ref(), and in add_key_field(), must be kept in sync: if the
|
|
applicability conditions in one place are relaxed, they should also be
|
|
relaxed elsewhere.
|
|
- for the execution phase, all possible execution methods must test
|
|
ref->null_rejecting.
|
|
*/
|
|
Item *left_item = func->arguments()[0]->real_item();
|
|
Item *right_item = func->arguments()[1]->real_item();
|
|
bool redundant = false;
|
|
if (left_item->type() == Item::FIELD_ITEM &&
|
|
!(left_item->used_tables() & null_extended) &&
|
|
test_if_ref(thd, down_cast<Item_field *>(left_item), right_item,
|
|
&redundant)) {
|
|
return true;
|
|
}
|
|
if (redundant) {
|
|
return false;
|
|
}
|
|
if (right_item->type() == Item::FIELD_ITEM &&
|
|
!(right_item->used_tables() & null_extended) &&
|
|
test_if_ref(thd, down_cast<Item_field *>(right_item), left_item,
|
|
&redundant)) {
|
|
return true;
|
|
}
|
|
if (redundant) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
*reduced = cond;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
@brief
|
|
Remove redundant predicates and cache constant expressions.
|
|
|
|
@details
|
|
Do a final round on pushed down table conditions and HAVING
|
|
clause. Optimize them for faster execution by removing
|
|
predicates being obsolete due to the access path selected
|
|
for the table. Constant expressions are also cached
|
|
to avoid evaluating them for each row being compared.
|
|
|
|
@param thd thread handler
|
|
|
|
@returns false if success, true if error
|
|
|
|
@note This function is run after conditions have been pushed down to
|
|
individual tables, so transformation is applied to JOIN_TAB::condition
|
|
and not to the WHERE condition.
|
|
*/
|
|
bool JOIN::finalize_table_conditions(THD *thd) {
|
|
/*
|
|
Unnecessary to reduce conditions for const tables as they are only
|
|
evaluated once.
|
|
*/
|
|
assert(!plan_is_const());
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
const Opt_trace_array trace_tables(trace, "finalizing_table_conditions");
|
|
|
|
for (uint i = const_tables; i < tables; i++) {
|
|
Item *condition = best_ref[i]->condition();
|
|
if (condition == nullptr) continue;
|
|
|
|
/*
|
|
Table predicates known to be true by the selected
|
|
(ref-)access method may be removed from the condition
|
|
*/
|
|
Opt_trace_object trace_cond(trace);
|
|
trace_cond.add_utf8_table(best_ref[i]->table_ref);
|
|
trace_cond.add("original_table_condition", condition);
|
|
|
|
/*
|
|
Calculate the set of possibly NULL extended tables when 'condition'
|
|
is evaluated. As it is evaluated on a found row from table, that
|
|
table is subtracted from the nullable tables. Note that a FOUND_MATCH
|
|
trigger is a special case, handled in reduce_cond_for_table().
|
|
*/
|
|
const table_map null_extended =
|
|
query_block->outer_join & ~best_ref[i]->table_ref->map();
|
|
if (reduce_cond_for_table(thd, condition, null_extended, &condition)) {
|
|
return true;
|
|
}
|
|
if (condition != nullptr) condition->update_used_tables();
|
|
|
|
/*
|
|
Cache constant expressions in table conditions.
|
|
(Moved down from WHERE- and ON-clauses)
|
|
*/
|
|
if (condition != nullptr) {
|
|
cache_const_expr_arg cache_arg;
|
|
cache_const_expr_arg *analyzer_arg = &cache_arg;
|
|
condition = condition->compile(
|
|
&Item::cache_const_expr_analyzer, (uchar **)&analyzer_arg,
|
|
&Item::cache_const_expr_transformer, (uchar *)&cache_arg);
|
|
if (condition == nullptr) return true;
|
|
}
|
|
|
|
trace_cond.add("final_table_condition ", condition);
|
|
best_ref[i]->set_condition(condition);
|
|
}
|
|
|
|
/* Cache constant expressions in HAVING-clauses. */
|
|
if (having_cond != nullptr) {
|
|
cache_const_expr_arg cache_arg;
|
|
cache_const_expr_arg *analyzer_arg = &cache_arg;
|
|
having_cond = having_cond->compile(
|
|
&Item::cache_const_expr_analyzer, (uchar **)&analyzer_arg,
|
|
&Item::cache_const_expr_transformer, (uchar *)&cache_arg);
|
|
if (having_cond == nullptr) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
@brief
|
|
Add keys to derived tables'/views' result tables in a list
|
|
|
|
@details
|
|
This function generates keys for all derived tables/views of the query_block
|
|
to which this join corresponds to with help of the
|
|
Table_ref:generate_keys function.
|
|
|
|
@return false all keys were successfully added.
|
|
@return true OOM error
|
|
*/
|
|
|
|
bool JOIN::generate_derived_keys() {
|
|
assert(query_block->materialized_derived_table_count);
|
|
|
|
for (Table_ref *table = query_block->leaf_tables; table;
|
|
table = table->next_leaf) {
|
|
table->derived_keys_ready = true;
|
|
/* Process tables that aren't materialized yet. */
|
|
if (table->uses_materialization() && !table->table->is_created() &&
|
|
table->generate_keys())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
For each materialized derived table/view, informs every TABLE of the key it
|
|
will (not) use, segregates used keys from unused keys in TABLE::key_info,
|
|
and eliminates unused keys.
|
|
*/
|
|
|
|
void JOIN::finalize_derived_keys() {
|
|
assert(query_block->materialized_derived_table_count);
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
table_map processed_tables = 0;
|
|
|
|
for (uint i = 0; i < tables; i++) {
|
|
TABLE *const table = best_ref[i]->table();
|
|
Table_ref *const tr = best_ref[i]->table_ref;
|
|
/*
|
|
Process the table's key definitions if:
|
|
1) it is a materialized derived table, and
|
|
2) it is not yet instantiated, and
|
|
3) it has some keys defined, and
|
|
4) it has not yet been processed (may happen if there are more than one
|
|
local references to the same CTE, which are processed on seeing the
|
|
first reference).
|
|
*/
|
|
if (table == nullptr || !tr->uses_materialization() || // (1)
|
|
table->is_created() || // (2)
|
|
table->s->keys == 0 || // (3)
|
|
(processed_tables & tr->map())) { // (4)
|
|
continue;
|
|
}
|
|
/*
|
|
Collect all used keys before starting to shuffle them:
|
|
First create a map from key number to the table using the key:
|
|
*/
|
|
assert(table->s->keys <= MAX_INDEXES);
|
|
TABLE *table_map[MAX_INDEXES];
|
|
for (uint j = 0; j < table->s->keys; j++) {
|
|
table_map[j] = nullptr;
|
|
}
|
|
|
|
Key_map used_keys;
|
|
|
|
// Mark all unique indexes as in use, since they have an effect
|
|
// (deduplication) whether any expression refers to them or not.
|
|
// In particular, they are used if we want to materialize a UNION DISTINCT
|
|
// directly into the derived table.
|
|
for (uint key_idx = 0; key_idx < table->s->keys; ++key_idx) {
|
|
if (table->key_info[key_idx].flags & HA_NOSAME) {
|
|
used_keys.set_bit(key_idx);
|
|
}
|
|
}
|
|
// Same for the hash key used for manual deduplication, if any.
|
|
// (It always has index 0 if it exists.)
|
|
if (table->hash_field) {
|
|
used_keys.set_bit(0);
|
|
}
|
|
|
|
Derived_refs_iterator it(tr);
|
|
|
|
while (TABLE *t = it.get_next()) {
|
|
if (t->pos_in_table_list->query_block != query_block) {
|
|
continue;
|
|
}
|
|
JOIN_TAB *jtab = t->reginfo.join_tab;
|
|
Key_use *const keyuse = jtab->position()->key;
|
|
if (keyuse != nullptr) {
|
|
used_keys.set_bit(keyuse->key);
|
|
table_map[keyuse->key] = t;
|
|
} else {
|
|
jtab->keys().clear_all();
|
|
jtab->const_keys.clear_all();
|
|
processed_tables |= t->pos_in_table_list->map();
|
|
}
|
|
}
|
|
/*
|
|
This call is required to establish the initial value for
|
|
TABLE_SHARE::first_unused_tmp_key.
|
|
*/
|
|
(void)table->s->find_first_unused_tmp_key(used_keys);
|
|
|
|
// Process keys in increasing key order
|
|
for (uint j = 0; j < table->s->keys; j++) {
|
|
TABLE *const t = table_map[j];
|
|
if (t == nullptr) continue;
|
|
|
|
/*
|
|
Eliminate possible keys created by this JOIN and which it doesn't use.
|
|
Collect all keys of this table which are used by any reference in
|
|
this query block. Any other query block doesn't matter as:
|
|
- either it was optimized before, so it's not using a key we may
|
|
want to drop.
|
|
- or it was optimized in this same window, so:
|
|
* either we own the window, then any key we may want to
|
|
drop is not visible to it.
|
|
* or it owns the window, then we are using only existing keys.
|
|
- or it will be optimized after, so it's not using any key yet.
|
|
|
|
used_keys is a mix of possible used keys and existing used keys.
|
|
*/
|
|
if (t->pos_in_table_list->query_block != query_block) {
|
|
continue;
|
|
}
|
|
|
|
JOIN_TAB *jtab = t->reginfo.join_tab;
|
|
Key_use *const keyuse = jtab->position()->key;
|
|
assert(keyuse != nullptr);
|
|
|
|
// Also updates table->s->first_unused_tmp_key.
|
|
uint new_idx = t->s->find_first_unused_tmp_key(used_keys);
|
|
|
|
const uint old_idx = keyuse->key;
|
|
assert(old_idx != new_idx);
|
|
|
|
if (old_idx > new_idx) {
|
|
assert(t->s->owner_of_possible_tmp_keys == query_block);
|
|
Derived_refs_iterator it1(tr);
|
|
while (TABLE *t1 = it1.get_next()) {
|
|
/*
|
|
Unlike the collection of used_keys, references from other query
|
|
blocks must be considered here, as they need a key_info array
|
|
consistent with the to-be-changed table->s->keys.
|
|
*/
|
|
t1->move_tmp_key(old_idx, it1.is_first());
|
|
}
|
|
used_keys.clear_bit(old_idx);
|
|
used_keys.set_bit(new_idx);
|
|
} else {
|
|
new_idx = old_idx; // Index stays at same slot
|
|
}
|
|
|
|
/*
|
|
If the key was created by earlier-optimized query blocks, and is
|
|
already used by nonlocal references, those don't need any further
|
|
update: they are already setup to use it and we're not moving the key.
|
|
If the key was created by this query block, nonlocal references cannot
|
|
possibly be referencing it.
|
|
In both cases, only local references need to update their Key_use.
|
|
*/
|
|
Derived_refs_iterator it2(tr);
|
|
while (TABLE *t2 = it2.get_next()) {
|
|
if (t2->pos_in_table_list->query_block != query_block) continue;
|
|
JOIN_TAB *jt2 = t2->reginfo.join_tab;
|
|
Key_use *ku2 = jt2->position()->key;
|
|
if (ku2 != nullptr && ku2->key == old_idx) {
|
|
processed_tables |= t2->pos_in_table_list->map();
|
|
const bool key_is_const = jt2->const_keys.is_set(old_idx);
|
|
// tab->keys() was never set, so must be set
|
|
jt2->keys().clear_all();
|
|
jt2->keys().set_bit(new_idx);
|
|
jt2->const_keys.clear_all();
|
|
if (key_is_const) jt2->const_keys.set_bit(new_idx);
|
|
for (Key_use *kit = ku2;
|
|
kit->table_ref == jt2->table_ref && kit->key == old_idx; kit++) {
|
|
kit->key = new_idx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, we know how many keys remain in the table.
|
|
if (table->s->owner_of_possible_tmp_keys != query_block) continue;
|
|
|
|
// Release lock:
|
|
table->s->owner_of_possible_tmp_keys = nullptr;
|
|
it.rewind();
|
|
while (TABLE *t = it.get_next()) {
|
|
t->drop_unused_tmp_keys(it.is_first());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief
|
|
Extract a condition that can be checked after reading given table
|
|
|
|
@param thd Current session.
|
|
@param cond Condition to analyze
|
|
@param tables Tables for which "current field values" are available
|
|
@param used_table Table(s) that we are extracting the condition for (may
|
|
also include PSEUDO_TABLE_BITS, and may be zero)
|
|
@param exclude_expensive_cond Do not push expensive conditions
|
|
|
|
@retval <>NULL Generated condition
|
|
@retval = NULL Already checked, OR error
|
|
|
|
@details
|
|
Extract the condition that can be checked after reading the table(s)
|
|
specified in @c used_table, given that current-field values for tables
|
|
specified in @c tables bitmap are available.
|
|
If @c used_table is 0, extract conditions for all tables in @c tables.
|
|
|
|
This function can be used to extract conditions relevant for a table
|
|
in a join order. Together with its caller, it will ensure that all
|
|
conditions are attached to the first table in the join order where all
|
|
necessary fields are available, and it will also ensure that a given
|
|
condition is attached to only one table.
|
|
To accomplish this, first initialize @c tables to the empty
|
|
set. Then, loop over all tables in the join order, set @c used_table to
|
|
the bit representing the current table, accumulate @c used_table into the
|
|
@c tables set, and call this function. To ensure correct handling of
|
|
const expressions and outer references, add the const table map and
|
|
OUTER_REF_TABLE_BIT to @c used_table for the first table. To ensure
|
|
that random expressions are evaluated for the final table, add
|
|
RAND_TABLE_BIT to @c used_table for the final table.
|
|
|
|
The function assumes that constant, inexpensive parts of the condition
|
|
have already been checked. Constant, expensive parts will be attached
|
|
to the first table in the join order, provided that the above call
|
|
sequence is followed.
|
|
|
|
The call order will ensure that conditions covering tables in @c tables
|
|
minus those in @c used_table, have already been checked.
|
|
|
|
The function takes into account that some parts of the condition are
|
|
guaranteed to be true by employed 'ref' access methods (the code that
|
|
does this is located at the end, search down for "EQ_FUNC").
|
|
|
|
@note
|
|
make_cond_for_info_schema() uses an algorithm similar to
|
|
make_cond_for_table().
|
|
*/
|
|
|
|
Item *make_cond_for_table(THD *thd, Item *cond, table_map tables,
|
|
table_map used_table, bool exclude_expensive_cond) {
|
|
/*
|
|
May encounter an Item_cache_int as "condition" here, so cannot
|
|
assert that it satisfies is_bool_func().
|
|
*/
|
|
/*
|
|
Ignore this condition if
|
|
1. We are extracting conditions for a specific table, and
|
|
2. that table is not referenced by the condition, but not if
|
|
3. this is a constant condition not checked at optimization time and
|
|
this is the first table we are extracting conditions for.
|
|
(Assuming that used_table == tables for the first table.)
|
|
*/
|
|
if (used_table && // 1
|
|
!(cond->used_tables() & used_table) && // 2
|
|
!(cond->cost().IsExpensive() && used_table == tables)) // 3
|
|
return nullptr;
|
|
|
|
if (cond->type() == Item::COND_ITEM) {
|
|
if (((Item_cond *)cond)->functype() == Item_func::COND_AND_FUNC) {
|
|
/* Create new top level AND item */
|
|
Item_cond_and *new_cond = new Item_cond_and;
|
|
if (!new_cond) return nullptr;
|
|
List_iterator<Item> li(*((Item_cond *)cond)->argument_list());
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
Item *fix = make_cond_for_table(thd, item, tables, used_table,
|
|
exclude_expensive_cond);
|
|
if (fix) new_cond->argument_list()->push_back(fix);
|
|
}
|
|
switch (new_cond->argument_list()->elements) {
|
|
case 0:
|
|
return nullptr; // Always true
|
|
case 1:
|
|
return new_cond->argument_list()->head();
|
|
default:
|
|
if (new_cond->fix_fields(thd, nullptr)) return nullptr;
|
|
return new_cond;
|
|
}
|
|
} else { // Or list
|
|
Item_cond_or *new_cond = new Item_cond_or;
|
|
if (!new_cond) return nullptr;
|
|
List_iterator<Item> li(*((Item_cond *)cond)->argument_list());
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
Item *fix = make_cond_for_table(thd, item, tables, table_map(0),
|
|
exclude_expensive_cond);
|
|
if (!fix) return nullptr; // Always true
|
|
new_cond->argument_list()->push_back(fix);
|
|
}
|
|
if (new_cond->fix_fields(thd, nullptr)) return nullptr;
|
|
return new_cond;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Omit this condition if
|
|
1. Some tables referred by the condition are not available, or
|
|
2. We are extracting conditions for all tables, the condition is
|
|
considered 'expensive', and we want to delay evaluation of such
|
|
conditions to the execution phase.
|
|
*/
|
|
if ((cond->used_tables() & ~tables) || // 1
|
|
(!used_table && exclude_expensive_cond &&
|
|
cond->cost().IsExpensive())) // 2
|
|
return nullptr;
|
|
|
|
return cond;
|
|
}
|
|
|
|
/**
|
|
Separates the predicates in a join condition and pushes them to the
|
|
join step where all involved tables are available in the join prefix.
|
|
ON clauses from JOIN expressions are also pushed to the most appropriate step.
|
|
|
|
@param join Join object where predicates are pushed.
|
|
|
|
@param cond Pointer to condition which may contain an arbitrary number of
|
|
predicates, combined using AND, OR and XOR items.
|
|
If NULL, equivalent to a predicate that returns true for all
|
|
row combinations.
|
|
|
|
|
|
@retval true Found impossible WHERE clause, or out-of-memory
|
|
@retval false Other
|
|
*/
|
|
|
|
static bool make_join_query_block(JOIN *join, Item *cond) {
|
|
assert(cond == nullptr || cond->is_bool_func());
|
|
THD *thd = join->thd;
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
DBUG_TRACE;
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(join);
|
|
|
|
// Add IS NOT NULL conditions to table conditions:
|
|
if (add_not_null_conds(join)) return true;
|
|
|
|
/*
|
|
Extract constant conditions that are part of the WHERE clause.
|
|
Constant parts of join conditions from outer joins are attached to
|
|
the appropriate table condition in JOIN::attach_join_conditions().
|
|
*/
|
|
if (cond) /* Because of GroupIndexSkipScanIterator */
|
|
{ /* there may be a select without a cond. */
|
|
if (join->primary_tables > 1)
|
|
cond->update_used_tables(); // Table number may have changed
|
|
if (join->plan_is_const() &&
|
|
join->query_block->master_query_expression() ==
|
|
thd->lex->unit) // The outer-most query block
|
|
join->const_table_map |= RAND_TABLE_BIT;
|
|
}
|
|
/*
|
|
Extract conditions that depend on constant tables.
|
|
The const part of the query's WHERE clause can be checked immediately
|
|
and if it is not satisfied then the join has empty result
|
|
*/
|
|
Item *const_cond = nullptr;
|
|
if (cond)
|
|
const_cond = make_cond_for_table(thd, cond, join->const_table_map,
|
|
table_map(0), true);
|
|
|
|
// Add conditions added by add_not_null_conds()
|
|
for (uint i = 0; i < join->const_tables; i++) {
|
|
if (and_conditions(&const_cond, join->best_ref[i]->condition()))
|
|
return true;
|
|
}
|
|
DBUG_EXECUTE("where",
|
|
print_where(thd, const_cond, "constants", QT_ORDINARY););
|
|
if (const_cond != nullptr &&
|
|
evaluate_during_optimization(const_cond, join->query_block)) {
|
|
const bool const_cond_result = const_cond->val_int() != 0;
|
|
if (thd->is_error()) return true;
|
|
|
|
Opt_trace_object trace_const_cond(trace);
|
|
trace_const_cond.add("condition_on_constant_tables", const_cond)
|
|
.add("condition_value", const_cond_result);
|
|
if (const_cond_result) {
|
|
/*
|
|
If all the tables referred by the condition are const tables and
|
|
if the condition is not expensive, we can remove the where condition
|
|
as it will always evaluate to "true".
|
|
*/
|
|
if (join->plan_is_const() &&
|
|
!(cond->used_tables() & ~join->const_table_map) &&
|
|
!cond->cost().IsExpensive()) {
|
|
DBUG_PRINT("info", ("Found always true WHERE condition"));
|
|
join->where_cond = nullptr;
|
|
}
|
|
} else {
|
|
DBUG_PRINT("info", ("Found impossible WHERE condition"));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Extract remaining conditions from WHERE clause and join conditions,
|
|
and attach them to the most appropriate table condition. This means that
|
|
a condition will be evaluated as soon as all fields it depends on are
|
|
available. For outer join conditions, the additional criterion is that
|
|
we must have determined whether outer-joined rows are available, or
|
|
have been NULL-extended, see JOIN::attach_join_conditions() for details.
|
|
*/
|
|
{
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
Opt_trace_object trace_conditions(trace, "attaching_conditions_to_tables");
|
|
trace_conditions.add("original_condition", cond);
|
|
Opt_trace_array trace_attached_comp(trace,
|
|
"attached_conditions_computation");
|
|
|
|
for (uint i = join->const_tables; i < join->tables; i++) {
|
|
JOIN_TAB *const tab = join->best_ref[i];
|
|
|
|
if (!tab->position()) continue;
|
|
/*
|
|
first_inner is the X in queries like:
|
|
SELECT * FROM t1 LEFT OUTER JOIN (t2 JOIN t3) ON X
|
|
*/
|
|
const plan_idx first_inner = tab->first_inner();
|
|
const table_map used_tables = tab->prefix_tables();
|
|
const table_map current_map = tab->added_tables();
|
|
Item *tmp = nullptr;
|
|
|
|
if (cond)
|
|
tmp = make_cond_for_table(thd, cond, used_tables, current_map, false);
|
|
/* Add conditions added by add_not_null_conds(). */
|
|
if (and_conditions(&tmp, tab->condition())) return true;
|
|
|
|
if (cond && !tmp && tab->range_scan()) { // Outer join
|
|
assert(tab->type() == JT_RANGE || tab->type() == JT_INDEX_MERGE);
|
|
/*
|
|
Hack to handle the case where we only refer to a table
|
|
in the ON part of an OUTER JOIN. In this case we want the code
|
|
below to check if we should use 'quick' instead.
|
|
*/
|
|
DBUG_PRINT("info", ("Item_func_true"));
|
|
tmp = new Item_func_true(); // Always true
|
|
}
|
|
if (tmp || !cond || tab->type() == JT_REF ||
|
|
tab->type() == JT_REF_OR_NULL || tab->type() == JT_EQ_REF ||
|
|
first_inner != NO_PLAN_IDX) {
|
|
DBUG_EXECUTE("where",
|
|
print_where(thd, tmp, tab->table()->alias, QT_ORDINARY););
|
|
/*
|
|
If tab is an inner table of an outer join operation,
|
|
add a match guard to the pushed down predicate.
|
|
The guard will turn the predicate on only after
|
|
the first match for outer tables is encountered.
|
|
*/
|
|
if (cond && tmp) {
|
|
/*
|
|
Because of GroupIndexSkipScanIterator there may be a select without
|
|
a cond, so neutralize the hack above.
|
|
*/
|
|
if (!(tmp = add_found_match_trig_cond(join, first_inner, tmp,
|
|
NO_PLAN_IDX)))
|
|
return true;
|
|
tab->set_condition(tmp);
|
|
} else {
|
|
tab->set_condition(nullptr);
|
|
}
|
|
|
|
DBUG_EXECUTE("where",
|
|
print_where(thd, tmp, tab->table()->alias, QT_ORDINARY););
|
|
|
|
if (tab->range_scan()) {
|
|
if (tab->needed_reg.is_clear_all() && tab->type() != JT_CONST) {
|
|
/*
|
|
We keep (for now) the QUICK AM calculated in
|
|
get_quick_record_count().
|
|
*/
|
|
} else {
|
|
::destroy_at(tab->range_scan());
|
|
tab->set_range_scan(nullptr);
|
|
}
|
|
}
|
|
|
|
if ((tab->type() == JT_ALL || tab->type() == JT_RANGE ||
|
|
tab->type() == JT_INDEX_MERGE || tab->type() == JT_INDEX_SCAN) &&
|
|
tab->use_quick != QS_RANGE) {
|
|
/*
|
|
We plan to scan (table/index/range scan).
|
|
Check again if we should use an index. We can use an index if:
|
|
|
|
1a) There is a condition that range optimizer can work on, and
|
|
1b) There are non-constant conditions on one or more keys, and
|
|
1c) Some of the non-constant fields may have been read
|
|
already. This may be the case if this is not the first
|
|
table in the join OR this is a subselect with
|
|
non-constant conditions referring to an outer table
|
|
(dependent subquery)
|
|
or,
|
|
2a) There are conditions only relying on constants
|
|
2b) This is the first non-constant table
|
|
2c) There is a limit of rows to read that is lower than
|
|
the fanout for this table, predicate filters included
|
|
(i.e., the estimated number of rows that will be
|
|
produced for this table per row combination of
|
|
previous tables)
|
|
2d) The query is NOT run with FOUND_ROWS() (because in that
|
|
case we have to scan through all rows to count them anyway)
|
|
*/
|
|
enum {
|
|
DONT_RECHECK,
|
|
NOT_FIRST_TABLE,
|
|
LOW_LIMIT
|
|
} recheck_reason = DONT_RECHECK;
|
|
|
|
assert(tab->const_keys.is_subset(tab->keys()));
|
|
|
|
const join_type orig_join_type = tab->type();
|
|
const AccessPath *const orig_range_scan = tab->range_scan();
|
|
|
|
if (cond && // 1a
|
|
(tab->keys() != tab->const_keys) && // 1b
|
|
(i > 0 || // 1c
|
|
(join->query_block->master_query_expression()->item &&
|
|
cond->is_outer_reference())))
|
|
recheck_reason = NOT_FIRST_TABLE;
|
|
else if (!tab->const_keys.is_clear_all() && // 2a
|
|
i == join->const_tables && // 2b
|
|
(join->query_expression()->select_limit_cnt <
|
|
(tab->position()->rows_fetched *
|
|
tab->position()->filter_effect)) && // 2c
|
|
!join->calc_found_rows) // 2d
|
|
recheck_reason = LOW_LIMIT;
|
|
|
|
// Don't recheck if the storage engine does not support index access.
|
|
if ((tab->table()->file->ha_table_flags() & HA_NO_INDEX_ACCESS) != 0)
|
|
recheck_reason = DONT_RECHECK;
|
|
|
|
if (tab->position()->sj_strategy == SJ_OPT_LOOSE_SCAN) {
|
|
/*
|
|
Semijoin loose scan has settled for a certain index-based access
|
|
method with suitable characteristics, don't substitute it.
|
|
*/
|
|
recheck_reason = DONT_RECHECK;
|
|
}
|
|
|
|
if (recheck_reason != DONT_RECHECK) {
|
|
Opt_trace_object trace_one_table(trace);
|
|
trace_one_table.add_utf8_table(tab->table_ref);
|
|
Opt_trace_object trace_table(trace, "rechecking_index_usage");
|
|
if (recheck_reason == NOT_FIRST_TABLE)
|
|
trace_table.add_alnum("recheck_reason", "not_first_table");
|
|
else
|
|
trace_table.add_alnum("recheck_reason", "low_limit")
|
|
.add("limit", join->query_expression()->select_limit_cnt)
|
|
.add("row_estimate", tab->position()->rows_fetched *
|
|
tab->position()->filter_effect);
|
|
|
|
/* Join with outer join condition */
|
|
Item *orig_cond = tab->condition();
|
|
tab->and_with_condition(tab->join_cond());
|
|
|
|
/*
|
|
We can't call sel->cond->fix_fields,
|
|
as it will break tab->join_cond() if it's AND condition
|
|
(fix_fields currently removes extra AND/OR levels).
|
|
Yet attributes of the just built condition are not needed.
|
|
Thus we call sel->cond->quick_fix_field for safety.
|
|
*/
|
|
if (tab->condition() && !tab->condition()->fixed)
|
|
tab->condition()->quick_fix_field();
|
|
|
|
Key_map usable_keys = tab->keys();
|
|
enum_order interesting_order = ORDER_NOT_RELEVANT;
|
|
|
|
if (recheck_reason == LOW_LIMIT) {
|
|
int read_direction = 0;
|
|
|
|
/*
|
|
If the current plan is to use range, then check if the
|
|
already selected index provides the order dictated by the
|
|
ORDER BY clause.
|
|
*/
|
|
if (tab->range_scan() &&
|
|
used_index(tab->range_scan()) != MAX_KEY) {
|
|
const uint ref_key = used_index(tab->range_scan());
|
|
bool skip_quick;
|
|
read_direction = test_if_order_by_key(
|
|
&join->order, tab->table(), ref_key, nullptr, &skip_quick);
|
|
if (skip_quick) read_direction = 0;
|
|
/*
|
|
If the index provides order there is no need to recheck
|
|
index usage; we already know from the former call to
|
|
test_quick_select() that a range scan on the chosen
|
|
index is cheapest. Note that previous calls to
|
|
test_quick_select() did not take order direction
|
|
(ASC/DESC) into account, so in case of DESC ordering
|
|
we still need to recheck.
|
|
*/
|
|
if (read_direction == 1 ||
|
|
(read_direction == -1 &&
|
|
reverse_sort_possible(tab->range_scan()) &&
|
|
!make_reverse(get_used_key_parts(tab->range_scan()),
|
|
tab->range_scan()))) {
|
|
recheck_reason = DONT_RECHECK;
|
|
}
|
|
}
|
|
|
|
// We do a cost based search for an ordering index here, if:
|
|
// 1. "prefer_ordering_index" switch is on or
|
|
// 2. An index is forced for order by or
|
|
// 3. Optimizer has chosen to do table scan.
|
|
if (recheck_reason != DONT_RECHECK &&
|
|
(thd->optimizer_switch_flag(
|
|
OPTIMIZER_SWITCH_PREFER_ORDERING_INDEX) ||
|
|
tab->table()->force_index_order || tab->type() == JT_ALL)) {
|
|
DBUG_EXECUTE_IF("prefer_ordering_index_check", {
|
|
const char act[] =
|
|
"now wait_for "
|
|
"signal.prefer_ordering_index_check_continue";
|
|
assert(!debug_sync_set_action(current_thd,
|
|
STRING_WITH_LEN(act)));
|
|
});
|
|
|
|
int best_key = -1;
|
|
ha_rows select_limit =
|
|
join->query_expression()->select_limit_cnt;
|
|
|
|
/* Use index specified in FORCE INDEX FOR ORDER BY, if any. */
|
|
if (tab->table()->force_index_order)
|
|
usable_keys.intersect(tab->table()->keys_in_use_for_order_by);
|
|
|
|
// Do a cost based search on the indexes that give sort order.
|
|
test_if_cheaper_ordering(
|
|
tab, &join->order, tab->table(), usable_keys, -1,
|
|
select_limit, &best_key, &read_direction, &select_limit);
|
|
if (best_key < 0)
|
|
recheck_reason = DONT_RECHECK; // No usable keys
|
|
else {
|
|
// Only usable_key is the best_key chosen
|
|
usable_keys.clear_all();
|
|
usable_keys.set_bit(best_key);
|
|
interesting_order =
|
|
(read_direction == -1 ? ORDER_DESC : ORDER_ASC);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool search_if_impossible = recheck_reason != DONT_RECHECK;
|
|
if (search_if_impossible) {
|
|
if (tab->range_scan()) {
|
|
::destroy_at(tab->range_scan());
|
|
tab->set_type(JT_ALL);
|
|
}
|
|
AccessPath *range_scan;
|
|
MEM_ROOT temp_mem_root(key_memory_test_quick_select_exec,
|
|
thd->variables.range_alloc_block_size);
|
|
search_if_impossible =
|
|
test_quick_select(
|
|
thd, thd->mem_root, &temp_mem_root, usable_keys,
|
|
used_tables & ~tab->table_ref->map(), 0,
|
|
join->calc_found_rows
|
|
? HA_POS_ERROR
|
|
: join->query_expression()->select_limit_cnt,
|
|
false, // don't force quick range
|
|
interesting_order, tab->table(),
|
|
tab->skip_records_in_range(), tab->condition(),
|
|
&tab->needed_reg, tab->table()->force_index,
|
|
join->query_block, &range_scan) < 0;
|
|
tab->set_range_scan(range_scan);
|
|
}
|
|
tab->set_condition(orig_cond);
|
|
if (search_if_impossible) {
|
|
/*
|
|
Before reporting "Impossible WHERE" for the whole query
|
|
we have to check isn't it only "impossible ON" instead
|
|
*/
|
|
if (!tab->join_cond())
|
|
return true; // No ON, so it's really "impossible WHERE"
|
|
const Opt_trace_object trace_without_on(trace,
|
|
"without_ON_clause");
|
|
if (tab->range_scan()) {
|
|
::destroy_at(tab->range_scan());
|
|
tab->set_type(JT_ALL);
|
|
}
|
|
AccessPath *range_scan;
|
|
MEM_ROOT temp_mem_root(key_memory_test_quick_select_exec,
|
|
thd->variables.range_alloc_block_size);
|
|
const bool impossible_where =
|
|
test_quick_select(
|
|
thd, thd->mem_root, &temp_mem_root, tab->keys(),
|
|
used_tables & ~tab->table_ref->map(), 0,
|
|
join->calc_found_rows
|
|
? HA_POS_ERROR
|
|
: join->query_expression()->select_limit_cnt,
|
|
false, // don't force quick range
|
|
ORDER_NOT_RELEVANT, tab->table(),
|
|
tab->skip_records_in_range(), tab->condition(),
|
|
&tab->needed_reg, tab->table()->force_index,
|
|
join->query_block, &range_scan) < 0;
|
|
tab->set_range_scan(range_scan);
|
|
if (impossible_where) return true; // Impossible WHERE
|
|
}
|
|
|
|
/*
|
|
Access method changed. This is after deciding join order
|
|
and access method for all other tables so the info
|
|
updated below will not have any effect on the execution
|
|
plan.
|
|
*/
|
|
if (tab->range_scan())
|
|
tab->set_type(calc_join_type(tab->range_scan()));
|
|
|
|
} // end of "if (recheck_reason != DONT_RECHECK)"
|
|
|
|
if (!tab->table()->quick_keys.is_subset(tab->checked_keys) ||
|
|
!tab->needed_reg.is_subset(tab->checked_keys)) {
|
|
tab->keys().merge(tab->table()->quick_keys);
|
|
tab->keys().merge(tab->needed_reg);
|
|
|
|
/*
|
|
The logic below for assigning tab->use_quick is strange.
|
|
It bases the decision of which access method to use
|
|
(dynamic range, range, scan) based on seemingly
|
|
unrelated information like the presence of another index
|
|
with too bad selectivity to be used.
|
|
|
|
Consider the following scenario:
|
|
|
|
The join optimizer has decided to use join order
|
|
(t1,t2), and 'tab' is currently t2. Further, assume that
|
|
there is a join condition between t1 and t2 using some
|
|
range operator (e.g. "t1.x < t2.y").
|
|
|
|
It has been decided that a table scan is best for t2.
|
|
make_join_query_block() then reran the range optimizer a few
|
|
lines up because there is an index 't2.good_idx'
|
|
covering the t2.y column. If 'good_idx' is the only
|
|
index in t2, the decision below will be to use dynamic
|
|
range. However, if t2 also has another index 't2.other'
|
|
which the range access method can be used on but
|
|
selectivity is bad (#rows estimate is high), then table
|
|
scan is chosen instead.
|
|
|
|
Thus, the choice of DYNAMIC RANGE vs SCAN depends on the
|
|
presence of an index that has so bad selectivity that it
|
|
will not be used anyway.
|
|
*/
|
|
if (!tab->needed_reg.is_clear_all() &&
|
|
(tab->table()->quick_keys.is_clear_all() ||
|
|
(tab->range_scan() &&
|
|
(tab->range_scan()->num_output_rows() >= 100.0)))) {
|
|
tab->use_quick = QS_DYNAMIC_RANGE;
|
|
tab->set_type(JT_ALL);
|
|
} else
|
|
tab->use_quick = QS_RANGE;
|
|
}
|
|
|
|
if (tab->type() != orig_join_type ||
|
|
tab->range_scan() != orig_range_scan) // Access method changed
|
|
tab->position()->filter_effect = COND_FILTER_STALE;
|
|
}
|
|
}
|
|
|
|
if (join->attach_join_conditions(i)) return true;
|
|
}
|
|
trace_attached_comp.end();
|
|
|
|
/*
|
|
In outer joins the loop above, in iteration for table #i, may push
|
|
conditions to a table before #i. Thus, the processing below has to be in
|
|
a separate loop:
|
|
*/
|
|
const Opt_trace_array trace_attached_summary(trace,
|
|
"attached_conditions_summary");
|
|
for (uint i = join->const_tables; i < join->tables; i++) {
|
|
JOIN_TAB *const tab = join->best_ref[i];
|
|
if (!tab->table()) continue;
|
|
Item *const tab_cond = tab->condition();
|
|
Opt_trace_object trace_one_table(trace);
|
|
trace_one_table.add_utf8_table(tab->table_ref).add("attached", tab_cond);
|
|
if (tab_cond && tab_cond->has_subquery()) // traverse only if needed
|
|
{
|
|
/*
|
|
Why we pass walk_subquery=false: imagine
|
|
WHERE t1.col IN (SELECT * FROM t2
|
|
WHERE t2.col IN (SELECT * FROM t3)
|
|
and tab==t1. The grandchild subquery (SELECT * FROM t3) should not
|
|
be marked as "in condition of t1" but as "in condition of t2", for
|
|
correct calculation of the number of its executions.
|
|
*/
|
|
std::pair<Query_block *, int> pair_object(join->query_block, i);
|
|
tab_cond->walk(&Item::inform_item_in_cond_of_tab, enum_walk::POSTFIX,
|
|
pointer_cast<uchar *>(&pair_object));
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Remove the following expressions from ORDER BY and GROUP BY:
|
|
Constant expressions @n
|
|
Expression that only uses tables that are of type EQ_REF and the reference
|
|
is in the ORDER list or if all refereed tables are of the above type.
|
|
|
|
In the following, the X field can be removed:
|
|
@code
|
|
SELECT * FROM t1,t2 WHERE t1.a=t2.a ORDER BY t1.a,t2.X
|
|
SELECT * FROM t1,t2,t3 WHERE t1.a=t2.a AND t2.b=t3.b ORDER BY t1.a,t3.X
|
|
@endcode
|
|
|
|
These can't be optimized:
|
|
@code
|
|
SELECT * FROM t1,t2 WHERE t1.a=t2.a ORDER BY t2.X,t1.a
|
|
SELECT * FROM t1,t2 WHERE t1.a=t2.a AND t1.b=t2.b ORDER BY t1.a,t2.c
|
|
SELECT * FROM t1,t2 WHERE t1.a=t2.a ORDER BY t2.b,t1.a
|
|
@endcode
|
|
|
|
@param join join object
|
|
@param start_order clause being analyzed (ORDER BY, GROUP BY...)
|
|
@param tab table
|
|
@param cached_eq_ref_tables bitmap: bit Z is set if the table of map Z
|
|
was already the subject of an eq_ref_table() call for the same clause; then
|
|
the return value of this previous call can be found at bit Z of
|
|
'eq_ref_tables'
|
|
@param eq_ref_tables see above.
|
|
*/
|
|
|
|
static bool eq_ref_table(JOIN *join, ORDER *start_order, JOIN_TAB *tab,
|
|
table_map *cached_eq_ref_tables,
|
|
table_map *eq_ref_tables) {
|
|
/* We can skip const tables only if not an outer table */
|
|
if (tab->type() == JT_CONST && tab->first_inner() == NO_PLAN_IDX) return true;
|
|
if (tab->type() != JT_EQ_REF || tab->table()->is_nullable()) return false;
|
|
|
|
const table_map map = tab->table_ref->map();
|
|
uint found = 0;
|
|
|
|
for (Item **ref_item = tab->ref().items,
|
|
**end = ref_item + tab->ref().key_parts;
|
|
ref_item != end; ref_item++) {
|
|
if (!(*ref_item)->const_item()) { // Not a const ref
|
|
ORDER *order;
|
|
for (order = start_order; order; order = order->next) {
|
|
if ((*ref_item)->eq(order->item[0], false)) break;
|
|
}
|
|
if (order) {
|
|
if (!(order->used & map)) {
|
|
found++;
|
|
order->used |= map;
|
|
}
|
|
continue; // Used in ORDER BY
|
|
}
|
|
if (!only_eq_ref_tables(join, start_order, (*ref_item)->used_tables(),
|
|
cached_eq_ref_tables, eq_ref_tables))
|
|
return false;
|
|
}
|
|
}
|
|
/* Check that there was no reference to table before sort order */
|
|
for (; found && start_order; start_order = start_order->next) {
|
|
if (start_order->used & map) {
|
|
found--;
|
|
continue;
|
|
}
|
|
if (start_order->depend_map & map) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// @see eq_ref_table()
|
|
static bool only_eq_ref_tables(JOIN *join, ORDER *order, table_map tables,
|
|
table_map *cached_eq_ref_tables,
|
|
table_map *eq_ref_tables) {
|
|
tables &= ~PSEUDO_TABLE_BITS;
|
|
for (JOIN_TAB **tab = join->map2table; tables; tab++, tables >>= 1) {
|
|
if (tables & 1) {
|
|
const table_map map = (*tab)->table_ref->map();
|
|
bool is_eq_ref;
|
|
if (*cached_eq_ref_tables & map) // then there exists a cached bit
|
|
is_eq_ref = *eq_ref_tables & map;
|
|
else {
|
|
is_eq_ref = eq_ref_table(join, order, *tab, cached_eq_ref_tables,
|
|
eq_ref_tables);
|
|
if (is_eq_ref)
|
|
*eq_ref_tables |= map;
|
|
else
|
|
*eq_ref_tables &= ~map;
|
|
*cached_eq_ref_tables |= map; // now there exists a cached bit
|
|
}
|
|
if (!is_eq_ref) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
Check if an expression in ORDER BY or GROUP BY is a duplicate of a
|
|
preceding expression.
|
|
|
|
@param first_order the first expression in the ORDER BY or
|
|
GROUP BY clause
|
|
@param possible_dup the expression that might be a duplicate of
|
|
another expression preceding it the ORDER BY
|
|
or GROUP BY clause
|
|
|
|
@returns true if possible_dup is a duplicate, false otherwise
|
|
*/
|
|
static bool duplicate_order(const ORDER *first_order,
|
|
const ORDER *possible_dup) {
|
|
const ORDER *order;
|
|
for (order = first_order; order; order = order->next) {
|
|
if (order == possible_dup) {
|
|
// all expressions preceding possible_dup have been checked.
|
|
return false;
|
|
} else {
|
|
const Item *it1 = order->item[0]->real_item();
|
|
const Item *it2 = possible_dup->item[0]->real_item();
|
|
|
|
if (it1->eq(it2, false)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Remove all constants and check if ORDER only contains simple
|
|
expressions.
|
|
|
|
simple_order is set to true if sort_order only uses fields from head table
|
|
and the head table is not a LEFT JOIN table.
|
|
|
|
@param first_order List of GROUP BY or ORDER BY sub-clauses.
|
|
@param cond WHERE condition.
|
|
@param change If true, remove sub-clauses that need not be evaluated.
|
|
If this is not set, then only simple_order is calculated.
|
|
@param[out] simple_order Set to true if we are only using simple expressions.
|
|
@param group_by True if first_order represents a grouping operation.
|
|
|
|
@returns new sort order, after const elimination (when change is true).
|
|
*/
|
|
|
|
ORDER *JOIN::remove_const(ORDER *first_order, Item *cond, bool change,
|
|
bool *simple_order, bool group_by) {
|
|
DBUG_TRACE;
|
|
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
if (plan_is_const())
|
|
return change ? nullptr : first_order; // No need to sort
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_disable_I_S trace_disabled(trace, first_order == nullptr);
|
|
Opt_trace_object trace_simpl(
|
|
trace, group_by ? "simplifying_group_by" : "simplifying_order_by");
|
|
if (trace->is_started()) {
|
|
String str;
|
|
Query_block::print_order(
|
|
thd, &str, first_order,
|
|
enum_query_type(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER |
|
|
QT_NO_DEFAULT_DB));
|
|
trace_simpl.add_utf8("original_clause", str.ptr(), str.length());
|
|
}
|
|
Opt_trace_array trace_each_item(trace, "items");
|
|
|
|
JOIN_TAB *const first_tab = best_ref[const_tables];
|
|
const table_map first_table = first_tab->table_ref->map();
|
|
const table_map not_const_tables = ~const_table_map;
|
|
table_map ref;
|
|
// Caches to avoid repeating eq_ref_table() calls, @see eq_ref_table()
|
|
table_map eq_ref_tables = 0, cached_eq_ref_tables = 0;
|
|
|
|
ORDER **prev_ptr = &first_order;
|
|
*simple_order = !first_tab->join_cond();
|
|
|
|
// De-optimization in conjunction with window functions
|
|
if (group_by && m_windows.elements > 0) *simple_order = false;
|
|
|
|
update_depend_map(first_order);
|
|
|
|
for (ORDER *order = first_order; order; order = order->next) {
|
|
Opt_trace_object trace_one_item(trace);
|
|
trace_one_item.add("item", order->item[0]);
|
|
const table_map order_tables = order->item[0]->used_tables();
|
|
|
|
if (order->item[0]->has_aggregation() || order->item[0]->has_wf() ||
|
|
/*
|
|
If the outer table of an outer join is const (either by itself or
|
|
after applying WHERE condition), grouping on a field from such a
|
|
table will be optimized away and filesort without temporary table
|
|
will be used unless we prevent that now. Filesort is not fit to
|
|
handle joins and the join condition is not applied. We can't detect
|
|
the case without an expensive test, however, so we force temporary
|
|
table for all queries containing more than one table, ROLLUP, and an
|
|
outer join.
|
|
*/
|
|
(primary_tables > 1 && rollup_state == RollupState::INITED &&
|
|
query_block->outer_join)) {
|
|
*simple_order = false; // Must use a temporary table to sort
|
|
} else if ((order_tables & not_const_tables) == 0 &&
|
|
evaluate_during_optimization(order->item[0], query_block)) {
|
|
if (order->item[0]->has_subquery()) {
|
|
if (!thd->lex->is_explain()) {
|
|
const Opt_trace_array trace_subselect(trace, "subselect_evaluation");
|
|
String str;
|
|
order->item[0]->val_str(&str);
|
|
}
|
|
order->item[0]->mark_subqueries_optimized_away();
|
|
}
|
|
trace_one_item.add("uses_only_constant_tables", true);
|
|
continue; // skip const item
|
|
} else if (duplicate_order(first_order, order)) {
|
|
/*
|
|
If 'order' is a duplicate of an expression earlier in the
|
|
ORDER/GROUP BY sequence, it can be removed from the ORDER BY
|
|
or GROUP BY clause.
|
|
*/
|
|
trace_one_item.add("duplicate_item", true);
|
|
continue;
|
|
} else if (order->in_field_list && order->item[0]->has_subquery()) {
|
|
/*
|
|
If the order item is a subquery that is also in the field
|
|
list, a temp table should be used to avoid evaluating the
|
|
subquery for each row both when a) creating a sort index and
|
|
b) getting the value.
|
|
Example: "SELECT (SELECT ... ) as a ... GROUP BY a;"
|
|
*/
|
|
*simple_order = false;
|
|
} else if (order_tables & (RAND_TABLE_BIT | OUTER_REF_TABLE_BIT)) {
|
|
*simple_order = false;
|
|
} else {
|
|
if (cond != nullptr && check_field_is_const(cond, order->item[0])) {
|
|
trace_one_item.add("equals_constant_in_where", true);
|
|
continue;
|
|
}
|
|
if ((ref = order_tables & (not_const_tables ^ first_table))) {
|
|
if (!(order_tables & first_table) &&
|
|
only_eq_ref_tables(this, first_order, ref, &cached_eq_ref_tables,
|
|
&eq_ref_tables)) {
|
|
trace_one_item.add("eq_ref_to_preceding_items", true);
|
|
continue;
|
|
}
|
|
*simple_order = false; // Must do a temp table to sort
|
|
}
|
|
}
|
|
if (change) *prev_ptr = order; // use this entry
|
|
prev_ptr = &order->next;
|
|
}
|
|
if (change) *prev_ptr = nullptr;
|
|
if (prev_ptr == &first_order) // Nothing to sort/group
|
|
*simple_order = true;
|
|
DBUG_PRINT("exit", ("simple_order: %d", (int)*simple_order));
|
|
|
|
trace_each_item.end();
|
|
trace_simpl.add("resulting_clause_is_simple", *simple_order);
|
|
if (trace->is_started() && change) {
|
|
String str;
|
|
Query_block::print_order(
|
|
thd, &str, first_order,
|
|
enum_query_type(QT_TO_SYSTEM_CHARSET | QT_SHOW_SELECT_NUMBER |
|
|
QT_NO_DEFAULT_DB));
|
|
trace_simpl.add_utf8("resulting_clause", str.ptr(), str.length());
|
|
}
|
|
|
|
return first_order;
|
|
}
|
|
|
|
/**
|
|
Optimize conditions by
|
|
|
|
a) applying transitivity to build multiple equality predicates
|
|
(MEP): if x=y and y=z the MEP x=y=z is built.
|
|
b) apply constants where possible. If the value of x is known to be
|
|
42, x is replaced with a constant of value 42. By transitivity, this
|
|
also applies to MEPs, so the MEP in a) will become 42=x=y=z.
|
|
c) remove conditions that are always false or always true
|
|
|
|
@param thd Thread handler
|
|
@param[in,out] cond WHERE or HAVING condition to optimize
|
|
@param[out] cond_equal The built multiple equalities
|
|
@param join_list list of join operations with join conditions
|
|
= NULL: Called for HAVING condition
|
|
@param[out] cond_value Not changed if cond was empty
|
|
COND_TRUE if cond is always true
|
|
COND_FALSE if cond is impossible
|
|
COND_OK otherwise
|
|
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
|
|
bool optimize_cond(THD *thd, Item **cond, COND_EQUAL **cond_equal,
|
|
mem_root_deque<Table_ref *> *join_list,
|
|
Item::cond_result *cond_value) {
|
|
DBUG_TRACE;
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
Opt_trace_object trace_cond(trace, "condition_processing");
|
|
trace_cond.add_alnum("condition", join_list ? "WHERE" : "HAVING");
|
|
trace_cond.add("original_condition", *cond);
|
|
const Opt_trace_array trace_steps(trace, "steps");
|
|
|
|
/*
|
|
Enter this function
|
|
a) For a WHERE condition or a query having outer join.
|
|
b) For a HAVING condition.
|
|
*/
|
|
assert(*cond || join_list);
|
|
|
|
/*
|
|
Build all multiple equality predicates and eliminate equality
|
|
predicates that can be inferred from these multiple equalities.
|
|
For each reference of a field included into a multiple equality
|
|
that occurs in a function set a pointer to the multiple equality
|
|
predicate. Substitute a constant instead of this field if the
|
|
multiple equality contains a constant.
|
|
This is performed for the WHERE condition and any join conditions, but
|
|
not for the HAVING condition.
|
|
*/
|
|
if (join_list) {
|
|
Opt_trace_object step_wrapper(trace);
|
|
step_wrapper.add_alnum("transformation", "equality_propagation");
|
|
{
|
|
const Opt_trace_disable_I_S disable_trace_wrapper(
|
|
trace, !(*cond && (*cond)->has_subquery()));
|
|
const Opt_trace_array trace_subselect(trace, "subselect_evaluation");
|
|
if (build_equal_items(thd, *cond, cond, nullptr, true, join_list,
|
|
cond_equal))
|
|
return true;
|
|
}
|
|
step_wrapper.add("resulting_condition", *cond);
|
|
}
|
|
/*
|
|
change field = field to field = const for each found field = const
|
|
*/
|
|
if (*cond) {
|
|
Opt_trace_object step_wrapper(trace);
|
|
step_wrapper.add_alnum("transformation", "constant_propagation");
|
|
{
|
|
const Opt_trace_disable_I_S disable_trace_wrapper(
|
|
trace, !(*cond)->has_subquery());
|
|
const Opt_trace_array trace_subselect(trace, "subselect_evaluation");
|
|
if (propagate_cond_constants(thd, nullptr, *cond, *cond)) return true;
|
|
}
|
|
step_wrapper.add("resulting_condition", *cond);
|
|
}
|
|
|
|
/*
|
|
Remove all instances of item == item
|
|
Remove all and-levels where CONST item != CONST item
|
|
*/
|
|
DBUG_EXECUTE("where",
|
|
print_where(thd, *cond, "after const change", QT_ORDINARY););
|
|
if (*cond) {
|
|
Opt_trace_object step_wrapper(trace);
|
|
step_wrapper.add_alnum("transformation", "trivial_condition_removal");
|
|
{
|
|
const Opt_trace_disable_I_S disable_trace_wrapper(
|
|
trace, !(*cond)->has_subquery());
|
|
const Opt_trace_array trace_subselect(trace, "subselect_evaluation");
|
|
if (remove_eq_conds(thd, *cond, cond, cond_value)) return true;
|
|
}
|
|
step_wrapper.add("resulting_condition", *cond);
|
|
}
|
|
if (thd->is_error()) return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Checks if a condition can be evaluated during constant folding. It can be
|
|
evaluated if it is constant during execution and not expensive to evaluate. If
|
|
it contains a subquery, it should not be evaluated if the option
|
|
OPTION_NO_SUBQUERY_DURING_OPTIMIZATION is active.
|
|
*/
|
|
static bool can_evaluate_condition(THD *thd, Item *condition) {
|
|
return condition->const_for_execution() && !condition->cost().IsExpensive() &&
|
|
evaluate_during_optimization(condition,
|
|
thd->lex->current_query_block());
|
|
}
|
|
|
|
/**
|
|
Calls fold_condition. If that made the condition constant for execution,
|
|
simplify and fold again. @see fold_condition() for arguments.
|
|
*/
|
|
static bool fold_condition_exec(THD *thd, Item *cond, Item **retcond,
|
|
Item::cond_result *cond_value) {
|
|
if (fold_condition(thd, cond, retcond, cond_value)) return true;
|
|
if (*retcond != nullptr &&
|
|
can_evaluate_condition(thd, *retcond)) // simplify further maybe
|
|
return remove_eq_conds(thd, *retcond, retcond, cond_value);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Removes const and eq items. Returns the new item, or nullptr if no condition.
|
|
|
|
@param thd thread handler
|
|
@param cond the condition to handle
|
|
@param[out] retcond condition after const removal
|
|
@param[out] cond_value resulting value of the condition
|
|
=COND_OK condition must be evaluated (e.g. field = constant)
|
|
=COND_TRUE always true (e.g. 1 = 1)
|
|
=COND_FALSE always false (e.g. 1 = 2)
|
|
|
|
@returns false if success, true if error
|
|
*/
|
|
bool remove_eq_conds(THD *thd, Item *cond, Item **retcond,
|
|
Item::cond_result *cond_value) {
|
|
assert(cond->real_item()->is_bool_func());
|
|
if (cond->type() == Item::COND_ITEM) {
|
|
Item_cond *const item_cond = down_cast<Item_cond *>(cond);
|
|
const bool and_level = item_cond->functype() == Item_func::COND_AND_FUNC;
|
|
List_iterator<Item> li(*item_cond->argument_list());
|
|
bool should_fix_fields = false;
|
|
*cond_value = Item::COND_UNDEF;
|
|
Item *item;
|
|
while ((item = li++)) {
|
|
Item *new_item;
|
|
Item::cond_result tmp_cond_value;
|
|
if (remove_eq_conds(thd, item, &new_item, &tmp_cond_value)) return true;
|
|
|
|
if (new_item == nullptr)
|
|
li.remove();
|
|
else if (item != new_item) {
|
|
(void)li.replace(new_item);
|
|
should_fix_fields = true;
|
|
}
|
|
if (*cond_value == Item::COND_UNDEF) *cond_value = tmp_cond_value;
|
|
switch (tmp_cond_value) {
|
|
case Item::COND_OK: // Not true or false
|
|
if (and_level || *cond_value == Item::COND_FALSE)
|
|
*cond_value = tmp_cond_value;
|
|
break;
|
|
case Item::COND_FALSE:
|
|
if (and_level) // Always false
|
|
{
|
|
*cond_value = tmp_cond_value;
|
|
*retcond = nullptr;
|
|
return false;
|
|
}
|
|
break;
|
|
case Item::COND_TRUE:
|
|
if (!and_level) // Always true
|
|
{
|
|
*cond_value = tmp_cond_value;
|
|
*retcond = nullptr;
|
|
return false;
|
|
}
|
|
break;
|
|
case Item::COND_UNDEF: // Impossible
|
|
assert(false); /* purecov: deadcode */
|
|
}
|
|
}
|
|
if (should_fix_fields) item_cond->update_used_tables();
|
|
|
|
if (item_cond->argument_list()->elements == 0 ||
|
|
*cond_value != Item::COND_OK) {
|
|
*retcond = nullptr;
|
|
return false;
|
|
}
|
|
if (item_cond->argument_list()->elements == 1) {
|
|
/*
|
|
BUG#11765699:
|
|
We're dealing with an AND or OR item that has only one
|
|
argument. However, it is not an option to empty the list
|
|
because:
|
|
|
|
- this function is called for either JOIN::conds or
|
|
JOIN::having, but these point to the same condition as
|
|
Query_block::where and Query_block::having do.
|
|
|
|
- The return value of remove_eq_conds() is assigned to
|
|
JOIN::conds and JOIN::having, so emptying the list and
|
|
returning the only remaining item "replaces" the AND or OR
|
|
with item for the variables in JOIN. However, the return
|
|
value is not assigned to the Query_block counterparts. Thus,
|
|
if argument_list is emptied, Query_block forgets the item in
|
|
argument_list()->head().
|
|
|
|
item is therefore returned, but argument_list is not emptied.
|
|
*/
|
|
item = item_cond->argument_list()->head();
|
|
/*
|
|
Consider reenabling the line below when the optimizer has been
|
|
split into properly separated phases.
|
|
|
|
item_cond->argument_list()->empty();
|
|
*/
|
|
*retcond = item;
|
|
return false;
|
|
}
|
|
} else if (can_evaluate_condition(thd, cond)) {
|
|
bool value;
|
|
if (eval_const_cond(thd, cond, &value)) return true;
|
|
*cond_value = value ? Item::COND_TRUE : Item::COND_FALSE;
|
|
*retcond = nullptr;
|
|
return false;
|
|
} else { // Boolean compare function
|
|
*cond_value = cond->eq_cmp_result();
|
|
if (*cond_value == Item::COND_OK) {
|
|
return fold_condition_exec(thd, cond, retcond, cond_value);
|
|
}
|
|
Item *left_item = down_cast<Item_func *>(cond)->arguments()[0];
|
|
Item *right_item = down_cast<Item_func *>(cond)->arguments()[1];
|
|
if (left_item->eq(right_item, true) && !cond->is_non_deterministic()) {
|
|
/*
|
|
Two identical items are being compared:
|
|
1) If the items are not nullable, return result from eq_cmp_result(),
|
|
that is, we can short circuit because result is statically always
|
|
known to be true or false, depending on which operator we are
|
|
dealing with. If the operator allows equality, *cond_value is
|
|
Item::COND_TRUE (a non-null value is always equal to itself), else
|
|
Item::COND_FALSE (a non-null value is never unequal to itself).
|
|
2) If the items are nullable and the result from eq_cmp_result() is
|
|
false, result is always false, that is, the operator doesn't
|
|
allow for equality, the result is always false: Any non-null
|
|
value cannot obviously be unequal to itself, and any NULL value
|
|
would yield an undefined result (e.g. NULL < NULL
|
|
is undefined), and hence Item::COND_FALSE in this context is the
|
|
effective result.
|
|
(Call order ensures test is not applied to conditions with explicit
|
|
truth value test)
|
|
3) If the <=> operator is used, result is always true because
|
|
NULL = NULL is true for this operator
|
|
*/
|
|
if (!left_item->is_nullable() || *cond_value == Item::COND_FALSE ||
|
|
down_cast<Item_func *>(cond)->functype() == Item_func::EQUAL_FUNC) {
|
|
*retcond = nullptr;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return fold_condition_exec(thd, cond, retcond, cond_value);
|
|
}
|
|
|
|
/**
|
|
Check if GROUP BY/DISTINCT can be optimized away because the set is
|
|
already known to be distinct.
|
|
|
|
Used in removing the GROUP BY/DISTINCT of the following types of
|
|
statements:
|
|
@code
|
|
SELECT [DISTINCT] <unique_key_cols>... FROM <single_table_ref>
|
|
[GROUP BY <unique_key_cols>,...]
|
|
@endcode
|
|
|
|
If (a,b,c is distinct)
|
|
then <any combination of a,b,c>,{whatever} is also distinct
|
|
|
|
This function checks if all the key parts of any of the unique keys
|
|
of the table are referenced by a list : either the select list
|
|
through find_field_in_item_list or GROUP BY list through
|
|
find_field_in_order_list.
|
|
If the above holds and the key parts cannot contain NULLs then we
|
|
can safely remove the GROUP BY/DISTINCT,
|
|
as no result set can be more distinct than an unique key.
|
|
|
|
@param tab The join table to operate on.
|
|
@param find_func function to iterate over the list and search
|
|
for a field
|
|
@param data data that's passed through to to find_func
|
|
|
|
@retval
|
|
1 found
|
|
@retval
|
|
0 not found.
|
|
|
|
@note
|
|
The function assumes that make_outerjoin_info() has been called in
|
|
order for the check for outer tables to work.
|
|
*/
|
|
|
|
static bool list_contains_unique_index(JOIN_TAB *tab,
|
|
bool (*find_func)(Field *, void *),
|
|
void *data) {
|
|
TABLE *table = tab->table();
|
|
|
|
if (tab->is_inner_table_of_outer_join()) return false;
|
|
for (uint keynr = 0; keynr < table->s->keys; keynr++) {
|
|
if (keynr == table->s->primary_key ||
|
|
(table->key_info[keynr].flags & HA_NOSAME)) {
|
|
KEY *keyinfo = table->key_info + keynr;
|
|
KEY_PART_INFO *key_part, *key_part_end;
|
|
|
|
for (key_part = keyinfo->key_part,
|
|
key_part_end = key_part + keyinfo->user_defined_key_parts;
|
|
key_part < key_part_end; key_part++) {
|
|
if (key_part->field->is_nullable() || !find_func(key_part->field, data))
|
|
break;
|
|
}
|
|
if (key_part == key_part_end) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Helper function for list_contains_unique_index.
|
|
Find a field reference in a list of ORDER structures.
|
|
Finds a direct reference of the Field in the list.
|
|
|
|
@param field The field to search for.
|
|
@param data ORDER *.The list to search in
|
|
|
|
@retval
|
|
1 found
|
|
@retval
|
|
0 not found.
|
|
*/
|
|
|
|
static bool find_field_in_order_list(Field *field, void *data) {
|
|
ORDER *group = (ORDER *)data;
|
|
bool part_found = false;
|
|
for (ORDER *tmp_group = group; tmp_group; tmp_group = tmp_group->next) {
|
|
const Item *item = (*tmp_group->item)->real_item();
|
|
if (item->type() == Item::FIELD_ITEM &&
|
|
down_cast<const Item_field *>(item)->field->eq(field)) {
|
|
part_found = true;
|
|
break;
|
|
}
|
|
}
|
|
return part_found;
|
|
}
|
|
|
|
/**
|
|
Helper function for list_contains_unique_index.
|
|
Find a field reference in a dynamic list of Items.
|
|
Finds a direct reference of the Field in the list.
|
|
|
|
@param[in] field The field to search for.
|
|
@param[in] data List<Item> *.The list to search in
|
|
|
|
@retval
|
|
1 found
|
|
@retval
|
|
0 not found.
|
|
*/
|
|
|
|
static bool find_field_in_item_list(Field *field, void *data) {
|
|
mem_root_deque<Item *> *fields =
|
|
reinterpret_cast<mem_root_deque<Item *> *>(data);
|
|
bool part_found = false;
|
|
|
|
for (const Item *item : VisibleFields(*fields)) {
|
|
if (item->type() == Item::FIELD_ITEM &&
|
|
down_cast<const Item_field *>(item)->field->eq(field)) {
|
|
part_found = true;
|
|
break;
|
|
}
|
|
}
|
|
return part_found;
|
|
}
|
|
|
|
ORDER *create_order_from_distinct(THD *thd, Ref_item_array ref_item_array,
|
|
ORDER *order_list,
|
|
mem_root_deque<Item *> *fields,
|
|
bool skip_aggregates,
|
|
bool convert_bit_fields_to_long,
|
|
bool *all_order_by_fields_used) {
|
|
ORDER *group = nullptr, **prev = &group;
|
|
|
|
*all_order_by_fields_used = true;
|
|
|
|
for (ORDER *order = order_list; order; order = order->next) {
|
|
if (order->in_field_list) {
|
|
ORDER *ord = (ORDER *)thd->memdup((char *)order, sizeof(ORDER));
|
|
if (!ord) return nullptr;
|
|
*prev = ord;
|
|
prev = &ord->next;
|
|
(*ord->item)->marker = Item::MARKER_DISTINCT_GROUP;
|
|
} else
|
|
*all_order_by_fields_used = false;
|
|
}
|
|
|
|
Mem_root_array<std::pair<Item *, ORDER *>> bit_fields_to_add(thd->mem_root);
|
|
|
|
for (Item *&item : VisibleFields(*fields)) {
|
|
if (!item->const_item() && (!skip_aggregates || !item->has_aggregation()) &&
|
|
item->marker != Item::MARKER_DISTINCT_GROUP) {
|
|
/*
|
|
Don't put duplicate columns from the SELECT list into the
|
|
GROUP BY list.
|
|
*/
|
|
ORDER *ord_iter;
|
|
for (ord_iter = group; ord_iter; ord_iter = ord_iter->next)
|
|
if ((*ord_iter->item)->eq(item, true)) goto next_item;
|
|
|
|
ORDER *ord = (ORDER *)thd->mem_calloc(sizeof(ORDER));
|
|
if (!ord) return nullptr;
|
|
|
|
if (item->type() == Item::FIELD_ITEM &&
|
|
item->data_type() == MYSQL_TYPE_BIT && convert_bit_fields_to_long) {
|
|
/*
|
|
Because HEAP tables can't index BIT fields we need to use an
|
|
additional hidden field for grouping because later it will be
|
|
converted to a LONG field. Original field will remain of the
|
|
BIT type and will be returned to a client.
|
|
@note setup_ref_array() needs to account for the extra space.
|
|
@note We need to defer the actual adding to after the loop,
|
|
or we will invalidate the iterator to “fields”.
|
|
*/
|
|
Item_field *new_item = new Item_field(thd, (Item_field *)item);
|
|
ord->item = &item; // Temporary; for the duplicate check above.
|
|
bit_fields_to_add.push_back(std::make_pair(new_item, ord));
|
|
} else if (ref_item_array.is_null()) {
|
|
// No slices are in use, so just use the field from the list.
|
|
ord->item = &item;
|
|
} else {
|
|
/*
|
|
We have here only visible fields, so we can use simple indexing
|
|
of ref_item_array (order in the array and in the list are same)
|
|
*/
|
|
ord->item = &ref_item_array[0];
|
|
}
|
|
ord->direction = ORDER_ASC;
|
|
*prev = ord;
|
|
prev = &ord->next;
|
|
}
|
|
next_item:
|
|
if (!ref_item_array.is_null()) {
|
|
ref_item_array.pop_front();
|
|
}
|
|
}
|
|
for (const auto &item_and_order : bit_fields_to_add) {
|
|
item_and_order.second->item =
|
|
thd->lex->current_query_block()->add_hidden_item(item_and_order.first);
|
|
thd->lex->current_query_block()->hidden_items_from_optimization++;
|
|
}
|
|
*prev = nullptr;
|
|
return group;
|
|
}
|
|
|
|
/**
|
|
Return table number if there is only one table in sort order
|
|
and group and order is compatible, else return 0.
|
|
*/
|
|
|
|
static TABLE *get_sort_by_table(ORDER *a, ORDER *b, Table_ref *tables) {
|
|
DBUG_TRACE;
|
|
table_map map = (table_map)0;
|
|
|
|
if (!a)
|
|
a = b; // Only one need to be given
|
|
else if (!b)
|
|
b = a;
|
|
|
|
for (; a && b; a = a->next, b = b->next) {
|
|
if (!(*a->item)->eq(*b->item, true)) return nullptr;
|
|
map |= a->item[0]->used_tables();
|
|
}
|
|
map &= ~INNER_TABLE_BIT;
|
|
if (!map || (map & (RAND_TABLE_BIT | OUTER_REF_TABLE_BIT))) return nullptr;
|
|
|
|
for (; !(map & tables->map()); tables = tables->next_leaf)
|
|
;
|
|
if (map != tables->map()) return nullptr; // More than one table
|
|
DBUG_PRINT("exit", ("sort by table: %d", tables->tableno()));
|
|
return tables->table;
|
|
}
|
|
|
|
/**
|
|
Update some values in keyuse for faster choose_table_order() loop.
|
|
|
|
@todo Check if this is the real meaning of ref_table_rows.
|
|
*/
|
|
|
|
void JOIN::optimize_keyuse() {
|
|
for (size_t ix = 0; ix < keyuse_array.size(); ++ix) {
|
|
Key_use *keyuse = &keyuse_array.at(ix);
|
|
table_map map;
|
|
/*
|
|
If we find a ref, assume this table matches a proportional
|
|
part of this table.
|
|
For example 100 records matching a table with 5000 records
|
|
gives 5000/100 = 50 records per key
|
|
Constant tables are ignored.
|
|
To avoid bad matches, we don't make ref_table_rows less than 100.
|
|
*/
|
|
keyuse->ref_table_rows = ~(ha_rows)0; // If no ref
|
|
if (keyuse->used_tables &
|
|
(map = keyuse->used_tables & ~(const_table_map | PSEUDO_TABLE_BITS))) {
|
|
uint tableno;
|
|
for (tableno = 0; !(map & 1); map >>= 1, tableno++) {
|
|
}
|
|
if (map == 1) // Only one table
|
|
{
|
|
TABLE *tmp_table = join_tab[tableno].table();
|
|
|
|
keyuse->ref_table_rows =
|
|
max<ha_rows>(tmp_table->file->stats.records, 100);
|
|
}
|
|
}
|
|
/*
|
|
Outer reference (external field) is constant for single executing
|
|
of subquery
|
|
*/
|
|
if (keyuse->used_tables == OUTER_REF_TABLE_BIT) keyuse->ref_table_rows = 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
Function sets FT hints, initializes FT handlers
|
|
and checks if FT index can be used as covered.
|
|
*/
|
|
|
|
bool JOIN::optimize_fts_query() {
|
|
ASSERT_BEST_REF_IN_JOIN_ORDER(this);
|
|
|
|
assert(query_block->has_ft_funcs());
|
|
|
|
// Only used by the old optimizer.
|
|
assert(!thd->lex->using_hypergraph_optimizer());
|
|
|
|
for (uint i = const_tables; i < tables; i++) {
|
|
JOIN_TAB *tab = best_ref[i];
|
|
if (tab->type() != JT_FT) continue;
|
|
|
|
Item_func_match *ifm;
|
|
Item_func_match *ft_func =
|
|
down_cast<Item_func_match *>(tab->position()->key->val);
|
|
List_iterator<Item_func_match> li(*(query_block->ftfunc_list));
|
|
|
|
while ((ifm = li++)) {
|
|
if (!(ifm->used_tables() & tab->table_ref->map()) || ifm->master)
|
|
continue;
|
|
|
|
if (ifm != ft_func) {
|
|
if (ifm->can_skip_ranking())
|
|
ifm->set_hints(this, FT_NO_RANKING, HA_POS_ERROR, false);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Check if internal sorting is needed. FT_SORTED flag is set
|
|
if no ORDER BY clause or ORDER BY MATCH function is the same
|
|
as the function that is used for FT index and FT table is
|
|
the first non-constant table in the JOIN.
|
|
*/
|
|
if (i == const_tables && !(ft_func->get_hints()->get_flags() & FT_BOOL) &&
|
|
(order.empty() || ft_func == test_if_ft_index_order(order.order)))
|
|
ft_func->set_hints(this, FT_SORTED, m_select_limit, false);
|
|
|
|
/*
|
|
Check if ranking is not needed. FT_NO_RANKING flag is set if
|
|
MATCH function is used only in WHERE condition and MATCH
|
|
function is not part of an expression.
|
|
*/
|
|
if (ft_func->can_skip_ranking())
|
|
ft_func->set_hints(this, FT_NO_RANKING,
|
|
order.empty() ? m_select_limit : HA_POS_ERROR, false);
|
|
}
|
|
|
|
return init_ftfuncs(thd, query_block);
|
|
}
|
|
|
|
/**
|
|
Check if FTS index only access is possible.
|
|
|
|
@param tab pointer to JOIN_TAB structure.
|
|
|
|
@return true if index only access is possible,
|
|
false otherwise.
|
|
*/
|
|
|
|
bool JOIN::fts_index_access(JOIN_TAB *tab) {
|
|
assert(tab->type() == JT_FT);
|
|
TABLE *table = tab->table();
|
|
|
|
// Give up if index-only access has already been disabled on this table.
|
|
if (table->no_keyread) {
|
|
return false;
|
|
}
|
|
|
|
if ((table->file->ha_table_flags() & HA_CAN_FULLTEXT_EXT) == 0)
|
|
return false; // Optimizations requires extended FTS support by table
|
|
// engine
|
|
|
|
/*
|
|
This optimization does not work with filesort nor GROUP BY
|
|
*/
|
|
if (grouped ||
|
|
(!order.empty() && m_ordered_index_usage != ORDERED_INDEX_ORDER_BY))
|
|
return false;
|
|
|
|
/*
|
|
Check whether the FTS result is covering. If only document id
|
|
and rank is needed, there is no need to access table rows.
|
|
*/
|
|
for (uint i = bitmap_get_first_set(table->read_set); i < table->s->fields;
|
|
i = bitmap_get_next_set(table->read_set, i)) {
|
|
if (table->field[i] != table->fts_doc_id_field ||
|
|
!tab->ft_func()->docid_in_result())
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool JOIN::contains_non_aggregated_fts() const {
|
|
return query_block->has_ft_funcs() &&
|
|
std::any_of(fields->begin(), fields->end(), [](Item *item) {
|
|
return WalkItem(item, enum_walk::PREFIX | enum_walk::POSTFIX,
|
|
NonAggregatedFullTextSearchVisitor(
|
|
[](Item_func_match *) { return true; }));
|
|
});
|
|
}
|
|
|
|
/**
|
|
For {semijoin,subquery} materialization: calculates various cost
|
|
information, based on a plan in join->best_positions covering the
|
|
to-be-materialized query block and only this.
|
|
|
|
@param join JOIN where plan can be found
|
|
@param sj_nest sj materialization nest (NULL if subquery materialization)
|
|
@param n_tables number of to-be-materialized tables
|
|
@param[out] sjm where computed costs will be stored
|
|
|
|
@note that this function modifies join->map2table, which has to be filled
|
|
correctly later.
|
|
*/
|
|
static void calculate_materialization_costs(JOIN *join, Table_ref *sj_nest,
|
|
uint n_tables,
|
|
Semijoin_mat_optimize *sjm) {
|
|
double mat_cost; // Estimated cost of materialization
|
|
double mat_rowcount; // Estimated row count before duplicate removal
|
|
double distinct_rowcount; // Estimated rowcount after duplicate removal
|
|
mem_root_deque<Item *> *inner_expr_list;
|
|
|
|
if (sj_nest) {
|
|
/*
|
|
get_partial_join_cost() assumes a regular join, which is correct when
|
|
we optimize a sj-materialization nest (always executed as regular
|
|
join).
|
|
*/
|
|
get_partial_join_cost(join, n_tables, &mat_cost, &mat_rowcount);
|
|
n_tables += join->const_tables;
|
|
inner_expr_list = &sj_nest->nested_join->sj_inner_exprs;
|
|
} else {
|
|
mat_cost = join->best_read;
|
|
mat_rowcount = static_cast<double>(join->best_rowcount);
|
|
inner_expr_list = &join->query_block->fields;
|
|
}
|
|
|
|
/*
|
|
Adjust output cardinality estimates. If the subquery has form
|
|
|
|
... oe IN (SELECT t1.colX, t2.colY, func(X,Y,Z) )
|
|
|
|
then the number of distinct output record combinations has an
|
|
upper bound of product of number of records matching the tables
|
|
that are used by the SELECT clause.
|
|
TODO:
|
|
We can get a more precise estimate if we
|
|
- use rec_per_key cardinality estimates. For simple cases like
|
|
"oe IN (SELECT t.key ...)" it is trivial.
|
|
- Functional dependencies between the tables in the semi-join
|
|
nest (the payoff is probably less here?)
|
|
*/
|
|
{
|
|
for (uint i = 0; i < n_tables; i++) {
|
|
JOIN_TAB *const tab = join->best_positions[i].table;
|
|
join->map2table[tab->table_ref->tableno()] = tab;
|
|
}
|
|
table_map map = 0;
|
|
for (Item *item : VisibleFields(*inner_expr_list)) {
|
|
map |= item->used_tables();
|
|
}
|
|
map &= ~PSEUDO_TABLE_BITS;
|
|
double rows = 1.0;
|
|
for (size_t tableno : BitsSetIn(map)) {
|
|
rows *= join->map2table[tableno]->table()->quick_condition_rows;
|
|
}
|
|
distinct_rowcount = min(mat_rowcount, rows);
|
|
}
|
|
/*
|
|
Calculate temporary table parameters and usage costs
|
|
*/
|
|
const uint rowlen = get_tmp_table_rec_length(*inner_expr_list);
|
|
|
|
const Cost_model_server *cost_model = join->cost_model();
|
|
|
|
Cost_model_server::enum_tmptable_type tmp_table_type;
|
|
if (rowlen * distinct_rowcount < join->thd->variables.max_heap_table_size)
|
|
tmp_table_type = Cost_model_server::MEMORY_TMPTABLE;
|
|
else
|
|
tmp_table_type = Cost_model_server::DISK_TMPTABLE;
|
|
|
|
/*
|
|
Let materialization cost include the cost to create the temporary
|
|
table and write the rows into it:
|
|
*/
|
|
mat_cost += cost_model->tmptable_create_cost(tmp_table_type);
|
|
mat_cost +=
|
|
cost_model->tmptable_readwrite_cost(tmp_table_type, mat_rowcount, 0.0);
|
|
|
|
sjm->materialization_cost.reset();
|
|
sjm->materialization_cost.add_io(mat_cost);
|
|
|
|
sjm->expected_rowcount = distinct_rowcount;
|
|
|
|
/*
|
|
Set the cost to do a full scan of the temptable (will need this to
|
|
consider doing sjm-scan):
|
|
*/
|
|
sjm->scan_cost.reset();
|
|
if (distinct_rowcount > 0.0) {
|
|
const double scan_cost = cost_model->tmptable_readwrite_cost(
|
|
tmp_table_type, 0.0, distinct_rowcount);
|
|
sjm->scan_cost.add_io(scan_cost);
|
|
}
|
|
|
|
// The cost to lookup a row in temp. table
|
|
const double row_cost =
|
|
cost_model->tmptable_readwrite_cost(tmp_table_type, 0.0, 1.0);
|
|
sjm->lookup_cost.reset();
|
|
sjm->lookup_cost.add_io(row_cost);
|
|
}
|
|
|
|
/**
|
|
Decides between EXISTS and materialization; performs last steps to set up
|
|
the chosen strategy.
|
|
@returns 'false' if no error
|
|
|
|
@note If UNION this is called on each contained JOIN.
|
|
|
|
*/
|
|
bool JOIN::decide_subquery_strategy() {
|
|
assert(query_expression()->item);
|
|
|
|
switch (query_expression()->item->subquery_type()) {
|
|
case Item_subselect::IN_SUBQUERY:
|
|
case Item_subselect::ALL_SUBQUERY:
|
|
case Item_subselect::ANY_SUBQUERY:
|
|
// All of those are children of Item_in_subselect and may use EXISTS
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
Item_in_subselect *const in_pred =
|
|
static_cast<Item_in_subselect *>(query_expression()->item);
|
|
|
|
Subquery_strategy chosen_method = in_pred->strategy;
|
|
// Materialization does not allow UNION so this can't happen:
|
|
assert(chosen_method != Subquery_strategy::SUBQ_MATERIALIZATION);
|
|
|
|
if ((chosen_method == Subquery_strategy::CANDIDATE_FOR_IN2EXISTS_OR_MAT) &&
|
|
compare_costs_of_subquery_strategies(&chosen_method))
|
|
return true;
|
|
|
|
switch (chosen_method) {
|
|
case Subquery_strategy::SUBQ_EXISTS:
|
|
if (query_block->m_windows.elements > 0) // grep for WL#10431
|
|
{
|
|
my_error(ER_NOT_SUPPORTED_YET, MYF(0),
|
|
"the combination of this ALL/ANY/SOME/IN subquery with this"
|
|
" comparison operator and with contained window functions");
|
|
return true;
|
|
}
|
|
return in_pred->finalize_exists_transform(thd, query_block);
|
|
case Subquery_strategy::SUBQ_MATERIALIZATION:
|
|
return in_pred->finalize_materialization_transform(thd, this);
|
|
default:
|
|
assert(false);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
Tells what is the cheapest between IN->EXISTS and subquery materialization,
|
|
in terms of cost, for the subquery's JOIN.
|
|
Input:
|
|
- join->{best_positions,best_read,best_rowcount} must contain the
|
|
execution plan of EXISTS (where 'join' is the subquery's JOIN)
|
|
- join2->{best_positions,best_read,best_rowcount} must be correctly set
|
|
(where 'join2' is the parent join, the grandparent join, etc).
|
|
Output:
|
|
join->{best_positions,best_read,best_rowcount} contain the cheapest
|
|
execution plan (where 'join' is the subquery's JOIN).
|
|
|
|
This plan choice has to happen before calling functions which set up
|
|
execution structures, like JOIN::get_best_combination().
|
|
|
|
@param[out] method chosen method (EXISTS or materialization) will be put
|
|
here.
|
|
@returns false if success
|
|
*/
|
|
bool JOIN::compare_costs_of_subquery_strategies(Subquery_strategy *method) {
|
|
*method = Subquery_strategy::SUBQ_EXISTS;
|
|
|
|
Subquery_strategy allowed_strategies = query_block->subquery_strategy(thd);
|
|
|
|
/*
|
|
A non-deterministic subquery should not use materialization, unless forced.
|
|
For a detailed explanation, see Query_block::decorrelate_where_cond().
|
|
Here, the same logic is applied also for subqueries that are not converted
|
|
to semi-join.
|
|
*/
|
|
if (allowed_strategies == Subquery_strategy::CANDIDATE_FOR_IN2EXISTS_OR_MAT &&
|
|
(query_expression()->uncacheable & UNCACHEABLE_RAND))
|
|
allowed_strategies = Subquery_strategy::SUBQ_EXISTS;
|
|
|
|
if (allowed_strategies == Subquery_strategy::SUBQ_EXISTS) return false;
|
|
|
|
assert(allowed_strategies ==
|
|
Subquery_strategy::CANDIDATE_FOR_IN2EXISTS_OR_MAT ||
|
|
allowed_strategies == Subquery_strategy::SUBQ_MATERIALIZATION);
|
|
|
|
const JOIN *parent_join = query_expression()->outer_query_block()->join;
|
|
if (!parent_join || !parent_join->child_subquery_can_materialize)
|
|
return false;
|
|
|
|
Item_in_subselect *const in_pred =
|
|
static_cast<Item_in_subselect *>(query_expression()->item);
|
|
|
|
/*
|
|
Testing subquery_allows_etc() at each optimization is necessary as each
|
|
execution of a prepared statement may use a different type of parameter.
|
|
*/
|
|
if (!in_pred->subquery_allows_materialization(
|
|
thd, query_block, query_block->outer_query_block()))
|
|
return false;
|
|
|
|
Opt_trace_context *const trace = &thd->opt_trace;
|
|
const Opt_trace_object trace_wrapper(trace);
|
|
Opt_trace_object trace_subqmat(
|
|
trace, "execution_plan_for_potential_materialization");
|
|
const double saved_best_read = best_read;
|
|
const ha_rows saved_best_rowcount = best_rowcount;
|
|
POSITION *const saved_best_pos = best_positions;
|
|
|
|
if (in_pred->in2exists_added_to_where()) {
|
|
const Opt_trace_array trace_subqmat_steps(trace, "steps");
|
|
|
|
// Up to one extra slot per semi-join nest is needed (if materialized)
|
|
const uint sj_nests = query_block->sj_nests.size();
|
|
|
|
if (!(best_positions = new (thd->mem_root) POSITION[tables + sj_nests]))
|
|
return true;
|
|
|
|
// Compute plans which do not use outer references
|
|
|
|
assert(allow_outer_refs);
|
|
allow_outer_refs = false;
|
|
|
|
if (optimize_semijoin_nests_for_materialization(this)) return true;
|
|
|
|
if (Optimize_table_order(thd, this, nullptr).choose_table_order())
|
|
return true;
|
|
} else {
|
|
/*
|
|
If IN->EXISTS didn't add any condition to WHERE (only to HAVING, which
|
|
can happen if subquery has aggregates) then the plan for materialization
|
|
will be the same as for EXISTS - don't compute it again.
|
|
*/
|
|
trace_subqmat.add("surely_same_plan_as_EXISTS", true)
|
|
.add_alnum("cause", "EXISTS_did_not_change_WHERE");
|
|
}
|
|
|
|
Semijoin_mat_optimize sjm;
|
|
calculate_materialization_costs(this, nullptr, primary_tables, &sjm);
|
|
|
|
/*
|
|
The number of evaluations of the subquery influences costs, we need to
|
|
compute it.
|
|
*/
|
|
Opt_trace_object trace_subq_mat_decision(trace, "subq_mat_decision");
|
|
const double subq_executions = calculate_subquery_executions(in_pred, trace);
|
|
const double cost_exists = subq_executions * saved_best_read;
|
|
const double cost_mat_table = sjm.materialization_cost.total_cost();
|
|
const double cost_mat =
|
|
cost_mat_table + subq_executions * sjm.lookup_cost.total_cost();
|
|
const bool mat_chosen =
|
|
(allowed_strategies == Subquery_strategy::CANDIDATE_FOR_IN2EXISTS_OR_MAT)
|
|
? (cost_mat < cost_exists)
|
|
: true;
|
|
trace_subq_mat_decision
|
|
.add("cost_to_create_and_fill_materialized_table", cost_mat_table)
|
|
.add("cost_of_one_EXISTS", saved_best_read)
|
|
.add("number_of_subquery_evaluations", subq_executions)
|
|
.add("cost_of_materialization", cost_mat)
|
|
.add("cost_of_EXISTS", cost_exists)
|
|
.add("chosen", mat_chosen);
|
|
if (mat_chosen) {
|
|
*method = Subquery_strategy::SUBQ_MATERIALIZATION;
|
|
} else {
|
|
best_read = saved_best_read;
|
|
best_rowcount = saved_best_rowcount;
|
|
best_positions = saved_best_pos;
|
|
/*
|
|
Don't restore JOIN::positions or best_ref, they're not used
|
|
afterwards. best_positions is (like: by get_sj_strategy()).
|
|
*/
|
|
}
|
|
return false;
|
|
}
|
|
|
|
double calculate_subquery_executions(const Item_subselect *subquery,
|
|
Opt_trace_context *trace) {
|
|
const Opt_trace_array trace_parents(trace, "parent_fanouts");
|
|
double subquery_executions = 1.0;
|
|
for (;;) {
|
|
const Query_block *const parent_query_block =
|
|
subquery->query_expr()->outer_query_block();
|
|
const JOIN *const parent_join = parent_query_block->join;
|
|
if (parent_join == nullptr) {
|
|
/*
|
|
May be single-table UPDATE/DELETE, has no join.
|
|
@todo we should find how many rows it plans to UPDATE/DELETE, taking
|
|
inspiration in Explain_table::explain_rows_and_filtered().
|
|
This is not a priority as it applies only to
|
|
UPDATE - child(non-mat-subq) - grandchild(may-be-mat-subq).
|
|
And it will autosolve the day UPDATE gets a JOIN.
|
|
*/
|
|
break;
|
|
}
|
|
|
|
Opt_trace_object trace_parent(trace);
|
|
trace_parent.add_select_number(parent_query_block->select_number);
|
|
double parent_fanout;
|
|
if ( // safety, not sure needed
|
|
parent_join->plan_is_const() ||
|
|
// if subq is in condition on constant table:
|
|
!parent_join->child_subquery_can_materialize) {
|
|
parent_fanout = 1.0;
|
|
trace_parent.add("subq_attached_to_const_table", true);
|
|
} else {
|
|
if (subquery->in_cond_of_tab != NO_PLAN_IDX) {
|
|
/*
|
|
Subquery is attached to a certain 'pos', pos[-1].prefix_rowcount
|
|
is the number of times we'll start a loop accessing 'pos'; each such
|
|
loop will read pos->rows_fetched rows of 'pos', so subquery will
|
|
be evaluated pos[-1].prefix_rowcount * pos->rows_fetched times.
|
|
Exceptions:
|
|
- if 'pos' is first, use 1.0 instead of pos[-1].prefix_rowcount
|
|
- if 'pos' is first of a sj-materialization nest, same.
|
|
|
|
If in a sj-materialization nest, pos->rows_fetched and
|
|
pos[-1].prefix_rowcount are of the "nest materialization" plan
|
|
(copied back in fix_semijoin_strategies()), which is
|
|
appropriate as it corresponds to evaluations of our subquery.
|
|
|
|
pos->prefix_rowcount is not suitable because if we have:
|
|
select ... from ot1 where ot1.col in
|
|
(select it1.col1 from it1 where it1.col2 not in (subq));
|
|
and subq does subq-mat, and plan is ot1 - it1+firstmatch(ot1),
|
|
then:
|
|
- t1.prefix_rowcount==1 (due to firstmatch)
|
|
- subq is attached to it1, and is evaluated for each row read from
|
|
t1, potentially way more than 1.
|
|
*/
|
|
const uint idx = subquery->in_cond_of_tab;
|
|
assert((int)idx >= 0 && idx < parent_join->tables);
|
|
trace_parent.add("subq_attached_to_table", true);
|
|
QEP_TAB *const parent_tab = &parent_join->qep_tab[idx];
|
|
trace_parent.add_utf8_table(parent_tab->table_ref);
|
|
parent_fanout = parent_tab->position()->rows_fetched;
|
|
if ((idx > parent_join->const_tables) &&
|
|
!sj_is_materialize_strategy(parent_tab->position()->sj_strategy))
|
|
parent_fanout *= parent_tab[-1].position()->prefix_rowcount;
|
|
} else {
|
|
/*
|
|
Subquery is SELECT list, GROUP BY, ORDER BY, HAVING: it is evaluated
|
|
at the end of the parent join's execution.
|
|
It can be evaluated once per row-before-grouping:
|
|
SELECT SUM(t1.col IN (subq)) FROM t1 GROUP BY expr;
|
|
or once per row-after-grouping:
|
|
SELECT SUM(t1.col) AS s FROM t1 GROUP BY expr HAVING s IN (subq),
|
|
SELECT SUM(t1.col) IN (subq) FROM t1 GROUP BY expr
|
|
It's hard to tell. We simply assume 'once per
|
|
row-before-grouping'.
|
|
|
|
Another approximation:
|
|
SELECT ... HAVING x IN (subq) LIMIT 1
|
|
best_rowcount=1 due to LIMIT, though HAVING (and thus the subquery)
|
|
may be evaluated many times before HAVING becomes true and the limit
|
|
is reached.
|
|
*/
|
|
trace_parent.add("subq_attached_to_join_result", true);
|
|
parent_fanout = static_cast<double>(parent_join->best_rowcount);
|
|
}
|
|
}
|
|
subquery_executions *= parent_fanout;
|
|
trace_parent.add("fanout", parent_fanout);
|
|
const bool cacheable = parent_query_block->is_cacheable();
|
|
trace_parent.add("cacheable", cacheable);
|
|
if (cacheable) {
|
|
// Parent executed only once
|
|
break;
|
|
}
|
|
/*
|
|
Parent query is executed once per outer row => go up to find number of
|
|
outer rows. Example:
|
|
SELECT ... IN(subq-with-in2exists WHERE ... IN (subq-with-mat))
|
|
*/
|
|
subquery = parent_join->query_expression()->item;
|
|
if (subquery == nullptr) {
|
|
// derived table, materialized only once
|
|
break;
|
|
}
|
|
} // for(;;)
|
|
return subquery_executions;
|
|
}
|
|
|
|
/**
|
|
Optimize rollup specification.
|
|
|
|
Allocate objects needed for rollup processing.
|
|
|
|
@returns false if success, true if error.
|
|
*/
|
|
|
|
bool JOIN::optimize_rollup() {
|
|
tmp_table_param.allow_group_via_temp_table = false;
|
|
rollup_state = RollupState::INITED;
|
|
tmp_table_param.group_parts = send_group_parts;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
Refine the best_rowcount estimation based on what happens after tables
|
|
have been joined: LIMIT and type of result sink.
|
|
*/
|
|
void JOIN::refine_best_rowcount() {
|
|
// If plan is const, 0 or 1 rows should be returned
|
|
assert(!plan_is_const() || best_rowcount <= 1);
|
|
|
|
if (plan_is_const()) return;
|
|
|
|
/*
|
|
If a derived table, or a member of a UNION which itself forms a derived
|
|
table:
|
|
setting estimate to 0 or 1 row would mark the derived table as const.
|
|
The row count is bumped to the nearest higher value, so that the
|
|
query block will not be evaluated during optimization.
|
|
*/
|
|
if (best_rowcount <= 1 &&
|
|
query_block->master_query_expression()->first_query_block()->linkage ==
|
|
DERIVED_TABLE_TYPE)
|
|
best_rowcount = PLACEHOLDER_TABLE_ROW_ESTIMATE;
|
|
|
|
/*
|
|
There will be no more rows than defined in the LIMIT clause. Use it
|
|
as an estimate. If LIMIT 1 is specified, the query block will be
|
|
considered "const", with actual row count 0 or 1.
|
|
*/
|
|
best_rowcount = std::min(best_rowcount, query_expression()->select_limit_cnt);
|
|
}
|
|
|
|
mem_root_deque<Item *> *JOIN::get_current_fields() {
|
|
assert((int)current_ref_item_slice >= 0);
|
|
if (current_ref_item_slice == REF_SLICE_SAVED_BASE) return fields;
|
|
return &tmp_fields[current_ref_item_slice];
|
|
}
|
|
|
|
const Cost_model_server *JOIN::cost_model() const {
|
|
assert(thd != nullptr);
|
|
return thd->cost_model();
|
|
}
|
|
|
|
/**
|
|
@} (end of group Query_Optimizer)
|
|
*/
|
|
|
|
/**
|
|
This function is used to get the key length of Item object on
|
|
which one tmp field will be created during create_tmp_table.
|
|
This function references KEY_PART_INFO::init_from_field().
|
|
|
|
@param item A inner item of outer join
|
|
|
|
@return The length of a item to be as a key of a temp table
|
|
*/
|
|
|
|
static uint32 get_key_length_tmp_table(Item *item) {
|
|
uint32 len = 0;
|
|
|
|
item = item->real_item();
|
|
if (item->type() == Item::FIELD_ITEM)
|
|
len = ((Item_field *)item)->field->key_length();
|
|
else
|
|
len = item->max_length;
|
|
|
|
if (item->is_nullable()) len += HA_KEY_NULL_LENGTH;
|
|
|
|
// references KEY_PART_INFO::init_from_field()
|
|
const enum_field_types type = item->data_type();
|
|
if (type == MYSQL_TYPE_BLOB || type == MYSQL_TYPE_VARCHAR ||
|
|
type == MYSQL_TYPE_GEOMETRY)
|
|
len += HA_KEY_BLOB_LENGTH;
|
|
|
|
return len;
|
|
}
|
|
|
|
bool evaluate_during_optimization(const Item *item, const Query_block *select) {
|
|
/*
|
|
Should only be called on items that are const_for_execution(), as those
|
|
items are the only ones that are allowed to be evaluated during optimization
|
|
in the first place.
|
|
|
|
Additionally, allow items that only access tables in JOIN::const_table_map.
|
|
This should not be necessary, but the const_for_execution() property is not
|
|
always updated correctly by update_used_tables() for certain subqueries.
|
|
*/
|
|
assert(item->const_for_execution() ||
|
|
(item->used_tables() & ~select->join->const_table_map) == 0);
|
|
|
|
// If the Item does not access any tables, it can always be evaluated.
|
|
if (item->const_item()) return true;
|
|
|
|
return !item->has_subquery() || (select->active_options() &
|
|
OPTION_NO_SUBQUERY_DURING_OPTIMIZATION) == 0;
|
|
}
|
|
|
|
/// Does this path scan any base tables in a secondary engine?
|
|
static bool ReferencesSecondaryEngineBaseTables(AccessPath *path) {
|
|
bool found = false;
|
|
WalkAccessPaths(path, /*join=*/nullptr, WalkAccessPathPolicy::ENTIRE_TREE,
|
|
[&found](const AccessPath *subpath, const JOIN *) {
|
|
TABLE *table = GetBasicTable(subpath);
|
|
if (table != nullptr && table->s->is_secondary_engine()) {
|
|
found = true;
|
|
}
|
|
return found;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
bool IteratorsAreNeeded(const THD *thd, AccessPath *root_path) {
|
|
const handlerton *secondary_engine = SecondaryEngineHandlerton(thd);
|
|
|
|
// Queries running in the primary engine always need iterators.
|
|
if (secondary_engine == nullptr) {
|
|
return true;
|
|
}
|
|
|
|
// If the entire query is optimized away, we create iterators regardless of
|
|
// whether an external executor is used, since the secondary engine may decide
|
|
// not to offload the query to the external executor in this case.
|
|
if (!ReferencesSecondaryEngineBaseTables(root_path)) {
|
|
return true;
|
|
}
|
|
|
|
// Otherwise, create iterators if the secondary engine does not use an
|
|
// external executor.
|
|
return !IsBitSet(static_cast<int>(SecondaryEngineFlag::USE_EXTERNAL_EXECUTOR),
|
|
secondary_engine->secondary_engine_flags);
|
|
}
|
|
|
|
/// Constant used to signal that there is no limit in EstimateRowAccesses().
|
|
static constexpr double kNoLimit = std::numeric_limits<double>::infinity();
|
|
|
|
/**
|
|
Estimates how many rows that have to be read from the outer table of a join in
|
|
order to reach the given limit.
|
|
|
|
@param join_path The AccessPath representing the join.
|
|
@param outer The AccessPath representing the outer table in the join.
|
|
@param limit The maximum number of rows to read from the join result.
|
|
|
|
@return The number of rows to read from the outer table before reaching the
|
|
limit, or kNoLimit if the entire table is expected to be read.
|
|
*/
|
|
static double GetRowsNeededFromOuterTable(const AccessPath *join_path,
|
|
const AccessPath *outer,
|
|
double limit) {
|
|
const double input_rows = outer->num_output_rows();
|
|
const double output_rows = join_path->num_output_rows();
|
|
|
|
if (input_rows > 0 && output_rows > 0) {
|
|
const double fanout = output_rows / input_rows;
|
|
return ceil(limit / fanout);
|
|
}
|
|
|
|
return kNoLimit;
|
|
}
|
|
|
|
/**
|
|
Estimates the number of row accesses that will be performed by a nested loop
|
|
join.
|
|
|
|
Nested loop join reads the outer table once, and the inner table once per row
|
|
in the outer table. If there is a limit on the query, it might not need to
|
|
read all rows in the outer table.
|
|
|
|
@param join_path The AccessPath representing the nested loop join.
|
|
@param num_evaluations The number of times the join will be executed.
|
|
@param limit The maximum number of rows to read from the join result.
|
|
@return An estimate of the number of row accesses.
|
|
*/
|
|
static double EstimateRowAccessesInNestedLoopJoin(const AccessPath *join_path,
|
|
const AccessPath *outer,
|
|
const AccessPath *inner,
|
|
double num_evaluations,
|
|
double limit) {
|
|
const double limit_on_outer =
|
|
GetRowsNeededFromOuterTable(join_path, outer, limit);
|
|
return EstimateRowAccesses(outer, num_evaluations, limit_on_outer) +
|
|
EstimateRowAccesses(
|
|
inner,
|
|
num_evaluations * min(limit_on_outer, outer->num_output_rows()),
|
|
kNoLimit);
|
|
}
|
|
|
|
/**
|
|
Estimates the number of row accesses performed by the subqueries contained in
|
|
an item. The returned estimate is pessimistic, as it assumes the contained
|
|
subqueries are evaluated each time the item is evaluated. If the item for
|
|
example is an Item_cond_or, say, x=y OR (SELECT ...), the subquery is not
|
|
evaluated if x=y is true, but the estimate does not take the selectivity of
|
|
x=y into account.
|
|
|
|
@param item The item to check.
|
|
@param num_evaluations The number of times the item is evaluated.
|
|
@return An estimate of the number of row accesses.
|
|
*/
|
|
static double EstimateRowAccessesInItem(Item *item, double num_evaluations) {
|
|
double rows = 0.0;
|
|
WalkItem(item, enum_walk::PREFIX, [num_evaluations, &rows](Item *subitem) {
|
|
if (subitem->type() == Item::SUBQUERY_ITEM) {
|
|
Item_subselect *subselect = down_cast<Item_subselect *>(subitem);
|
|
Query_expression *qe = subselect->query_expr();
|
|
Query_block *query_block = qe->first_query_block();
|
|
AccessPath *path;
|
|
if (qe->root_access_path() != nullptr) {
|
|
path = qe->root_access_path();
|
|
} else {
|
|
path = qe->item->root_access_path();
|
|
}
|
|
rows += EstimateRowAccesses(
|
|
path, query_block->is_cacheable() ? 1.0 : num_evaluations, kNoLimit);
|
|
}
|
|
return false;
|
|
});
|
|
return rows;
|
|
}
|
|
|
|
double EstimateRowAccesses(const AccessPath *path, double num_evaluations,
|
|
double limit) {
|
|
assert(limit >= 0.0);
|
|
|
|
double rows = 0.0;
|
|
WalkAccessPaths(
|
|
path, /*join=*/nullptr, WalkAccessPathPolicy::ENTIRE_TREE,
|
|
[num_evaluations, limit, &rows](const AccessPath *subpath, const JOIN *) {
|
|
// Count rows accessed in base tables.
|
|
if (const TABLE *table = GetBasicTable(subpath);
|
|
table != nullptr && table->s != nullptr &&
|
|
table->s->tmp_table != INTERNAL_TMP_TABLE &&
|
|
table->pos_in_table_list != nullptr &&
|
|
table->pos_in_table_list->is_base_table()) {
|
|
double num_output_rows = subpath->num_output_rows();
|
|
|
|
// Workaround for the old optimizer only: test_if_skip_sort_order()
|
|
// sets the cardinality of index scans to the same as the query
|
|
// block's limit, if there is one, so the estimate in the access path
|
|
// may be too low. Get the cardinality from the handler's statistics
|
|
// instead.
|
|
if (subpath->type == AccessPath::INDEX_SCAN &&
|
|
!current_thd->lex->using_hypergraph_optimizer()) {
|
|
num_output_rows = table->file->stats.records;
|
|
}
|
|
|
|
// Workaround for HeatWave. All access paths in HeatWave currently
|
|
// have num_output_rows set to zero. Get the handler's estimate
|
|
// instead.
|
|
if (num_output_rows == 0 && table->s->is_secondary_engine()) {
|
|
assert(subpath->type == AccessPath::TABLE_SCAN ||
|
|
subpath->type == AccessPath::SAMPLE_SCAN);
|
|
num_output_rows = table->file->stats.records;
|
|
if (table->pos_in_table_list->has_tablesample()) {
|
|
num_output_rows =
|
|
num_output_rows *
|
|
(table->pos_in_table_list->get_sampling_percentage() / 100);
|
|
}
|
|
}
|
|
|
|
assert(num_output_rows >= 0);
|
|
|
|
rows += num_evaluations * min(limit, num_output_rows);
|
|
return true; // Done with this subtree.
|
|
}
|
|
|
|
switch (subpath->type) {
|
|
case AccessPath::LIMIT_OFFSET: {
|
|
const auto ¶m = subpath->limit_offset();
|
|
rows += EstimateRowAccesses(
|
|
param.child, num_evaluations,
|
|
min(limit, static_cast<double>(param.limit)));
|
|
return true;
|
|
}
|
|
case AccessPath::AGGREGATE: {
|
|
// Assume that aggregation needs to read the entire input,
|
|
// regardless of limit. This might be too pessimistic for explicitly
|
|
// grouped queries, but let's be conservative for now.
|
|
rows += EstimateRowAccesses(subpath->aggregate().child,
|
|
num_evaluations, kNoLimit);
|
|
return true;
|
|
}
|
|
case AccessPath::TEMPTABLE_AGGREGATE: {
|
|
// Temptable aggregation needs to read the entire input.
|
|
rows += EstimateRowAccesses(
|
|
subpath->temptable_aggregate().subquery_path, num_evaluations,
|
|
kNoLimit);
|
|
return true;
|
|
}
|
|
case AccessPath::MATERIALIZE: {
|
|
// Materialize once per query or once per evaluation.
|
|
const double num_materializations =
|
|
subpath->materialize().param->rematerialize ? num_evaluations
|
|
: 1.0;
|
|
for (const MaterializePathParameters::Operand &operand :
|
|
subpath->materialize().param->m_operands) {
|
|
rows += EstimateRowAccesses(operand.subquery_path,
|
|
num_materializations, kNoLimit);
|
|
}
|
|
return true;
|
|
}
|
|
case AccessPath::APPEND: {
|
|
// UNION ALL can stop reading from children once the limit is
|
|
// reached.
|
|
double limit_on_next_child = limit;
|
|
for (AppendPathParameters child : *subpath->append().children) {
|
|
if (limit_on_next_child <= 0) break;
|
|
rows += EstimateRowAccesses(child.path, num_evaluations,
|
|
limit_on_next_child);
|
|
limit_on_next_child -= child.path->num_output_rows();
|
|
}
|
|
return true;
|
|
}
|
|
case AccessPath::NESTED_LOOP_JOIN: {
|
|
rows += EstimateRowAccessesInNestedLoopJoin(
|
|
subpath, subpath->nested_loop_join().outer,
|
|
subpath->nested_loop_join().inner, num_evaluations, limit);
|
|
return true;
|
|
}
|
|
case AccessPath::NESTED_LOOP_SEMIJOIN_WITH_DUPLICATE_REMOVAL: {
|
|
rows += EstimateRowAccessesInNestedLoopJoin(
|
|
subpath,
|
|
subpath->nested_loop_semijoin_with_duplicate_removal().outer,
|
|
subpath->nested_loop_semijoin_with_duplicate_removal().inner,
|
|
num_evaluations, limit);
|
|
return true;
|
|
}
|
|
case AccessPath::BKA_JOIN: {
|
|
// BKA join reads each side once.
|
|
const auto ¶m = subpath->bka_join();
|
|
rows += EstimateRowAccesses(param.outer, num_evaluations, kNoLimit);
|
|
rows += EstimateRowAccesses(param.inner, num_evaluations, kNoLimit);
|
|
return true;
|
|
}
|
|
case AccessPath::HASH_JOIN: {
|
|
// Hash join reads each side once. If there is a LIMIT clause, it
|
|
// might not need to read all rows from the outer table.
|
|
const auto ¶m = subpath->hash_join();
|
|
rows += EstimateRowAccesses(
|
|
param.outer, num_evaluations,
|
|
GetRowsNeededFromOuterTable(subpath, param.outer, limit));
|
|
rows += EstimateRowAccesses(param.inner, num_evaluations, kNoLimit);
|
|
// Subqueries in the non-equijoin conditions may access rows.
|
|
for (Item *item : param.join_predicate->expr->join_conditions) {
|
|
rows += EstimateRowAccessesInItem(
|
|
item, num_evaluations * subpath->num_output_rows());
|
|
}
|
|
return true;
|
|
}
|
|
case AccessPath::FILTER: {
|
|
// Filters may access rows in subqueries. Count them.
|
|
const auto ¶m = subpath->filter();
|
|
const double input_rows = param.child->num_output_rows();
|
|
const double output_rows = subpath->num_output_rows();
|
|
const double rows_needed_from_child =
|
|
(input_rows > 0 && output_rows > 0)
|
|
? limit / (output_rows / input_rows)
|
|
: kNoLimit;
|
|
rows += EstimateRowAccessesInItem(
|
|
param.condition,
|
|
num_evaluations * min(param.child->num_output_rows(),
|
|
rows_needed_from_child));
|
|
rows += EstimateRowAccesses(param.child, num_evaluations,
|
|
rows_needed_from_child);
|
|
return true;
|
|
}
|
|
case AccessPath::SORT: {
|
|
// SORT needs to read the entire input, regardless of LIMIT.
|
|
rows += EstimateRowAccesses(subpath->sort().child, num_evaluations,
|
|
kNoLimit);
|
|
return true;
|
|
}
|
|
default:
|
|
return false; // Keep traversing.
|
|
}
|
|
});
|
|
|
|
return rows;
|
|
}
|
|
|
|
bool IsHashEquijoinCondition(const Item_eq_base *item, table_map left_side,
|
|
table_map right_side) {
|
|
// We are not able to create hash join conditions from row values consisting
|
|
// of multiple columns, so let them be added as extra conditions instead.
|
|
if (item->get_comparator()->get_child_comparator_count() > 1) {
|
|
return false;
|
|
}
|
|
|
|
table_map left_arg_tables = item->get_arg(0)->used_tables();
|
|
table_map right_arg_tables = item->get_arg(1)->used_tables();
|
|
|
|
// The equality is commutative. If the left side of the equality doesn't
|
|
// reference any table on the left side of the join, swap left and right to
|
|
// see if it's satisfied the other way around.
|
|
if (!Overlaps(left_arg_tables, left_side)) {
|
|
std::swap(left_arg_tables, right_arg_tables);
|
|
}
|
|
|
|
// One side of the equality should reference tables on one side of the join,
|
|
// and the other side of the equality should reference the other side of the
|
|
// join.
|
|
return Overlaps(left_arg_tables, left_side) &&
|
|
!Overlaps(left_arg_tables, right_side) &&
|
|
Overlaps(right_arg_tables, right_side) &&
|
|
!Overlaps(right_arg_tables, left_side);
|
|
}
|