Skip to content

Commit 5b10c71

Browse files
committed
WL#14672: Enable the hypergraph optimizer for UPDATE [8/8, hash join]
Enable use of hash join for UPDATE statements when using the hypergraph optimizer. With hash joins, the row IDs used for delayed update cannot be retrieved from the underlying scan, because the hash join iterator does not guarantee that the underlying scans are positioned on the correct row. Instead, the hash join iterator is instructed to store row IDs of the underlying tables in the join buffer. The existing execution code assumes that UPDATE uses nested loop joins only, and enables semi-consistent reads on the outermost table when the isolation level is read committed or lower. Hash joins are not yet prepared for doing semi-consistent reads, so the patch disables semi-consistent reads when the outer table is involved in a hash join. Change-Id: I8e712cca62f3e9c3beee5c93ddbd84ebcc61aeb1
1 parent e33434a commit 5b10c71

File tree

9 files changed

+124
-51
lines changed

9 files changed

+124
-51
lines changed

mysql-test/t/multi_update.test

+4
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ create table t2 (n int(10), d int(10));
230230
insert into t1 values(1,1),(3,2);
231231
insert into t2 values(1,10),(1,20);
232232
UPDATE t1,t2 SET t1.d=t2.d,t2.d=30 WHERE t1.n=t2.n;
233+
# It is unspecified which order the assignments are performed in, and
234+
# in which order the rows from t2 are read, so for n=1 the value of d
235+
# can end up as 10, 20 or 30, depending on the plan chosen.
236+
--replace_result 20 10 30 10
233237
select * from t1;
234238
select * from t2;
235239
UPDATE t1 a ,t2 b SET a.d=b.d,b.d=30 WHERE a.n=b.n;

sql/iterators/delete_rows_iterator.h

+9-6
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,15 @@ class DeleteRowsIterator final : public RowIterator {
6060
table_map m_tables_to_delete_from;
6161
/// The tables to delete from immediately while scanning the join result.
6262
table_map m_immediate_tables;
63-
/// The target tables whose row IDs are stored in the hash join buffer.
64-
/// This means all target tables that are below a hash join path.
65-
/// Such tables will already have the row ID available in handler::ref, and
66-
/// calling handler::position() will put an incorrect row ID (most likely the
67-
/// last row read from the table) into handler::ref.
68-
table_map m_tables_with_rowid_in_hash_join_buffer;
63+
/// All the tables that are part of a hash join. We use this map to find out
64+
/// how to get the row ID from a table when buffering row IDs for delayed
65+
/// delete. For those tables that are part of a hash join, the row ID will
66+
/// already be available in handler::ref, and calling handler::position() will
67+
/// overwrite it with an incorrect row ID (most likely the last row read from
68+
/// the table). For those that are not part of a hash join,
69+
/// handler::position() must be called to get the current row ID from the
70+
/// underlying scan.
71+
table_map m_hash_join_tables;
6972
/// The target tables that live in transactional storage engines.
7073
table_map m_transactional_tables{0};
7174
/// The target tables that have before delete triggers.

sql/iterators/update_rows_iterator.h

+13-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
#include "my_alloc.h"
3030
#include "my_base.h"
31+
#include "my_table_map.h"
3132
#include "sql/iterators/row_iterator.h"
3233
#include "sql/sql_list.h"
3334

@@ -50,8 +51,9 @@ class UpdateRowsIterator final : public RowIterator {
5051
List<TABLE> unupdated_check_opt_tables,
5152
COPY_INFO **update_operations,
5253
mem_root_deque<Item *> **fields_for_table,
53-
mem_root_deque<Item *> **values_for_table);
54-
~UpdateRowsIterator() override = default;
54+
mem_root_deque<Item *> **values_for_table,
55+
table_map tables_with_rowid_in_buffer);
56+
~UpdateRowsIterator() override;
5557
bool Init() override;
5658
int Read() override;
5759
void StartPSIBatchMode() override { m_source->StartPSIBatchMode(); }
@@ -95,6 +97,15 @@ class UpdateRowsIterator final : public RowIterator {
9597
ha_rows m_found_rows{0};
9698
/// The number of rows actually updated.
9799
ha_rows m_updated_rows{0};
100+
/// All the tables that are part of a hash join. We use this map to find out
101+
/// how to get the row ID from a table when buffering row IDs for delayed
102+
/// update. For those tables that are part of a hash join, the row ID will
103+
/// already be available in handler::ref, and calling handler::position() will
104+
/// overwrite it with an incorrect row ID (most likely the last row read from
105+
/// the table). For those that are not part of a hash join,
106+
/// handler::position() must be called to get the current row ID from the
107+
/// underlying scan.
108+
table_map m_hash_join_tables;
98109

99110
/// Perform all the immediate updates for the current row returned by the
100111
/// join, and buffer row IDs for the non-immediate tables.

sql/join_optimizer/access_path.cc

+10-10
Original file line numberDiff line numberDiff line change
@@ -1344,16 +1344,16 @@ void ExpandFilterAccessPaths(THD *thd, AccessPath *path_arg, const JOIN *join,
13441344
});
13451345
}
13461346

