MySQL SQL执行由sql_parse.cc、sql_executor.cc、sql_optimizer.cc和存储引擎接口(如ha_innodb.cc)四大模块接力完成,依次经历词法分析→语法树构建→逻辑优化→执行计划生成→物理执行→结果返回。

SQL执行流程在MySQL源码里从哪几个关键模块串起来
MySQL的SQL执行不是单一线程跑完的事,而是由sql_parse.cc、sql_executor.cc、sql_optimizer.cc和存储引擎接口(如ha_innodb.cc)四块核心代码接力完成的。你查一条SELECT * FROM t WHERE id=1,实际会依次触发词法分析→语法树构建→逻辑优化→执行计划生成→物理执行→结果返回。
常见错误现象:加了EXPLAIN FORMAT=TREE能看到执行计划,但改了optimizer_switch参数却没生效——说明你还没摸到sql_optimizer.cc里真正做规则匹配的optimize_cond()和make_join_statistics()这两处。
-
sql_parse.cc里的mysql_parse()是入口,它把字符串喂给Lex_input_stream,出错时parse_sql()返回非0,对应错误信息像ER_PARSE_ERROR - 优化器决策点不在
SELECT_LEX::optimize()这一层就结束了,真正下推条件、重排JOIN顺序是在JOIN::make_join_plan()里用动态规划做的 - InnoDB不直接执行SQL,而是通过
handler::index_read()或handler::rnd_next()响应上层调用,所以ha_innodb.cc里找不到“执行UPDATE”的逻辑,只看到“按索引定位+构造mtr事务”
想调试一条SQL到底走了哪些函数,怎么打日志最有效
别一上来就gdb断点mysql_execute_command()——它太靠上,跳转太多;也别在ha_innodb.cc里狂加fprintf(stderr, ...),容易冲掉关键上下文。应该聚焦三个可控且信息密度高的埋点位置。
使用场景:排查慢查询为什么没走索引,或者确认WHERE条件是否真的下推到了引擎层。
- 在
JOIN::exec()开头加LogErr(INFORMATION_LEVEL, "join exec start, plan=%s", join->best_positions[0].table->alias);,能看清优化器选的驱动表 - 在
ha_innodb.cc的index_read()里打印m_cursor->m_btr_cur.m_page_id,可验证是否真用了二级索引而非全表扫 - 开启
--debug=d,info,parser,optimizer启动mysqld,比自己写日志更稳,输出会进hostname.err,但注意parser级别日志量极大,只建议临时开
为什么看源码时总卡在THD和Query_block结构体上
THD不是简单的“线程句柄”,它是整个SQL生命周期的状态容器:保存当前事务状态、临时表列表、权限缓存、字符集上下文,甚至THD::query_plan字段在8.0后才真正存执行计划树。而Query_block(5.7叫SELECT_LEX)也不是单纯的语法树节点,它自带where_cond、join_list、group_list这些指针,指向的都是动态分配的表达式对象。
参数差异:5.7中SELECT_LEX::master_unit()返回的是SELECT_LEX_UNIT,8.0里被拆成Query_expression和Query_block两层,如果你在5.7源码里搜Query_block会找不到。
- 调试时别只看
THD::query(),它只是原始SQL字符串;关键要看THD::m_query_plan(8.0)或THD::lex->select_lex(5.7) -
Item类族(如Item_func_eq、Item_field)是条件表达式的底层表示,WHERE子句最终被编译成Item_cond_and树,遍历它要用traverse_cond()而非简单递归 - 不要假设
Query_block::where_cond一定非空——当有HAVING无WHERE时,它就是NULL,此时条件在having_cond里
从源码角度看,prepare阶段和execute阶段的边界在哪
边界在Prepared_statement::execute_loop()调用mysql_execute_server_prepare()之后。prepare阶段只做解析+优化一次,生成可复用的Prepared_statement对象;execute阶段每次调用都走execute_server_prepare(),但跳过解析和优化,直接进入JOIN::exec()。
性能影响明显:如果prepare时没绑定参数类型(比如用mysql_stmt_bind_param()传了MYSQL_TYPE_VAR_STRING但长度超限),execute阶段会在Protocol::send_result_set_metadata()里触发隐式类型转换,导致索引失效——这在源码里体现为Item::save_in_field()调用type_handler_string->Item_send_str()时重新构造临时String对象。
- prepare阶段的优化结果存在
Prepared_statement::m_query_plan,execute阶段直接复用,所以EXPLAIN只能在prepare后执行一次 - 如果SQL含用户变量(如
@a:=@a+1),prepare阶段无法确定其值,优化器会放弃对该部分做常量折叠,这部分逻辑在Item_func_get_user_var::fix_fields()里 - 8.0启用了
statement_digest后,prepare语句会被哈希进PS_cache,路径是sql/sql_prepare.cc里的Prepared_statement_map::insert(),查不到语句可能是因为digest冲突或cache已满
真正难啃的不是函数调用链,而是THD里那些跨阶段存活的状态字段——比如THD::m_locks在prepare时不申请,在execute时才加,但释放时机又依赖事务提交。这种隐式依赖,文档不会写,只有跟断点进trans_commit_stmt()和ha_commit_trans()才能看清。










