惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
S
Secure Thoughts
月光博客
月光博客
美团技术团队
雷峰网
雷峰网
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
W
WeLiveSecurity
P
Proofpoint News Feed
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AI
AI
Last Week in AI
Last Week in AI
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Webroot Blog
Webroot Blog
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
N
News and Events Feed by Topic
罗磊的独立博客
The Register - Security
The Register - Security
Blog — PlanetScale
Blog — PlanetScale
T
Threat Research - Cisco Blogs
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
B
Blog
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Engineering at Meta
Engineering at Meta
Latest news
Latest news
IT之家
IT之家
D
DataBreaches.Net
博客园 - 司徒正美
N
Netflix TechBlog - Medium
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的? Redis源码阅读:list实现(ziplist, quicklist)
TiDB 源码阅读(四):AST、逻辑计划、物理计划
Jiajun Huang · 2025-10-04 · via Jiajun的技术笔记

前面我们已经看过服务端如何监听,以及接受请求,开始处理请求到最后返回数据的大体流程。这篇文章中,我们再度探索 AST、逻辑计划和物理计划,以求有更深入的了解。

为什么需要AST

AST 的全称是 抽象语法树(Abstract Syntax Tree)

简单来说,当你在数据库客户端输入一条SQL语句,比如:

SELECT name, age FROM users WHERE age > 18;

数据库系统并不会直接去理解和执行这串文本字符。它做的第一件事,就是像编译器和解释器一样,将这条SQL语句“翻译”成一个内部的数据结构。这个数据结构就是AST

AST是什么样子的?

AST是一个树形结构,它:

  • 抛弃了不重要的细节:比如空格、换行符、定界符(如分号)等。
  • 保留了代码的语法结构:将SQL中的关键字、表名、列名、操作符、值等,按照它们的语法关系组织成一棵树。
  • 每个节点都代表一个语法结构
    • 根节点 可能代表整个 SELECT 语句。
    • 子节点 可能分别代表 SELECT 列表、FROM 子句、WHERE 子句。
    • SELECT 列表节点下,又会有两个子节点,分别代表列 nameage
    • WHERE 子句节点下,会有一个代表“大于”操作的节点,这个节点又有两个子节点,分别代表列 age 和数字 18

上面那条SQL的AST可以简化为以下结构:

          [SelectStmt]
          /     |     \
     [TargetList] [FromClause] [WhereClause]
        /   \        |            |
 [ColumnRef] ... [TableRef]   [A_Expr (>)]
    /   \          |           /        \
[name] [age]    [users]  [ColumnRef]  [Const (18)]
                                |
                              [age]

核心思想:AST是源代码语法结构的一种抽象表示,它用树的形式清晰地表达了代码的层次和组成关系。

因此,AST的作用,就是让客户端传来的SQL变得让服务端可理解,可操作。

逻辑计划

那么有了AST之后,是在哪里开始转化成逻辑执行计划的呢?在 pkg/planner/optimize.go 中的 optimize 函数:

func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW, is infoschema.InfoSchema) (base.Plan, types.NameSlice, float64, error) {
	// ... 省略部分代码 ...

	// build logical plan
	hintProcessor := hint.NewQBHintHandler(sctx.GetSessionVars().StmtCtx)
	node.Node.Accept(hintProcessor)
	defer hintProcessor.HandleUnusedViewHints()
	builder := planBuilderPool.Get().(*core.PlanBuilder)
	defer planBuilderPool.Put(builder.ResetForReuse())
	builder.Init(sctx, is, hintProcessor)
	p, err := buildLogicalPlan(ctx, sctx, node, builder)  // 调用 buildLogicalPlan
    // ... 省略部分代码 ...
}

func buildLogicalPlan(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW, builder *core.PlanBuilder) (base.Plan, error) {
	sctx.GetSessionVars().PlanID.Store(0)
	sctx.GetSessionVars().PlanColumnID.Store(0)
	sctx.GetSessionVars().MapScalarSubQ = nil
	sctx.GetSessionVars().MapHashCode2UniqueID4ExtendedCol = nil

	// ... 省略 failpoint ...

	// reset fields about rewrite
	sctx.GetSessionVars().RewritePhaseInfo.Reset()
	beginRewrite := time.Now()
	p, err := builder.Build(ctx, node)  // 核心:调用 PlanBuilder.Build
	if err != nil {
		return nil, err
	}
	sctx.GetSessionVars().RewritePhaseInfo.DurationRewrite = time.Since(beginRewrite)

    // ... 省略部分代码 ...
}

