Join (Inner Join)
Join 算法
Specifies JOIN algorithm.
Possible values:
hash — Hash join algorithm is used.
partial_merge — Sort-merge algorithm is used.
prefer_partial_merge — ClickHouse always tries to use merge join if possible.
auto — ClickHouse tries to change hash join to merge join on the fly to avoid out of memory.
Default value: hash
.
QueryPlan
MacBook.local :) explain select * from t1 join t2 on t1.x = t2.x1
EXPLAIN
SELECT *
FROM t1
INNER JOIN t2 ON t1.x = t2.x1
Query id: b7068d63-dc61-4389-a12c-ea8016e32bc0
┌─explain──────────────────────────────────────────────────────────────────────────────────────┐
│ Expression ((Projection + Before ORDER BY)) │
│ Join (JOIN) │
│ Expression (Before JOIN) │
│ SettingQuotaAndLimits (Set limits and quota after reading from storage) │
│ ReadFromMergeTree │
│ Expression ((Joined actions + (Rename joined columns + (Projection + Before ORDER BY)))) │
│ SettingQuotaAndLimits (Set limits and quota after reading from storage) │
│ ReadFromMergeTree │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
8 rows in set. Elapsed: 0.003 sec.
查看 Pipeline
MacBook.local :) explain pipeline select * from t1 join t2 on t1.x = t2.x1
EXPLAIN PIPELINE
SELECT *
FROM t1
INNER JOIN t2 ON t1.x = t2.x1
Query id: c054b9e0-d82c-4bc4-a22d-64dd9f556cf0
┌─explain──────────────────────────┐
│ (Expression) │
│ ExpressionTransform │
│ (Join) │
│ JoiningTransform 2 → 1 │
│ FillingRightJoinSide │
│ (Expression) │
│ ExpressionTransform │
│ (SettingQuotaAndLimits) │
│ (ReadFromMergeTree) │
│ MergeTreeInOrder 0 → 1 │
│ (Expression) │
│ ExpressionTransform │
│ (SettingQuotaAndLimits) │
│ (ReadFromMergeTree) │
│ MergeTreeInOrder 0 → 1 │
└──────────────────────────────────┘
15 rows in set. Elapsed: 0.005 sec.
MacBook.local :)
HashJoin 本质
Table1 INNER JOIN Table2
- Table2 被转换成一个HashTable1.
- Table1 去HashTable1中查找,如果发现key值匹配, 就将相关列的行的值copy到结果级。如果右表有多个行匹配,那么就copy多个对应行的值到对应列。
举例
Table1 Table2
x y x1 y1
-------- ----------
1 'a' 1 'c'
2 'b' 1 'd'
-------------------
Exeute:
select * from Table1 Inner Join Table2 on Table1.x = Table2.x1
Result:
x y x1 y1
--------------------
1 'a' 1 'c'
1 'a' 1 'd'
--------------------
HashJoin 的关键
遍历左表key column的同时,遇到多个匹配需要duplicate,或者filter。
ClickHouse 中 HashJoin的实现
│ JoiningTransform 2 → 1 │
│ FillingRightJoinSide
FillingRightJoinSideTransform
本质:使用右表构建HashTable.
HashTable结构示意图
HashTable 可扩容.
[14789223245, [1,3,4,[Column1Ptr, Column2Ptr]] ] --HashCell
...
[79137398732, [2,[Column3Ptr]] --HashCell
...
[76349271791, [5,[Column5Ptr]] --HashCell
...
[hash值, [row_number, [指向相关列的指针]]] //
HashTable 在ClickHouse的实现
HashJoin使用的HashTable与LowCardinality结构中使用的一样。HashJoin结构体中创建HashTable,HashCell中存储StringRef与RowsRef,并且保存当前key的hash值。
(细节)Join 构建HashMap debug 一览.
(细节)构建map_
'b' 是右表第一个元素.
为'b'在hashTable中找位置。 b被放在HashTable中13的位置.
构建一个holder.key.data
构建一个在buf对应的内存位置构建一个HashCell.
更新buf中的value(*this)的saved_hash值.
接着更新 'a'
(细节)RowRef 数据结构
HashTable debug细腻.
通过右表key的hash值,我们可以从RowRef中的Block快速通过Column的随机访问快速拿到任意列的值,可以拿到所有列的相关索引的值。
struct RowRef
{
using SizeT = uint32_t; /// Do not use size_t cause of memory economy
const Block * block = nullptr;
SizeT row_num = 0;
RowRef() {}
RowRef(const Block * block_, size_t row_num_) : block(block_), row_num(row_num_) {}
};
第二个'a'写入,会在对应的RowsRef中增加row_number。
if (emplace_result.isInserted())
new (&emplace_result.getMapped()) typename Map::mapped_type(stored_block, i);
else
{
/// The first element of the list is stored in the value of the hash table, the rest in the pool.
emplace_result.getMapped().insert({stored_block, i}, pool);
}
可以看到创建HashTable,包含了其他右表的列的信息。也就是说在后续查找某列中i row的值,可以直接随机查找。
JoiningTransform::work().
此时需要FillingRightJoinSide构建的Join object。
读取参与Join的左侧的Chunk (columns)。
构建的row_filter是HashJoin的关键。row_filter是 filter 左侧的表还是右侧的表?
开始HashJoin时,需要使用Join结构中的maps_ ,
Block使用的是左侧列组成的Block.
右边表需要被加到最终结果block中的column。 block_with_columns_to_add 变量
根据block_with_columns_to_add 构建added_columns。
SwitchJoinRightColumns 最重要的函数 filter 与replicate
参与计算这两个关键的要素,只需要参与Join 左表的key column 就可以。其他的filter和replicated都是和左表的key column保持一致。 这里的列都是新建并填充的,这样保证在匹配/不匹配的情况下填充时的灵活性。
size_t rows = added_columns.rows_to_add; // rows_to_add 是 左表 column的行. 此时右表的可以被map表示.
filter = IColumn::Filter(rows, 0);// 以左表行数 设置filter. 比较合理
added_columns.offsets_to_replicate = std::make_unique<IColumn::Offsets>(rows); // 以左表行数设置 需要复制的行. 合理. 如果是左表[a,a] -> 右表[a] => 那么左表行最终为[a,a]合理。
for (auto &row: rows){
// 第一层的过滤,目前不知道哪个算子提供这个join_mask_columns。测试时为Nullptr,即所有行都不进行过滤。
bool row_acceptable = !added_columns.isRowFiltered(i);
// 如果这行row需要被保留,我们需要查看这个left_key_column的第i行在map中的位置.在hashTable中查找。 获取 类型为String 类型的key
// StringRef key(chars + offsets[row - 1], offsets[row] - offsets[row - 1] - 1);
auto find_result = row_acceptable ? key_getter.findKey(map, i, pool) : FindResult();
if (find_result.isFound()){
setUsed<need_filter>(filter, i); 设置相应filter[i]为1.
// 将当前所有能和当前左表key join上的row添加到所有左边列。
// columns[i] 开启复制模式,因为columns这里一开始都是empty.
addFoundRowAll<Map, add_missing>(mapped, added_columns, current_offset); // 通过对各个列调用此方法来完成 左表 [a] -> [a,a] 右表,让 所有的列都完成对应的复制 [a,a]。
//void insertFrom(const IColumn & src, size_t n) override {
// data.push_back(assert_cast<const Self &>(src).getData()[n]);
// }
//
}
if constexpr need_replicated){
// 当前row i 更新对offsets进行更新.这里主要是为了后面replicate时,可以采用批量的方式. memcpyxxx 一种优化方式.
(*added_columns.offsets_to_replicate)[i] = current_offset;
}
}
执行完Join算法以后。将结果存放到block
for (size_t i = 0; i < added_columns.size(); ++i)
block.insert(added_columns.moveColumn(i));
返回前的replicate 操作
- ?最后返回之前需要创建一个新的column 新建 需要replicate的列。目前不知道最后一步实现的replicate的含义是什么。
for (size_t i = 0; i < existing_columns; ++i)
block.safeGetByPosition(i).column = block.safeGetByPosition(i).column->replicate(*offsets_to_replicate);
ColumnPtr ColumnString::replicate(const Offsets & replicate_offsets) const
{
size_t col_size = size();
if (col_size != replicate_offsets.size())
throw Exception("Size of offsets doesn't match size of column.", ErrorCodes::SIZES_OF_COLUMNS_DOESNT_MATCH);
auto res = ColumnString::create();
if (0 == col_size)
return res;
Chars & res_chars = res->chars;
Offsets & res_offsets = res->offsets;
res_chars.reserve(chars.size() / col_size * replicate_offsets.back());
res_offsets.reserve(replicate_offsets.back());
Offset prev_replicate_offset = 0;
Offset prev_string_offset = 0;
Offset current_new_offset = 0;
for (size_t i = 0; i < col_size; ++i)
{
size_t size_to_replicate = replicate_offsets[i] - prev_replicate_offset;
size_t string_size = offsets[i] - prev_string_offset;
for (size_t j = 0; j < size_to_replicate; ++j)
{
current_new_offset += string_size;
res_offsets.push_back(current_new_offset);
res_chars.resize(res_chars.size() + string_size);
// 这里就是利用了 前面记录的offset进行memcpy copy的优化.
memcpySmallAllowReadWriteOverflow15(
&res_chars[res_chars.size() - string_size], &chars[prev_string_offset], string_size);
}
prev_replicate_offset = replicate_offsets[i];
prev_string_offset = offsets[i];
}
return res;
}
总结
- 计算右表key的列hashTable,并将相关联的其他右表列信息也更新在map中。
- 根据map计算左表的最终结果放到block.
- 所有的计算重点都在 SwitchJoinRightColumns<KIND, STRICTNESS>(maps_, added_columns, data->type, null_map, used_flags) 函数中。
- 将右表key的列加入到block
- 返回。
收获
- RightTable到->Join,然后不同的JoiningTransform可以自由Join,将结果写入到local变量added_columns,这样可以使join并行化。
- HashTable 作用起到了关键性的作用。
问题:
- 在BuildQueryPlan 执行FillingRightJoinSide::transformHeader()和JoiningTransform::transformHeader()的意义是什么?
- 在JoiningTransform:: joinRightColumns()执行结束之后的对 block中已有的column进行replicate的作用是什么?
- Interpreter 执行优化?
- BlockStreaming.
- QueryPipeline = pipeline.
InBlockInputStream
Block RemoteQueryExecutor::read()
{
if (!sent_query)
{
sendQuery();
if (context->getSettingsRef().skip_unavailable_shards && (0 == connections->size()))
return {};
}
while (true)
{
if (was_cancelled)
return Block();
Packet packet = connections->receivePacket();
if (auto block = processPacket(std::move(packet)))
return *block;
else if (got_duplicated_part_uuids)
return std::get<Block>(restartQueryWithoutDuplicatedUUIDs());
}
}
IBlockInputStream可以 输出 Block(data) 给调用者。以RemoteBlockInputStream为例