【源码解读】CockroachDB Hash Join算子逻辑解读

关于CockroachDB

CockroachDB 是一个分布式关系型数据库,主要设计目标是可扩展,强一致和高可靠 。CockroachDB 旨在无人为干预情况下,以极短的中断时间容忍磁盘、主机、机架甚至整个数据中心的故障 。 CockroachDB 采用完全去中心化架构,集群中各个节点的地位完全对等,同时所有功能封装在一个二进制文件中,可以做到尽量不依赖配置文件直接部署。

关于Hash Join

所谓 Hash Join 就是在表的 join 时候选择一张表作为 buildSide 表来构造哈希表,另外一张表作为 probeSide 表,然后对 probeSide 表的每一行数据都去这个哈希表中查找是否有匹配的数据(其中 buildSide 表通常是小表,probeSide 表通常是较大的表)。

源码解读

本文解析源码位于cockroach/pkg/sql/opt/xform/coster.go 

cockroach/coster.go at master · cockroachdb/cockroach · GitHubhttps://github.com/cockroachdb/cockroach/blob/master/pkg/sql/opt/xform/coster.gocomputeHashJoinCost函数,大致位于779-837行

算子逻辑

Hash Join一般用于连接中,左右表数据量较大并且左表行数明显大于右表。连接过程中 Hash Join 为小表建立 hash table,读取大表的每一行,计算关联列的 hash key,通过 hash map 映射到对应元组。

代价计算涉及常数因子及辅助函数

常数因子

  • hugeCost:表示的是通常大于开销模型估计结果的一个代价值,hugeCost=1e100
  • cpuCostFactor:从 PostgreSQL 中的 DEFAULT_CPU_TUPLE_COST 借鉴而来,cpuCostFactor=0.01

辅助函数

computeFiltersCost:返回执行过滤器的设置和每行成本。这个函数的调用者应该添加 setupCost,并将 perRowCost 乘以预期要过滤的行数。

setupCost += cpuCostFactor
perRowCost += cpuCostFactor

此处是为了添加一个基本的 perRowCost, 这样调用者就不需要有自己的每行基本成本

算子开销计算公式及流程

计算公式

Cost = 1.25 * leftRowCount + 1.75 * rightRowCount * cpuCostFactor + filterSetup + rowsProcessed * filterPerRow

其中 1.25 是左边计算 hashkey 的开销,1.75 是建立 hashtable 的开销

具体计算流程

  1. 首先判断 hint 是否禁止 HashJoin,禁止则返回 hugeCost,否则进入下一 步。
  2. 得到leftRowCount 和 rightRowCount。
  3. 读表代价: cost := memo.Cost(1.25*leftRowCount+1.75*rightRowCount) * cpuCostFactor。
  4. 计算 filter cost。FiltersItem 筛选由包含 Select 或 Join 操作符的行,只有当所有条件都为 true 时,才过滤一行。如果该集合为空,则它永远不会过滤行;func ExtractJoinEqualityColumns 提取连接相等列,返回一对列(一个来自左边,一个来自右边),它们在连接中被约束为相等(并且具有等效类型)。
  5. eqMap := util.FastIntMap{}定义相等过滤器。FastIntMap 当键和值都很小 时,它更有效,它可以通过值传递。
  6. 估算重新计算的代价:cost += filterSetup。
  7. 计算两表连接后最终需要总行数的代价 cost+= memo.Cost(rowsProcessed) * filterPerRow,最后返回 cost 值。

实例测试

一般测试

SQL语句

SELECT * 
FROM customer 
JOIN nation 
ON (c_nationkey = n_nationkey) ;

表的情况

InputLeft Table: customerLeft row count=150000
Right Table: nationRight row count=25

实验结果

读表代价filter cost连接后的最终代价
leftRowCount = 150000filterPerRow = 0.01cost+=memo.Cost(rowsProcessed)*filterPerRow
rightRowCount = 25filterSetup = 0.01
cost = 1875.4375cost =1875.4475cost = 3375.4475
Output: cost=3375.4475