// Build builds the ast node to a Plan.
func (b *PlanBuilder) Build(ctx context.Context, node *resolve.NodeW) (base.Plan, error) {
	err := b.checkSEMStmt(node.Node)
	if err != nil {
		return nil, err
	}

	b.resolveCtx = node.GetResolveContext()
	b.optFlag |= rule.FlagPruneColumns
	switch x := node.Node.(type) {
	case *ast.AdminStmt:
		return b.buildAdmin(ctx, x)
	case *ast.DeallocateStmt:
		return &Deallocate{Name: x.Name}, nil
	case *ast.DeleteStmt:
		return b.buildDelete(ctx, x)
	case *ast.ExecuteStmt:
		return b.buildExecute(ctx, x)
	case *ast.ExplainStmt:
		return b.buildExplain(ctx, x)
	case *ast.ExplainForStmt:
		return b.buildExplainFor(x)
	case *ast.TraceStmt:
		return b.buildTrace(x)
	case *ast.InsertStmt:
		return b.buildInsert(ctx, x)
	// ...
	case *ast.SelectStmt:  // SELECT 语句
		if x.SelectIntoOpt != nil {
			return b.buildSelectInto(ctx, x)
		}
		return b.buildSelect(ctx, x)  // 调用 buildSelect
	case *ast.SetOprStmt:
		return b.buildSetOpr(ctx, x)
	case *ast.UpdateStmt:
		return b.buildUpdate(ctx, x)
	// ... 更多 case ...
    }
    // ... 省略部分代码 ...
}

比如 select 语句,就是在 buildSelect 函数中:

func (b *PlanBuilder) buildSelect(ctx context.Context, sel *ast.SelectStmt) (p base.LogicalPlan, err error) {
	b.pushSelectOffset(sel.QueryBlockOffset)
	b.pushTableHints(sel.TableHints, sel.QueryBlockOffset)
	defer func() {
		b.popSelectOffset()
		// table hints are only visible in the current SELECT statement.
		b.popTableHints()
	}()
	// ... 省略部分代码 ...

	// 这个方法会构建各种逻辑算子,如:
	// - LogicalDataSource(数据源)
	// - LogicalSelection(过滤条件)
	// - LogicalJoin(连接)
	// - LogicalAggregation(聚合)
	// - LogicalProjection(投影)
	// 等等
}

根据AST构建了逻辑执行计划之后,接下来做的事情就是优化逻辑执行计划:

	names := p.OutputNames()

	// Handle the non-logical plan statement.
	logic, isLogicalPlan := p.(base.LogicalPlan)
	if !isLogicalPlan {
		return p, names, 0, nil
	}

	core.RecheckCTE(logic)

	beginOpt := time.Now()
	finalPlan, cost, err := core.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic)  // 逻辑优化 + 物理优化
	// TODO: capture plan replayer here if it matches sql and plan digest

	sessVars.DurationOptimization = time.Since(beginOpt)
	return finalPlan, names, cost, err

有两种优化方式,分别是:

// doOptimize optimizes a logical plan into a physical plan,
// while also returning the optimized logical plan, the final physical plan, and the cost of the final plan.
// The returned logical plan is necessary for generating plans for Common Table Expressions (CTEs).
func doOptimize(ctx context.Context, sctx base.PlanContext, flag uint64, logic base.LogicalPlan) (
	base.LogicalPlan, base.PhysicalPlan, float64, error) {
	if sctx.GetSessionVars().GetSessionVars().EnableCascadesPlanner {
		return CascadesOptimize(ctx, sctx, flag, logic)
	}
	return VolcanoOptimize(ctx, sctx, flag, logic)
}

VolcanoOptimize vs CascadesOptimize

假设你要从 A 地到 B 地:

  • Volcano 优化器:像是用导航软件规划路线

    • 先决定走哪条路(逻辑优化)
    • 再决定用什么交通工具(物理优化)
    • 直接、高效,适合大多数场景
  • Cascades 优化器:像是有一个智能助手帮你规划

    • 把所有可能的路线都记录在”小本本”(Memo)里
    • 发现两条路其实是一样的?合并!不重复考虑
    • 系统化地探索更多可能性,找到更优解

核心区别对比

1. 优化流程

Volcano(两阶段流水线):

SQL 语句
  ↓
逻辑优化(应用各种规则)
  ↓
物理优化(选择执行方式)
  ↓
执行计划

Cascades(Memo + 任务驱动):

SQL 语句
  ↓
归一化(预处理)
  ↓
构建 Memo(记录所有等价方案)
  ↓
任务调度(系统化搜索)
  ↓
执行计划

2. 数据结构