1347-
table_map GetTablesWithRowIDsInHashJoin(AccessPath *path) {
1347+
table_map GetHashJoinTables(AccessPath *path) {
13481348
table_map tables = 0;
1349-
WalkAccessPaths(path, /*join=*/nullptr,
1350-
WalkAccessPathPolicy::STOP_AT_MATERIALIZATION,
1351-
[&tables](AccessPath *subpath, const JOIN *) {
1352-
if (subpath->type == AccessPath::HASH_JOIN &&
1353-
subpath->hash_join().store_rowids) {
1354-
tables |= subpath->hash_join().tables_to_get_rowid_for;
1355-
}
1356-
return false;
1357-
});
1349+
WalkAccessPaths(
1350+
path, /*join=*/nullptr, WalkAccessPathPolicy::STOP_AT_MATERIALIZATION,
1351+
[&tables](AccessPath *subpath, const JOIN *) {
1352+
if (subpath->type == AccessPath::HASH_JOIN) {
1353+
tables |= GetUsedTableMap(subpath, /*include_pruned_tables=*/true);
1354+
return true;
1355+
}
1356+
return false;
1357+
});
13581358
return tables;
13591359
}

sql/join_optimizer/access_path.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -1785,7 +1785,7 @@ void ExpandSingleFilterAccessPath(THD *thd, AccessPath *path, const JOIN *join,
17851785
const Mem_root_array<Predicate> &predicates,
17861786
unsigned num_where_predicates);
17871787

1788-
/// Returns the tables that have stored row IDs in the hash join result.
1789-
table_map GetTablesWithRowIDsInHashJoin(AccessPath *path);
1788+
/// Returns the tables that are part of a hash join.
1789+
table_map GetHashJoinTables(AccessPath *path);
17901790

17911791
#endif // SQL_JOIN_OPTIMIZER_ACCESS_PATH_H

sql/join_optimizer/join_optimizer.cc

-6
Original file line numberDiff line numberDiff line change
@@ -3028,12 +3028,6 @@ void CostingReceiver::ProposeHashJoin(
30283028
// to update or delete. The same applies to rows from the outer side, if the
30293029
// hash join spills to disk, so we need to store row IDs for both sides.
30303030
if (Overlaps(m_update_delete_target_nodes, left | right)) {
3031-
if (m_thd->lex->sql_command == SQLCOM_UPDATE_MULTI ||
3032-
m_thd->lex->sql_command == SQLCOM_UPDATE) {
3033-
// TODO(khatlen): Consider enabling hash join for UPDATE too. Must
3034-
// probably disable semi-consistent reads in that case.
3035-
return;
3036-
}
30373031
FindTablesToGetRowidFor(&join_path);
30383032
}
30393033

sql/sql_delete.cc

+7-5
Original file line numberDiff line numberDiff line change
@@ -921,10 +921,9 @@ DeleteRowsIterator::DeleteRowsIterator(
921921
m_tables_to_delete_from(tables_to_delete_from),
922922
m_immediate_tables(immediate_tables),
923923
// The old optimizer does not use hash join in DELETE statements.
924-
m_tables_with_rowid_in_hash_join_buffer(
925-
thd->lex->using_hypergraph_optimizer
926-
? GetTablesWithRowIDsInHashJoin(join->root_access_path())
927-
: 0),
924+
m_hash_join_tables(thd->lex->using_hypergraph_optimizer
925+
? GetHashJoinTables(join->root_access_path())
926+
: 0),
928927
m_tempfiles(thd->mem_root),
929928
m_delayed_tables(thd->mem_root) {
930929
for (const TABLE_LIST *tr = join->query_block->leaf_tables; tr != nullptr;
@@ -1076,7 +1075,10 @@ bool DeleteRowsIterator::DoImmediateDeletesAndBufferRowIds() {
10761075
// Check if using outer join and no row found, or row is already deleted
10771076
if (table->has_null_row() || table->has_deleted_row()) continue;
10781077

1079-
if (!Overlaps(map, m_tables_with_rowid_in_hash_join_buffer)) {
1078+
// Hash joins have already copied the row ID from the join buffer into
1079+
// table->file->ref. Nested loop joins have not, so we call position() to
1080+
// get the row ID from the handler.
1081+
if (!Overlaps(map, m_hash_join_tables)) {
10801082
table->file->position(table->record[0]);
10811083
}
10821084

sql/sql_update.cc

+41-20
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
#include "sql/iterators/timing_iterator.h"
6868
#include "sql/iterators/update_rows_iterator.h"
6969
#include "sql/join_optimizer/access_path.h"
70+
#include "sql/join_optimizer/bit_utils.h"
7071
#include "sql/join_optimizer/walk_access_paths.h"
7172
#include "sql/key.h" // is_key_used
7273
#include "sql/key_spec.h"
@@ -2114,8 +2115,15 @@ static bool AddRowIdAsTempTableField(THD *thd, TABLE *table,
21142115
/// @param table The table to get a row ID from.
21152116
/// @param tmp_table The temporary table in which to store the row ID.
21162117
/// @param field_num The field of tmp_table in which to store the row ID.
2117-
static void StoreRowId(TABLE *table, TABLE *tmp_table, int field_num) {
2118-
table->file->position(table->record[0]);
2118+
/// @param hash_join_tables A map of all tables that are part of a hash join.
2119+
static void StoreRowId(TABLE *table, TABLE *tmp_table, int field_num,
2120+
table_map hash_join_tables) {
2121+
// Hash joins have already copied the row ID from the join buffer into
2122+
// table->file->ref. Nested loop joins have not, so we call position() to get
2123+
// the row ID from the handler.
2124+
if (!Overlaps(hash_join_tables, table->pos_in_table_list->map())) {
2125+
table->file->position(table->record[0]);
2126+
}
21192127
tmp_table->visible_field_ptr()[field_num]->store(
21202128
pointer_cast<const char *>(table->file->ref), table->file->ref_length,
21212129
&my_charset_bin);
@@ -2252,14 +2260,6 @@ bool Query_result_update::optimize() {
22522260
if (thd->lex->is_ignore()) table->file->ha_extra(HA_EXTRA_IGNORE_DUP_KEY);
22532261
if (table == main_table) // First table in join
22542262
{
2255-
// As it's the first table in the join, and we're doing a nested loop
2256-
// join thanks to SELECT_NO_JOIN_CACHE, the table is the left argument
2257-
// of that NL join; thus, we can ask for semi-consistent read.
2258-
// It's a bit early to ask for it here, because we're before
2259-
// rnd_init/index_init; but cannot do it later, as we soon
2260-
// hand control over to iterators.
2261-
table->file->try_semi_consistent_read(true);
2262-
22632263
if (table == table_to_update) {
22642264
assert(bitmap_is_clear_all(&table->tmp_set));
22652265
table->mark_columns_needed_for_update(
@@ -2397,10 +2397,6 @@ void Query_result_update::cleanup(THD *thd) {
23972397
}
23982398
tmp_table_param = nullptr;
23992399
thd->check_for_truncated_fields = CHECK_FIELD_IGNORE; // Restore this setting
2400-
2401-
if (main_table != nullptr && main_table->is_created()) {
2402-
main_table->file->try_semi_consistent_read(false);
2403-
}
24042400
main_table = nullptr;
24052401
// Reset state and statistics members:
24062402
unupdated_check_opt_tables.clear();
@@ -2533,9 +2529,9 @@ bool UpdateRowsIterator::DoImmediateUpdatesAndBufferRowIds(
25332529
rowids of tables used in the CHECK OPTION condition.
25342530
*/
25352531
int field_num = 0;
2536-
StoreRowId(table, tmp_table, field_num++);
2532+
StoreRowId(table, tmp_table, field_num++, m_hash_join_tables);
25372533
for (TABLE &tbl : m_unupdated_check_opt_tables) {
2538-
StoreRowId(&tbl, tmp_table, field_num++);
2534+
StoreRowId(&tbl, tmp_table, field_num++, m_hash_join_tables);
25392535
}
25402536

25412537
/*
@@ -2835,7 +2831,26 @@ bool UpdateRowsIterator::DoDelayedUpdates(bool *trans_safe,
28352831
return true;
28362832
}
28372833

2838-
bool UpdateRowsIterator::Init() { return m_source->Init(); }
2834+
bool UpdateRowsIterator::Init() {
2835+
if (m_source->Init()) return true;
2836+
2837+
if (m_outermost_table != nullptr &&
2838+
!Overlaps(m_hash_join_tables,
2839+
m_outermost_table->pos_in_table_list->map())) {
2840+
// As it's the first table in the join, and we're doing a nested loop join,
2841+
// the table is the left argument of that nested loop join; thus, we can ask
2842+
// for semi-consistent read.
2843+
m_outermost_table->file->try_semi_consistent_read(true);
2844+
}
2845+
2846+
return false;
2847+
}
2848+
2849+
UpdateRowsIterator::~UpdateRowsIterator() {
2850+
if (m_outermost_table != nullptr && m_outermost_table->is_created()) {
2851+
m_outermost_table->file->try_semi_consistent_read(false);
2852+
}
2853+
}
28392854

28402855
int UpdateRowsIterator::Read() {
28412856
bool local_error = false;
@@ -3021,7 +3036,8 @@ UpdateRowsIterator::UpdateRowsIterator(
30213036
TABLE **tmp_tables, Copy_field *copy_fields,
30223037
List<TABLE> unupdated_check_opt_tables, COPY_INFO **update_operations,
30233038
mem_root_deque<Item *> **fields_for_table,
3024-
mem_root_deque<Item *> **values_for_table)
3039+
mem_root_deque<Item *> **values_for_table,
3040+
table_map tables_with_rowid_in_buffer)
30253041
: RowIterator(thd),
30263042
m_source(std::move(source)),
30273043
m_outermost_table(outermost_table),
@@ -3032,7 +3048,8 @@ UpdateRowsIterator::UpdateRowsIterator(
30323048
m_unupdated_check_opt_tables(unupdated_check_opt_tables),
30333049
m_update_operations(update_operations),
30343050
m_fields_for_table(fields_for_table),
3035-
m_values_for_table(values_for_table) {}
3051+
m_values_for_table(values_for_table),
3052+
m_hash_join_tables(tables_with_rowid_in_buffer) {}
30363053

30373054
unique_ptr_destroy_only<RowIterator> CreateUpdateRowsIterator(
30383055
THD *thd, MEM_ROOT *mem_root, JOIN *join,
@@ -3046,5 +3063,9 @@ unique_ptr_destroy_only<RowIterator> Query_result_update::create_iterator(
30463063
return NewIterator<UpdateRowsIterator>(
30473064
thd, mem_root, std::move(source), main_table, table_to_update,
30483065
update_tables, tmp_tables, copy_field, unupdated_check_opt_tables,
3049-
update_operations, fields_for_table, values_for_table);
3066+
update_operations, fields_for_table, values_for_table,
3067+
// The old optimizer does not use hash join in UPDATE statements.
3068+
thd->lex->using_hypergraph_optimizer
3069+
? GetHashJoinTables(unit->root_access_path())
3070+
: 0);
30503071
}

unittest/gunit/hypergraph_optimizer-t.cc

+38
Original file line numberDiff line numberDiff line change
@@ -4695,6 +4695,44 @@ TEST_F(HypergraphOptimizerTest, UpdatePreferImmediate) {
46954695
EXPECT_STREQ("t2", nested_loop_join.inner->eq_ref().table->alias);
46964696
}
46974697

4698+
TEST_F(HypergraphOptimizerTest, UpdateHashJoin) {
4699+
Query_block *query_block =
4700+
ParseAndResolve("UPDATE t1, t2 SET t1.x = 1, t2.x = 2 WHERE t1.y = t2.y",
4701+
/*nullable=*/false);
4702+
ASSERT_NE(nullptr, query_block);
4703+
4704+
// Size the tables so that a hash join is preferable to a nested loop join.
4705+
Fake_TABLE *t1 = m_fake_tables["t1"];
4706+
t1->file->stats.records = 100000;
4707+
t1->file->stats.data_file_length = 1e6;
4708+
Fake_TABLE *t2 = m_fake_tables["t2"];
4709+
t2->file->stats.records = 10000;
4710+
t2->file->stats.data_file_length = 1e5;
4711+
4712+
string trace;
4713+
AccessPath *root = FindBestQueryPlan(m_thd, query_block, &trace);
4714+
SCOPED_TRACE(trace); // Prints out the trace on failure.
4715+
ASSERT_NE(nullptr, root);
4716+
// Prints out the query plan on failure.
4717+
SCOPED_TRACE(PrintQueryPlan(0, root, query_block->join,
4718+
/*is_root_of_join=*/true));
4719+
4720+
ASSERT_EQ(AccessPath::UPDATE_ROWS, root->type);
4721+
// Both tables are updated.
4722+
EXPECT_EQ(t1->pos_in_table_list->map() | t2->pos_in_table_list->map(),
4723+
root->update_rows().tables_to_update);
4724+
// No immediate update with hash join.
4725+
EXPECT_EQ(0, root->update_rows().immediate_tables);
4726+
4727+
// Expect a hash join with the smaller table (t2) on the inner side.
4728+
ASSERT_EQ(AccessPath::HASH_JOIN, root->update_rows().child->type);
4729+
const auto &hash_join = root->update_rows().child->hash_join();
4730+
ASSERT_EQ(AccessPath::TABLE_SCAN, hash_join.outer->type);
4731+
EXPECT_EQ(t1, hash_join.outer->table_scan().table);
4732+
ASSERT_EQ(AccessPath::TABLE_SCAN, hash_join.inner->type);
4733+
EXPECT_EQ(t2, hash_join.inner->table_scan().table);
4734+
}
4735+
46984736
// An alias for better naming.
46994737
using HypergraphSecondaryEngineTest = HypergraphOptimizerTest;
47004738

0 commit comments

Comments
 (0)