从Mysql源代码角度分析一句简单sql的查询过程

1. 前言

使用mysql这么多年,以前一直只懂写sql,却不其中运行原理,直至最近抽时间看了一下mysql源代码,
对其事务运行原理及sql解析优化有一些更深入的理解.
本篇是讲述sql解析的开篇之作,希望透过最最简单的sql来让大家了解sql的查询解析过程,
如果本文力图把一个简单sql的执行过程所涉及的方法及其相关值的变化详细讲清楚,如果有问题欢迎留言.

2. 准备

2.1 参考

linux下使用eclipse debug mysql5.6

2.2 创建表

create table wlt_tab1(
id int primary key
);

2.3 执行查询

select * from wlt_tab1 where 1=0 and id=0

3. 执行查询

当前sql执行时序图
如果看不清楚,可在PC端点击查看到大图,
下面会针对上面时序图的每个方法详细解说!

3.1 do_handle_one_connection()

如果线程池中没有可用的缓存线程,则会通过本方法创建线程来处理用户请求.

3.2 do_command()

读取客户端传递的命令并分发

3.3 dispatch_command()

根据用户请求信息的第一个字段表示这个请求类型,以下摘取本方法代表性的简化 代码来说明本方法在查询过程中处理了哪些功能

switch(command){
    case COM_INIT_DB: ...;
    case COM_CHANGE_USER: ...;
    case COM_STMT_PREPARE: ...;
    //如果是查询请求
    case COM_QUERY:
    //从网络数据包中读取Query并存入thd->query
    alloc_query(thd,packet,packet_length);
    //解析
    mysql_parse(thd,thd->query(),thd->query_length(),&parser_state);
    ...
}

3.4 mysql_parse()

/*
  从lex_start方法源代码上看,本方法主要是将thd->lex对象内容重新清理
  置为初始化状态.
  注: thd是当前线程上下文信息类,后续与用户处理相关函数都会传入这个类,
  估计是c++没有像java的ThreadLocal那么方便的类,所以老是要这么麻烦地传
  来传去的
  lex: 语法分析对象
  本方法的实现在:sql_lex.cc
*/
lex_start(thd);
/*
查看query cache中是否有命中,如果有,则返回结果
如果没有,则作如下动作
*/
if(query_cache_send_result_to_client(thd,rawbuf,length)<=0){
    //解析sql
    bool err = parse_sql(thd,parser_state,NULL);
    //执行
    mysql_execute_command(thd);
}

parse_sql() sql解析过程

mysql解析过程如下:
sql解析
mysql是使用了开始的bison(即yacc的开源版)作为sql语法解析器
如上图所示,在lex词法解析阶段,会解析出select,from,where这几个token
接下来sql_yacc.cc的MYSQLparse会根据上面的token解析出语法树,yacc是使用巴科斯范式(BNF)表达语法规则,大家可以百度学习一下,下面节选几个与我们相关的表达式:

select_from:
          FROM join_table_list where_clause group_clause having_clause
          opt_order_clause opt_limit_clause procedure_analyse_clause
          {
            Select->context.table_list=
              Select->context.first_name_resolution_table=
                Select->table_list.first;
          }
        | FROM DUAL_SYM where_clause opt_limit_clause
          /* oracle compatibility: oracle always requires FROM clause,
             and DUAL is system table without fields.
             Is "SELECT 1 FROM DUAL" any better than "SELECT 1" ?
          Hmmm :) */
        ;
where_clause:
          /* empty */  { Select->where= 0; }
        | WHERE
          {
            Select->parsing_place= IN_WHERE;
          }
          expr
          {
            SELECT_LEX *select= Select;
            select->where= $3;
            select->parsing_place= NO_MATTER;
            if ($3)
              $3->top_level_item();
          }
        ;

parse_sql()方法执行完后,我们可以在gdb中查看语法树lex:

##查看select_lex->where
(gdb) call print_where(lex->select_lex->where,"",QT_WITHOUT_INTRODUCERS)
WHERE:() 0x7fff98005e10 ((1 = 0) and (`id` = 0))
(gdb) p lex->select_lex->table_list->first
$9 = (TABLE_LIST *) 0x7fff98005260
##查看sql使用的database
(gdb) p $9->db
$10 = 0x7fff980057c0 "wlt"
(gdb) p $9->table_name
$11 = 0x7fff98005218 "wlt_tab1"

3.5 mysql_execute_command()