Volcano: - 直接在计划树上操作 - 就像在一棵树上不断修剪、嫁接

Cascades: - 使用 Memo 这个”记忆本” - Group:存储逻辑上等价的计划(比如 A JOIN BB JOIN A) - GroupExpression:具体的某种实现方式 - 好处:避免重复计算,提高效率

3. 搜索策略

特性 Volcano Cascades
搜索方式 自顶向下递归 任务驱动
去重机制 有限 完善的 hash 去重
搜索空间 相对保守 更全面
复杂度 简单直接 更复杂但更强大

火山模型

在此,我想再聊聊火山模型:

火山模型,也称为迭代器模型,是数据库系统中一种经典的查询执行引擎的实现模型。它的核心思想是:查询计划中的每个运算符(Operator)都实现一个统一的、简单的接口,数据像火山喷发一样,通过调用这些接口,以一个元组(Tuple,即一行数据)为单位的“流”式地从底层运算符传递到顶层运算符。

这个统一的接口通常包含一个 next() 方法:

next(): 调用该方法,运算符会返回下一个元组;如果已经没有更多元组,则返回一个结束标记(如 EOF)。

在整个执行过程中,数据是以一次一行的方式在各个运算符之间“流动”的。每个运算符都像一个小处理器,通过 next() 方法“拉取”数据。

非常的简单明了。但是,缺点是啥呢?就是很多函数调用,效率不高,TiDB采用的解决方案是,从一次一行数据,改为一次一个chunk。

// Next implements the Executor Next interface.
func (e *SelectLockExec) Next(ctx context.Context, req *chunk.Chunk) error {
    // ...
}

除此之外,还能想到的一个缺点就是,如果底层不断的返回数据,全都要上层来判断是抛弃还是保留数据的话,效率会非常低,约等于 全表扫描。那么解决办法是啥呢?就是想办法把数据过滤,下推到真正查找数据的那一层,这就是谓词下推。

关于火山模型我们就聊这么多,接下来,我们继续看看物理计划。

物理计划

优化完逻辑计划的下一步,就是构建物理计划。物理计划,意思就是涉及到如何操作数据的计划,已经贴近数据层的操作了。

func doOptimize(ctx context.Context, sctx base.PlanContext, flag uint64, logic base.LogicalPlan) (
	base.LogicalPlan, base.PhysicalPlan, float64, error)

这个函数将逻辑计划转换成物理计划,最后返回逻辑计划,物理计划,以及计划的成本。

func physicalOptimize(logic base.LogicalPlan) (plan base.PhysicalPlan, cost float64, err error) {
    // 1. 计算统计信息
    logic.RecursiveDeriveStats(nil)

    // 2. 准备可能的属性
    preparePossibleProperties(logic)

    // 3. 创建根节点的物理属性需求
    prop := &property.PhysicalProperty{
        TaskTp:      property.RootTaskType,
        ExpectedCnt: math.MaxFloat64,
    }

    // 4. 【关键】调用 FindBestTask 寻找最优物理计划
    t, err := logic.FindBestTask(prop)  // 第1099行

    // 5. 从 Task 中提取物理计划
    return t.Plan(), cost, err
}
func findBestTask(super base.LogicalPlan, prop *property.PhysicalProperty) (bestTask base.Task, err error) {
    // 1. 检查缓存,避免重复计算
    bestTask = p.GetTask(prop)
    if bestTask != nil {
        return bestTask, nil
    }

    // 2. 枚举当前逻辑算子所有可能的物理实现
    physicalPlans, hintWorksWithProp, err := exhaustPhysicalPlans(p.Self(), newProp)

    // 3. 对每个物理算子,递归获取子节点的最优物理计划
    // 4. 计算代价并选择代价最低的
    // 5. 缓存结果
}

以一个实际SQL为例子

SELECT name, age
FROM users
WHERE age > 18 AND city = 'Beijing'
ORDER BY age DESC
LIMIT 10

第一步:SQL → 逻辑计划(Logical Plan)

1. 解析阶段(Parse)

SQL 首先被解析器转换成 AST(抽象语法树):

ast.SelectStmt {
    Fields: [name, age]
    From: TableSource{users}
    Where: BinaryExpr(AND) {
        Left: age > 18
        Right: city = 'Beijing'
    }
    OrderBy: age DESC
    Limit: 10
}

2. 构建逻辑计划(Build Logical Plan)

代码路径在 logical_plan_builder.go:911-939