Hash Join、Merge Join、Lookup Join 实验对比:

SQL语句

SELECT * 
FROM customer 
JOIN nation 
ON (c_nationkey = n_nationkey)
WHERE c_custkey > 100000;

表的情况

InputLeft Table: customerLeft row count=150000
Right Table: nationRight row count=25

实验结果

Hash Join ComputeCostMerge Join ComputeCostLookup Join ComputeCost
cost=1161.2729+0.01+6 0914... = 62075...cost=1032.0948+0.01+780 99... = 79131...cost=363914...

省略号...指小数点后的值

实验结论

  • 表中 Lookup Join ComputeCost 明显远大于 Hash Join 和 Merge Join;
  • 算子方法返回 cost:computeHashJoinCost(cost ≈ 1161)> computeMergeJoinCost(cost ≈ 1032),这是因为 Merge Join 在 join 的列无序的情 况下最后计算所得的 ComputeCost 还需要加上 sort 的 cost,使得 Merge Join 的 cost 大于 Hash Join;
  • 实际执行计划情况对比:
Hash Join 执行计划

Merge Join 执行计划
  • 从上图可以看出 Merge Join 需要对无序列排序;
  • 在实际执行计划的选择中,Merge Join 需要 join 的列排序 cost 不高的情 况,当列无序并且左表行数明显大于右表的时候执行计划会倾向选择 Hash Join。

附录:

func (c *coster) computeHashJoinCost(join memo.RelExpr) memo.Cost {
	if join.Private().(*memo.JoinPrivate).Flags.Has(memo.DisallowHashJoinStoreRight) {
		return hugeCost
	}
	leftRowCount := join.Child(0).(memo.RelExpr).Relational().Stats.RowCount
	rightRowCount := join.Child(1).(memo.RelExpr).Relational().Stats.RowCount
	if (join.Op() == opt.SemiJoinOp || join.Op() == opt.AntiJoinOp) && leftRowCount < rightRowCount {
		// If we have a semi or an anti join, during the execbuilding we choose
		// the relation with smaller cardinality to be on the right side, so we
		// need to swap row counts accordingly.
		// TODO(raduberinde): we might also need to look at memo.JoinFlags when
		// choosing a side.
		leftRowCount, rightRowCount = rightRowCount, leftRowCount
	}

	// A hash join must process every row from both tables once.
	//
	// We add some factors to account for the hashtable build and lookups. The
	// right side is the one stored in the hashtable, so we use a larger factor
	// for that side. This ensures that a join with the smaller right side is
	// preferred to the symmetric join.
	cost := memo.Cost(1.25*leftRowCount+1.75*rightRowCount) * cpuCostFactor

	// Add a cost for buffering rows that takes into account increased memory
	// pressure and the possibility of spilling to disk.
	cost += c.rowBufferCost(rightRowCount)

	// Compute filter cost. Fetch the equality columns so they can be
	// ignored later.
	on := join.Child(2).(*memo.FiltersExpr)
	leftEq, rightEq := memo.ExtractJoinEqualityColumns(
		join.Child(0).(memo.RelExpr).Relational().OutputCols,
		join.Child(1).(memo.RelExpr).Relational().OutputCols,
		*on,
	)
	// Generate a quick way to lookup if two columns are join equality
	// columns. We add in both directions because we don't know which way
	// the equality filters will be defined.
	eqMap := util.FastIntMap{}
	for i := range leftEq {
		left := int(leftEq[i])
		right := int(rightEq[i])
		eqMap.Set(left, right)
		eqMap.Set(right, left)
	}
	filterSetup, filterPerRow := c.computeFiltersCost(*on, eqMap)
	cost += filterSetup

	// Add the CPU cost of emitting the rows.
	rowsProcessed, ok := c.mem.RowsProcessed(join)
	if !ok {
		// This can happen as part of testing. In this case just return the number
		// of rows.
		rowsProcessed = join.Relational().Stats.RowCount
	}
	cost += memo.Cost(rowsProcessed) * filterPerRow

	return cost
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随处可见的打字员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值