这篇文章首先会回顾一下整个 SQL 的执行过程,用来说明为什么要范围计算,最后从源码的角度讲解一下 析取范式 DNF (disjunctive normal form) 和 合取范式 CNF (conjunctive normal form) 是如何转化为范围区间。
TiDB 在进行表扫描前会对查询条件,也就是 Selection 算子的过滤条件化简, 转为区间扫描。可以尽早的将无关的数据过滤掉,提升整个 SQL 的执行效率。
例如:
CREATE TABLE test1 (a int primary key, b int, c int,index (b));
explain select * from test1 where b=5 or ( b>5 and (b>6 or b <8) and b<12) ;
在上面的查询中,会对查询条件进行优化,将索引搜索的返回缩小。对于上面的 where 条件中的表达式区间,最终会优化为:
b=5 or ( b>5 and (b>6 or b <8) and b<12)
=>[5,12)
我们从 explain 中也可以看到优化结果:
+-------------------------+-------+---------+-----------------------+--------------------------------------------+
|id |estRows|task |access object |operator info |
+-------------------------+-------+---------+-----------------------+--------------------------------------------+
|IndexLookUp_10 |250.00 |root | | |
|├─IndexRangeScan_8(Build)|250.00 |cop[tikv]|table:test1, index:b(b)|range:[5,12), keep order:false, stats:pseudo|
|└─TableRowIDScan_9(Probe)|250.00 |cop[tikv]|table:test1 |keep order:false, stats:pseudo |
+-------------------------+-------+---------+-----------------------+--------------------------------------------+
在正式进入探究之前,我们先来看看 TiDB 的几个优化步骤,让不了的同学也能很好的掌握整个 SQL 优化过程。
对于上面我们的 SQL:
select * from test1 where b=5 or ( b>5 and (b>6 or b <8) and b<12) ;
首先会生成执行计划:
在执行完 logicalOptimize 逻辑优化之后,执行计划变为下面这样:
Selection算子被下推到了 DataSource 算子中,在 DataSource 的 pushedDownConds 中保存着下推的过滤算子:
对于我们的 pushedDownConds 展开来是一个二叉树结构:
因为索引底层是顺序排列的,所以要将这颗树转为扫描区间。
然后在执行 physicalOptimize 然后进行物理优化的时候会遍历 DataSource 算子的 possibleAccessPaths
...
for _, path := range ds.possibleAccessPaths {
if path.IsTablePath() {
continue
}
err := ds.fillIndexPath(path, ds.pushedDownConds)
if err != nil {
return nil, err
}
}
...
fillIndexPath 会调用 DetachCondAndBuildRangeForIndex 来生成扫描区间,这个函数会递归的调用如下 2 个函数:
detachDNFCondAndBuildRangeForIndex:展开 OR 条件连接也叫析取范式 DNF (disjunctive normal form),生成扫描区间或合并扫描区间;
detachCNFCondAndBuildRangeForIndex:展开 AND 条件连接也叫合取范式 CNF (conjunctive normal form),生成扫描区间或合并扫描区间;
整个执行过程如下:
上面的表达式树最终生成了这样的区间:[5,12)
。
然后 physicalOptimize 会递归所有的算子调用 findBestTask 函数,最后调用到 DataSoure 算子使用 Skyline-Pruning 索引裁剪,它会从 possibleAccessPaths 获取最优的执行计划:
func (ds *DataSource) skylinePruning(prop *property.PhysicalProperty) []*candidatePath {
candidates := make([]*candidatePath, 0, 4)
for _, path := range ds.possibleAccessPaths {
var currentCandidate *candidatePath
...
pruned := false
for i := len(candidates) - 1; i >= 0; i-- {
// 比较索引代价,判断是否进行裁剪
result := compareCandidates(candidates[i], currentCandidate)
if result == 1 {
pruned = true
break
} else if result == -1 {
candidates = append(candidates[:i], candidates[i+1:]...)
}
}
if !pruned {
candidates = append(candidates, currentCandidate)
}
}
...
return candidates
}
compareCandidates 函数会从下面三个方面进行判断一个索引的优劣:
where
条件,如果某个索引的列集合涵盖的访问条件越多,那么它在这个维度上更优。where
条件。如果某个索引的列集合涵盖的访问条件越多,则回表数量越少,那么它在这个维度上越优。例如:如果索引 idx_a
在这三个维度上都不比 idx_b
差,且有一个维度比 idx_b
好,那么 TiDB 会优先选择 idx_a
。
排除了不合适的索引之后,会根据下面的规则来选择一个代价最低的索引进行读表:
最后生成的执行计划为:PhysicalIndexLookUpReader。
在上面中我也说到了 DetachCondAndBuildRangeForIndex 会根据 where 条件来生成扫描区间。
func (d *rangeDetacher) detachDNFCondAndBuildRangeForIndex(condition *expression.ScalarFunction, newTpSlice []*types.FieldType) ([]*Range, []expression.Expression, bool, error) {
sc := d.sctx.GetSessionVars().StmtCtx
firstColumnChecker := &conditionChecker{
colUniqueID: d.cols[0].UniqueID,
shouldReserve: d.lengths[0] != types.UnspecifiedLength,
length: d.lengths[0],
}
rb := builder{sc: sc}
// 递归拉平 or 子项 Expression
dnfItems := expression.FlattenDNFConditions(condition)
newAccessItems := make([]expression.Expression, 0, len(dnfItems))
var totalRanges []*Range
hasResidual := false
for _, item := range dnfItems {
// 如果该子项 Expression 包含了 AND
if sf, ok := item.(*expression.ScalarFunction); ok && sf.FuncName.L == ast.LogicAnd {
// 递归拉平 and 子项 Expression
cnfItems := expression.FlattenCNFConditions(sf)
var accesses, filters []expression.Expression
res, err := d.detachCNFCondAndBuildRangeForIndex(cnfItems, newTpSlice, true)
if err != nil {
return nil, nil, false, nil
}
ranges := res.Ranges
accesses = res.AccessConds
filters = res.RemainedConds
if len(accesses) == 0 {
return FullRange(), nil, true, nil
}
if len(filters) > 0 {
hasResidual = true
}
totalRanges = append(totalRanges, ranges...)
newAccessItems = append(newAccessItems, expression.ComposeCNFCondition(d.sctx, accesses...))
} else if firstColumnChecker.check(item) {
if firstColumnChecker.shouldReserve {
hasResidual = true
firstColumnChecker.shouldReserve = d.lengths[0] != types.UnspecifiedLength
}
// 计算逻辑区间
points := rb.build(item)
// 将区间转化为外暴露的 range 结构
ranges, err := points2Ranges(sc, points, newTpSlice[0])
if err != nil {
return nil, nil, false, errors.Trace(err)
}
totalRanges = append(totalRanges, ranges...)
newAccessItems = append(newAccessItems, item)
} else {
//生成 [null, +∞) 区间
return FullRange(), nil, true, nil
}
}
// 区间并
// 例如区间:[a, b], [c, d],表示的是a <= c. If b >= c
// 那么这两个区间可以合并为:[a, max(b, d)].
totalRanges, err := UnionRanges(sc, totalRanges, d.mergeConsecutive)
if err != nil {
return nil, nil, false, errors.Trace(err)
}
return totalRanges, []expression.Expression{expression.ComposeDNFCondition(d.sctx, newAccessItems...)}, hasResidual, nil
}
detachDNFCondAndBuildRangeForIndex 方法中会拉平 or 子项,然后进行遍历,因为子项中可能嵌套子项,例如:where b=5 or ( b>5 and (b>6 or b <8) and b<12)
经过 FlattenDNFConditions 拉平之后会变成两个子项:EQ 和 AND
那么,对于 AND 子项来说会继续调用 FlattenCNFConditions 拉平,之后进入到 detachCNFCondAndBuildRangeForIndex 进行范围区间的提取,这个我们后面再说。先看看 EQ 这个子项的处理。
EQ 子项会进入到 build 方法中,根据类型判断构建 point :
func (r *builder) buildFromScalarFunc(expr *expression.ScalarFunction) []*point {
switch op := expr.FuncName.L; op {
case ast.GE, ast.GT, ast.LT, ast.LE, ast.EQ, ast.NE, ast.NullEQ:
return r.buildFormBinOp(expr)
...
case ast.In:
retPoints, _ := r.buildFromIn(expr)
return retPoints
case ast.Like:
return r.newBuildFromPatternLike(expr)
case ast.IsNull:
startPoint := &point{start: true}
endPoint := &point{}
return []*point{startPoint, endPoint}
case ast.UnaryNot:
return r.buildFromNot(expr.GetArgs()[0].(*expression.ScalarFunction))
}
return nil
}
buildFromScalarFunc 中包含了很多 buildFromXXX 方法,它们是计算一个具体函数的 range 的方法。比如 buildFromIn 便是处理 in 函数的方法。
每个 point 代表区间的一个端点:
type point struct {
value types.Datum
excl bool // exclude
start bool
}
value 表示端点的值, excl 表示端点为开区间的端点还是闭区间的端点,start 表示这个端点是左端点还是右端点。
我们这里的 EQ 子项会进入到 buildFormBinOp 方法中。
func (r *builder) buildFormBinOp(expr *expression.ScalarFunction) []*point {
...
var col *expression.Column
var ok bool
// 因为有的人喜欢这样写表达式:where 5=b,所以这里需要获取表达式中的列名和值
// 判断第一个参数是否是列字段
if col, ok = expr.GetArgs()[0].(*expression.Column); ok {
ft = col.RetType
// 获取值
value, err = expr.GetArgs()[1].Eval(chunk.Row{})
if err != nil {
return nil
}
op = expr.FuncName.L
} else {
// 参数的第二个是列
col, ok = expr.GetArgs()[1].(*expression.Column)
if !ok {
return nil
}
ft = col.RetType
value, err = expr.GetArgs()[0].Eval(chunk.Row{})
if err != nil {
return nil
}
// 因为表达式是这样写的:where 5=b 所以需要将表达式中的符号做一下反转
switch expr.FuncName.L {
case ast.GE:
op = ast.LE
case ast.GT:
op = ast.LT
case ast.LT:
op = ast.GT
case ast.LE:
op = ast.GE
default:
op = expr.FuncName.L
}
}
if op != ast.NullEQ && value.IsNull() {
return nil
}
...
//处理unsigned列
value, op, isValidRange := handleUnsignedCol(ft, value, op)
if !isValidRange {
return nil
}
// 处理越界情况
value, op, isValidRange = handleBoundCol(ft, value, op)
if !isValidRange {
return nil
}
// 构建区间端点
switch op {
case ast.NullEQ:
if value.IsNull() {
return []*point{{start: true}, {}} // [null, null]
}
fallthrough
case ast.EQ:
startPoint := &point{value: value, start: true}
endPoint := &point{value: value}
return []*point{startPoint, endPoint}
case ast.NE:
startPoint1 := &point{value: types.MinNotNullDatum(), start: true}
endPoint1 := &point{value: value, excl: true}
startPoint2 := &point{value: value, start: true, excl: true}
endPoint2 := &point{value: types.MaxValueDatum()}
return []*point{startPoint1, endPoint1, startPoint2, endPoint2}
...
}
return nil
}
buildFormBinOp 主要是对一些异常情况进行处理,如:unsigned列、越界、特殊列的值,然后构建区间端点 Point 数组。
然后就是调用 points2Ranges 将 Point 数组转为 range:
func points2Ranges(sc *stmtctx.StatementContext, rangePoints []*point, tp *types.FieldType) ([]*Range, error) {
ranges := make([]*Range, 0, len(rangePoints)/2)
for i := 0; i < len(rangePoints); i += 2 {
startPoint := rangePoints[i]
...
endPoint := rangePoints[i+1]
...
ran := &Range{
LowVal: []types.Datum{startPoint.value},
LowExclude: startPoint.excl,
HighVal: []types.Datum{endPoint.value},
HighExclude: endPoint.excl,
}
ranges = append(ranges, ran)
}
return ranges, nil
}
上面的代码形态我做了一些处理方面理解这段代码的意思,主要就是获取端点的开闭区间构建 Range。
func (d *rangeDetacher) detachCNFCondAndBuildRangeForIndex(conditions []expression.Expression, tpSlice []*types.FieldType, considerDNF bool) (*DetachRangeResult, error) {
var (
eqCount int
ranges []*Range
err error
)
...
res := &DetachRangeResult{}
// accessConds 用于抽出 eq/in 可以用于点查的条件构建范围查询
// newConditions 用来简化同字段出现多次的 eq 或 in 条件的情况,如:a in (1, 2, 3) and a in (2, 3, 4) 被简化为 a in (2, 3)
accessConds, filterConds, newConditions, emptyRange := ExtractEqAndInCondition(d.sctx, conditions, d.cols, d.lengths)
eqOrInCount := len(accessConds)
// 根据access构建范围区间
ranges, err = d.buildCNFIndexRange(tpSlice, eqOrInCount, accessConds)
if err != nil {
return res, err
}
res.Ranges = ranges
res.AccessConds = accessConds
checker := &conditionChecker{
colUniqueID: d.cols[eqOrInCount].UniqueID,
length: d.lengths[eqOrInCount],
shouldReserve: d.lengths[eqOrInCount] != types.UnspecifiedLength,
}
if considerDNF {
...
if eqOrInCount > 0 {
newCols := d.cols[eqOrInCount:]
newLengths := d.lengths[eqOrInCount:]
tailRes, err := DetachCondAndBuildRangeForIndex(d.sctx, newConditions, newCols, newLengths)
if len(tailRes.AccessConds) > 0 {
res.Ranges = appendRanges2PointRanges(res.Ranges, tailRes.Ranges)
res.AccessConds = append(res.AccessConds, tailRes.AccessConds...)
}
res.RemainedConds = append(res.RemainedConds, tailRes.RemainedConds...)
...
return res, nil
}
// 到这里,说明eqOrInCount = 0
// 遍历所有 conditions ,如果该condition是LogicOr Scalar Function类型的,则调用 DNF 相关函数进行处理
res.AccessConds, res.RemainedConds = detachColumnCNFConditions(d.sctx, newConditions, checker)
// 获取 AccessConds 的范围 range
ranges, err = d.buildCNFIndexRange(tpSlice, 0, res.AccessConds)
if err != nil {
return nil, err
}
res.Ranges = ranges
return res, nil
}
...
return res, nil
}
AND 表达式中,只有当之前的列均为点查的情况下,才会考虑下一个列。
例如:对于索引 (a, b, c),有条件 a > 1 and b = 1
,那么会被选中的只有 a > 1。对于条件 a in (1, 2, 3) and b > 1
,两个条件均会被选到用来计算 range。
所以在这个方法中,首先会调用 ExtractEqAndInCondition 函数抽离出 eq/in 可以用于点查的条件构建范围查询赋值到 accessConds 中,剩余的条件被抽离到 newConditions 中。
然后对于联合索引中,如果第一个字段是 eq/in 点查询,那么 eqOrInCount 不为0,就可以继续向后获取其他字段的范围。所以接下来会调用 DetachCondAndBuildRangeForIndex 获取其他字段的范围。
对于 eqOrInCount 等于0的条件,说明字段中不存在 eq/in 点查询,或者联合索引中左边的字段查询不为点查询,那么会调用 detachColumnCNFConditions 对单列索引进行处理。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/cRMXi0_VcMEZw36I06ywoQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。