func (b *PlanBuilder) buildSelection(ctx context.Context, p base.LogicalPlan,
    where ast.ExprNode, aggMapper map[*ast.AggregateFuncExpr]int) (base.LogicalPlan, error) {

    // 第921行:创建 LogicalSelection
    selection := logicalop.LogicalSelection{}.Init(b.ctx, b.getSelectOffset())

    // 第922-938行:将 WHERE 条件转换为 Expression
    for _, cond := range conditions {
        expr, np, err := b.rewrite(ctx, cond, p, aggMapper, false)
        expressions = append(expressions, expr)
    }

    // 第940-950行:转换为 CNF(合取范式)
    for _, expr := range expressions {
        cnfItems := expression.SplitCNFItems(expr)
        cnfExpres = append(cnfExpres, cnfItems...)
    }
}

生成的逻辑计划树:

LogicalLimit (limit=10)
    ↓
LogicalSort (orderBy=[age DESC])
    ↓
LogicalProjection (columns=[name, age])
    ↓
LogicalSelection (conditions=[age > 18, city = 'Beijing'])
    ↓
DataSource (table=users)

3. 逻辑优化(Logical Optimization)

optimizer.go:1040-1074logicalOptimize 函数中,应用各种逻辑优化规则:

func logicalOptimize(ctx context.Context, flag uint64, logic base.LogicalPlan) {
    // 应用优化规则列表
    for i, rule := range logicalRuleList {
        logic, _, err = rule.Optimize(ctx, logic)
    }
}

主要优化规则包括: - 列裁剪(Column Pruning):只保留 name, age, city 列 - 谓词下推(Predicate Push Down):把 WHERE 条件下推到 DataSource - 常量传播(Constant Propagation) - 投影消除(Projection Elimination)

优化后的逻辑计划:

LogicalLimit (limit=10)
    ↓
LogicalSort (orderBy=[age DESC])
    ↓
DataSource (table=users,
            columns=[name, age, city],
            pushed_conditions=[age > 18, city = 'Beijing'])

第二步:逻辑计划 → 物理计划(Physical Plan)

1. 物理优化入口

optimizer.go:1082-1119physicalOptimize 函数:

func physicalOptimize(logic base.LogicalPlan) (plan base.PhysicalPlan, cost float64, err error) {
    // 1. 计算统计信息
    logic.RecursiveDeriveStats(nil)

    // 2. 准备物理属性
    preparePossibleProperties(logic)

    // 3. 创建物理属性需求
    prop := &property.PhysicalProperty{
        TaskTp:      property.RootTaskType,
        ExpectedCnt: math.MaxFloat64,
    }

    // 4. 【关键】调用 FindBestTask 递归生成物理计划
    t, err := logic.FindBestTask(prop)  // ← 这里开始转换!

    return t.Plan(), cost, err
}

2. 递归生成物理计划

LogicalLimit 为例,在 find_best_task.go:574 开始:

func findBestTask(super base.LogicalPlan, prop *property.PhysicalProperty) (bestTask base.Task, err error) {
    // 1. 检查缓存
    bestTask = p.GetTask(prop)
    if bestTask != nil {
        return bestTask, nil  // 命中缓存,直接返回
    }

    // 2. 枚举当前逻辑算子的所有物理实现
    physicalPlans, hintWorksWithProp, err := exhaustPhysicalPlans(p.Self(), newProp)

    // 3. 对每个物理算子
    for _, pp := range physicalPlans {
        // 3.1 递归获取子节点的最优物理计划
        childTasks, err = iteration(iterObj, pp, childTasks, prop)

        // 3.2 把子计划附加到当前物理算子
        curTask := pp.Attach2Task(childTasks...)

        // 3.3 计算代价并比较
        if curIsBetter, err := compareTaskCost(curTask, bestTask); curIsBetter {
            bestTask = curTask
        }
    }

    // 4. 缓存并返回最优方案
    p.StoreTask(prop, bestTask)
    return bestTask, nil
}

3. 枚举物理算子

对于 LogicalSelection,在 physical_selection.go:54-79

func ExhaustPhysicalPlans4LogicalSelection(p *logicalop.LogicalSelection,
    prop *property.PhysicalProperty) ([]base.PhysicalPlan, bool, error) {

    // 为不同的执行引擎生成物理算子
    ret := make([]base.PhysicalPlan, 0, len(newProps))

    for _, newProp := range newProps {
        // 创建 PhysicalSelection
        sel := PhysicalSelection{
            Conditions: p.Conditions,  // 复用逻辑算子的条件
        }.Init(p.SCtx(), p.StatsInfo().ScaleByExpectCnt(...), ...)

        ret = append(ret, sel)
    }

    return ret, true, nil
}