//获取解析后的sql语法树
Lex *lex = thd->lex;
//根据解析后的sql语法树的类型,决定如何作下一步处理
switch(lex->sql_command){
    case SQLCOM_SHOW_STATUS:...;
    case SQLCOM_INSERT: ...;
    case SQLCOM_SELECT:
        ...
        res = execute_sqlcom_select(thd,all_tables);
}

3.6 execute_sqlcom_select()

3.7 handle_select()

3.8 mysql_select()

sql真正执行入口,
这里会分别执行:

  • JOIN::prepare() ; //预处理
  • JOIN::optimize();//查询优化
  • JOIN::exec();//执行

3.9 JOIN::prepare()

执行sql查询优化计划前的准备工作
其中 setup_wild()方法会把查询语句中的”*”扩展为表上的所有列

3.9.1 setup_wild()

可以看本方法的主要代码:

  while (wild_num && (item= it++))
  {
    if (item->type() == Item::FIELD_ITEM &&
    //如果field值为*
    ((Item_field*) item)->field_name[0] == '*' &&
    !((Item_field*) item)->field)
    {
              if (subsel &&
          subsel->substype() == Item_subselect::EXISTS_SUBS)
      {
      ...
      }else if (insert_fields(thd, ((Item_field*) item)->context,
                             ((Item_field*) item)->db_name,
                             ((Item_field*) item)->table_name, &it,
                             any_privileges))
      {
      ...
      }
    }

3.9.2 insert_fields()

//字段迭代器
Field_iterator_table_ref field_iterator;
field_iterator.set(tables);
for (; !field_iterator.end_of_fields(); field_iterator.next())
    {
      Item *item;
      item= field_iterator.create_item(thd);
      if (!found)
      {
        found= TRUE;
        it->replace(item); 
      }
      else
        it->after(item);   /* 将当前sql语句的表的字段一一加到fields_list中 */
     }

3.10 JOIN::optimize()

JOIN::optimize()函数主要功能是对sql各种优化,包括条件下推,关联索引列,计算最优查询优化执行计划…
与本请求sql优化相关的是optimize_cond()方法
处理本sql时,optimize_cond()方法最终会将select_lex->cond_value置为Item::COND_FALSE,针对这个结果,后续处理如下:

    if (select_lex->cond_value == Item::COND_FALSE || 
        select_lex->having_value == Item::COND_FALSE || 
        (!unit->select_limit_cnt && !(select_options & OPTION_FOUND_ROWS)))
    {                       /* Impossible cond */
      zero_result_cause=  select_lex->having_value == Item::COND_FALSE ?
                           "Impossible HAVING" : "Impossible WHERE";
      tables= 0;
      primary_tables= 0;
      best_rowcount= 0;
      goto setup_subq_exit;
    }

3.10.1 optimize_cond()

这个方法主要代码可以简化如下:

//等式合并
build_equal_items(thd,conds,NULL,true,join_list,cond_equal);
//常量求值
propagate_cond_constants(thd, (I_List<COND_CMP> *) 0, conds, conds);
//条件去除
remove_eq_conds(thd, conds, cond_value) ;

在刚进这个方法时,我们可以打印 conds对象的值

(gdb) p call print_where(conds,"",QT_WITHOUT_INTRODUCERS)
WHERE:() 0x7fff98005e10 ((1 = 0) and (`wlt`.`wlt_tab1`.`id` = 0))

remove_eq_conds()方法会优化掉条件中 1=0

3.10.1.1 remove_eq_conds()

本方法会调用: internal_remove_eq_conds(thd, cond, cond_value); // Scan all the condition

3.10.1.2 internal_remove_eq_conds()
 while ((item=li++))
    {
      /×这里会取当前条件组的第一个条件递归调用本方法
      在递归的方法中会判断到item->const_item()为true,
      并对1=0进行求值:
      *tmp_cond_value= eval_const_cond(cond) ? Item::COND_TRUE : Item::COND_FALSE;
      ×/
      Item *new_item=internal_remove_eq_conds(thd, item, &tmp_cond_value);
            switch (tmp_cond_value) {
      case Item::COND_OK:           // Not TRUE or FALSE
    if (and_level || *cond_value == Item::COND_FALSE)
      *cond_value=tmp_cond_value;
    break;
    //当前1=0的条件会进入 Item::COND_FALSE
      case Item::COND_FALSE:
    if (and_level)
    {
      *cond_value=tmp_cond_value;
      return (Item*) 0;         // Always false
    }
    break;
      }

这里在gdb中如果输入:

(gdb) call print_where(conds,"",QT_WITHOUT_INTRODUCERS)
WHERE:() (nil) 

3.11 JOIN::exec()

  //
  if (zero_result_cause)
  {
    //返回0结果行
    return_zero_rows(this, *columns_list);
    DBUG_VOID_RETURN;
  }

作者: 吴炼钿

为了巩固知识,做的一个网站。基于struts2+spring+springjdbc开发的代码分享网,所有源码已开源。 网站功能介绍: 1、邮件注册(采用阿里云企业邮箱),为了让大家体验一下邮箱注册功能。我已经在分享的源码中,为大家配置好了测试账户,大家可以在自己电脑上进行测试。 2、md5加密,注册用户,所有密码会以密文保存在数据库,可以保证安全。 3、代码分享功能(核心功能),该功能的主要特色是集成了优秀的文本编辑器,支持插入代码、插入链接、插入表情、插入图片、支持在线预览。同时也实现了文件上传(基于struts2的文件上传功能)。 4、代码下载,下载功能会判断用户是否下载过该代码,若下载过则不扣积分。下载功能也是基于struts2的下载模块实现的。 5、代码评论,该功能是我仿照qq空间评论功能实现的,在本站中,我是以时间倒叙方式显示的(也支持正序)。 6、代码收藏,用户可以收藏代码。 7、消息中心,分为了0系统消息、1评论消息、2兑换消息、3上传图片消息、4上传文件消息、5下载消息(用户扣除积分)、6下载消息。 8、代码中心,分为了分享代码、下载代码、评论代码、收藏代码。 9、设置功能,支持修改昵称、城市、性别、座右铭、密码、头像。 10、赞助兑换功能,支持1个赞助兑换10个积分,也支持用赞助升级称号。 11、其他功能包括:图片压缩处理功能(即使是几M的图片,压缩后也只有几十kb)。通用json处理功能(向方法中传递任何参数,int、string等,都会返回json数据,而且速度很快)。分词功能(点击某一个分享,进入详情页的时候,会对该分享名称进行分词,并且加入到head中,利于网站seo)。 可能还有一些其他功能,通过查看源码可了解。 网站技术介绍: 1、采用语言,java 2、后台框架,struts2+spring+spring JDBC 3、前台技术,layui+jquery+ajax 网站设计思路: 前台渲染是采用的jsp技术,为了保证网站的速度,我使用了几种方法: 1、我将重复的代码保存成单独的jsp文件然后引入(这样的好处就是重复的jsp文件只会加载一次,然后浏览器缓存,下次加载速度会提升)。比如,我将link和header单独提取出来,然后在其他页面进行引入: 2、所有的业务功能,我都放在了html加载完成之后,利用jquery+ajax获取数据后再渲染界面(这样的好处就是给用户的感觉是网站速度很快。因为用户打开后,立马渲染html代码,此时网站结构已经出现,接着用jqury+ajx去后台获取数据。由于我的sql语句严格控制在ms级别,所以只需要几百ms,数据即可拿到,此时渲染在页面上给用户的感觉很快) 3、sql语句的控制,本站的所有sql语句,均控制在1s以下。这块我花了很长时间进行sql优化,我举个例子:为了减少数据库的访问次数,我会想方设法通过一条语句获取所有信息,并且严格控制它的执行速度,绝对不可以超过1s。首页的下载榜、评论榜、收藏榜,这三个功能的数据就是通过一条sql语句获取的: #优化联合查询用户评论、下载、收藏的资源列表 select a.sort,a.id,r.name,a.nowtime,r.isjing,r.isyuan, ifnull(c.res_comments_num,0) as res_comments_num, ifnull(d.res_download_num,0) as res_download_num, ifnull(kp.res_keep_num,0) as res_keep_num from #sort为1代表用户评论的代码列表 (select 1 as sort,c.resources_id as id,c.nowtime as nowtime from comments c #需要指定用户 where c.user1_id = 1 group by c.resources_id union all #sort为2代表用户下载的代码列表 select 2 as sort,d.resources_id as id,d.nowtime as nowtime from download d #需要指定用户 where d.user_id = 1 group by d.resources_id union all #sort为3代表用户收藏的代码列表 select 3 as sort,k.resources_id as id,k.nowtime as nowtime from keep
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值