对于 DataSource,可能生成多种访问路径: - TableScan:全表扫描 - IndexScan:索引扫描(如果 agecity 有索引) - IndexLookup:索引回表

4. 代价计算和选择

假设 city 列有索引,age 列也有索引,系统会:

  1. 枚举所有可能的物理计划:

    方案A:全表扫描 + 过滤
    PhysicalLimit
     → PhysicalSort
       → PhysicalSelection(age>18, city='Beijing')
         → PhysicalTableScan(users)
    
    方案B:使用 city 索引
    PhysicalLimit
     → PhysicalSort(age DESC)
       → PhysicalSelection(age>18)
         → PhysicalIndexLookup(index=city_idx, filter=city='Beijing')
    
    方案C:使用 age 索引(已排序)
    PhysicalLimit
     → PhysicalSelection(city='Beijing')
       → PhysicalIndexLookup(index=age_idx, order=DESC)
    
  2. 计算每个方案的代价:

    cost = rows * cpu_cost + io_cost + network_cost + ...
    
  3. 选择代价最低的方案(假设选方案C)


最终的物理计划

PhysicalLimit (count=10, cost=100)
    ↓
PhysicalSelection (condition=[city='Beijing'], cost=500)
    ↓
PhysicalIndexReader (cost=2000)
    ↓
PhysicalIndexLookup (
        index=age_idx,
        order=DESC,
        range=[18, +∞],
        cost=1800
    )

完整流程图

┌────────────────────────────────────────────────────────────────┐
│ SELECT name, age FROM users WHERE age>18 AND city='Beijing'   │
│ ORDER BY age DESC LIMIT 10                                     │
└────────────┬───────────────────────────────────────────────────┘
             │ Parser (解析器)
             ↓
┌────────────────────────────────────────────────────────────────┐
│                       AST 抽象语法树                            │
└────────────┬───────────────────────────────────────────────────┘
             │ PlanBuilder.Build() (logical_plan_builder.go)
             ↓
┌────────────────────────────────────────────────────────────────┐
│                    初始逻辑计划树                               │
│  LogicalLimit → LogicalSort → LogicalProjection                │
│    → LogicalSelection → DataSource                             │
└────────────┬───────────────────────────────────────────────────┘
             │ logicalOptimize() (optimizer.go:1040)
             │ - 列裁剪、谓词下推、常量传播...
             ↓
┌────────────────────────────────────────────────────────────────┐
│                   优化后的逻辑计划                              │
│  LogicalLimit → LogicalSort → DataSource(带下推条件)           │
└────────────┬───────────────────────────────────────────────────┘
             │ physicalOptimize() (optimizer.go:1082)
             │ logic.FindBestTask(prop)
             ↓
┌────────────────────────────────────────────────────────────────┐
│           递归枚举和代价计算 (find_best_task.go)                │
│  - exhaustPhysicalPlans(): 枚举物理算子                        │
│  - 递归处理子节点                                               │
│  - compareTaskCost(): 代价比较                                 │
│  - 选择最优方案                                                 │
└────────────┬───────────────────────────────────────────────────┘
             ↓
┌────────────────────────────────────────────────────────────────┐
│                      最终物理计划                               │
│  PhysicalLimit → PhysicalSelection                             │
│    → PhysicalIndexReader → PhysicalIndexLookup                 │
└────────────┬───────────────────────────────────────────────────┘
             │ postOptimize() (optimizer.go:458)
             │ - 投影消除、并行化等后处理
             ↓
┌────────────────────────────────────────────────────────────────┐
│                   可执行的物理计划                              │
│                    (发送给执行引擎)                             │
└────────────────────────────────────────────────────────────────┘

关键代码位置总结

阶段 文件 关键函数 行数
构建逻辑计划 logical_plan_builder.go buildSelection() 911-939
逻辑优化 optimizer.go logicalOptimize() 1040-1074
物理优化入口 optimizer.go physicalOptimize() 1082-1119
递归生成物理计划 find_best_task.go findBestTask() 574+
枚举物理算子 exhaust_physical_plans.go exhaustPhysicalPlans() 54-96
物理算子实现 physical_selection.go ExhaustPhysicalPlans4LogicalSelection() 54-79

这就是从 SQL 到逻辑计划再到物理计划的完整过程!本质上是一个自顶向下的递归搜索过程,通过动态规划(记忆化搜索)找到代价最低的执行方案。

总结

这篇文章中,我们先从AST看起,然后接着看了逻辑计划与物理计划是如何来的,顺带介绍了数据库引擎中很重要的火山模型, 以及我们对最基本的模型的优缺点分析、优化方案。