MySQL update语法SQL解析源码分析

最近在MySQL中实现了Oracle/PostgreSQL中UPDATE…RETURNING…语法,实现过程中详细分析了update语法SQL解析过程,博客中记一笔

mysql_update调用过程

执行一条update语句的入口,函数参数:

int mysql_update(THD *thd,TABLE_LIST *tables,List<Item> &fields,
		 List<Item> &values,COND *conds,
		 uint order_num, ORDER *order, ha_rows limit,
		 enum enum_duplicates handle_duplicates, bool ignore,
                 ha_rows *updated_return);

传入参数比较多,重点介绍其中几个:
fields:set左值集合,即需要set的字段集合
values:set右值集合,与fields中的字段一一对应
conds:where字段,想详细了解这部分可以参考orczhou的一篇博客
found_return,found_return:这两个是输出参数,返回根据where条件找到的记录数和实际发生更新的记录数

从mysql_update被调用的地方mysql_execute_command,观察这些参数是从什么地方来的:

res= (up_result= mysql_update(thd, all_tables,
                                  select_lex->item_list,
                                  lex->value_list,
                                  select_lex->where,
                                  select_lex->order_list.elements,
                                  select_lex->order_list.first,
                                  unit->select_limit_cnt,
                                  lex->duplicates, lex->ignore,
                                  &found, &updated));

fields <- thd->lex->select_lex.item_list
values <- thd->lex->values_list
conds <- thd->lex->select_lex.where

实际上,这些参数都来自:thd->lex,MySQL支持的所有支持的SQL/存储过程/command在通过lex和yacc分析后都保存在这个结构中,这篇博客只关注与UPDATE语法相关的

update sql解析过程

解析入口:

dispatch_command
|-->mysql_parse
    |-->parse_sql
        |-->MYSQLparse

在sql/sql_yacc.cc中

#define yyparse         MYSQLparse

看到yyparse应该有些眉目了,mysql_parse解析的对象是单条SQL, 若SQL为multi-statement,会以分号解析成多条SQL多次调用mysql_parse,调用parse_sql之前都会先调用lex_start清理内存,解析规则是在sql/sql_yacc.yy中定义,sql_yacc.yy文件被gcc编译后会自动生成对应的sql_yacc.cc文件(这个定义了一个可执行的自动状态机,代码基本不可读)

sql/sql_yacc.yy

定义了一系列复杂的规则,需要关注两个规则:
query:所有的statement都会规约到这个规则
statement:MySQL支持的各种SQL定义,如果要添加一种SQL语法,需要在statement规则中增加一行,并在sql_yacc.yy文件中定义相应的规则描述

一种简单方法查找一条SQL对应的语法规则定义:通过thd->lex_sql_command确定SQL类型,然后在sql_yacc.yy中grep一下就OK了

在sql_yacc.yy中查看statement中update规则的定义

/* Update rows in a table */
update:
          UPDATE_SYM
          {     
            LEX *lex= Lex;
            mysql_init_select(lex);
            lex->sql_command= SQLCOM_UPDATE;
            lex->duplicates= DUP_ERROR; 
          }     
          opt_low_priority opt_ignore join_table_list
          SET update_list
          {     
            LEX *lex= Lex;
            if (lex->select_lex.table_list.elements > 1)
              lex->sql_command= SQLCOM_UPDATE_MULTI;
            else if (lex->select_lex.get_table_list()->derived)
            {     
              /* it is single table update and it is update of derived table */
              my_error(ER_NON_UPDATABLE_TABLE, MYF(0),
                       lex->select_lex.get_table_list()->alias, "UPDATE");
              MYSQL_YYABORT;
            }     
            /*    
              In case of multi-update setting write lock for all tables may
              be too pessimistic. We will decrease lock level if possible in
              mysql_multi_update().
            */
            Select->set_lock_for_tables($3);
          }     
          where_clause opt_order_clause delete_limit_clause {}
        ;
 
update_list:
          update_list ',' update_elem
        | update_elem
        ;
 
update_elem:
          simple_ident_nospvar equal expr_or_default
          {
            if (add_item_to_list(YYTHD, $1) || add_value_to_list(YYTHD, $3))
              MYSQL_YYABORT;
          }
        ;

非常清晰的定义!field保存到了lex->select_lex.item_list中,而value保存在lex->value_list中,并且它们是一一对应的

规则是嵌套定义的,如果想了解where条件怎么解析的,可以查看规则where_clause:

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();
          }
        ;

整个where条件会被规约成一个expr,保存在lex->where中

下面以一个例子说明update解析后的结果:

update t2 set b=b*2-1, c=c/2+4 where id=5;

set左值集合:fields

thd->lex->select_lex.item_list
List<Item>
|-->Item_field(b)
|-->Item_field(c)

set右值集合:values:

thd->lex->value_list
list<Item>
|-->Item_func_minus
    |-->args[0]:Item_func_mul
        |-->args[0]:Item_field(b)
        |-->args[1]:Item_int(2)
    |-->args[1]:Item_int(1)
|-->Item_func_plus
    |-->args[0]:Item_func_div
        |-->args[0]:Item_field(c)
        |-->args[1]:Item_int(2)
    |-->args[1]:Item_int(4)

where 条件:

thd->lex->select_lex.where
Item_func_eq
|-->args[0]:Item_field(id)
|-->args[1]:Item_int(5)

Debug Tips

1. 在sql_yacc.yy的文件头部加上一行
%verbose
make之后会生成sql_yacc.output文件,其中有关状态机的详细信息,包括各个状态与定义规则的对应关系,shift/reduce行为,规则冲突信息

state 568
  1575 update: UPDATE_SYM $@142 . opt_low_priority opt_ignore join_table_list SET update_list $@143 where_clause opt_order_cla
use delete_limit_clause
    LOW_PRIORITY  shift, and go to state 844
    $default  reduce using rule 1582 (opt_low_priority)
    opt_low_priority  go to state 936
 
state 936
  1575 update: UPDATE_SYM $@142 opt_low_priority . opt_ignore join_table_list SET update_list $@143 where_clause opt_order_clause delete_limit_clause                                                                                                       
    IGNORE_SYM  shift, and go to state 120
    $default  reduce using rule 910 (opt_ignore)
    opt_ignore  go to state 1399

:’.’表示当前状态对应的位置

2. 启动MySQL时,打开parser_debug标志可以看到sql解析过程中的状态转移信息
/u01/mysql/bin/mysqld –defaults-file=/u01/mysql/my.cnf –debug=”d,parser_debug” &
解析 “update t2 set b=b*2-1, c=c/2+4 where id=5” 时产生的调试信息:

Stack now 0 53 568 936 1399 1844 2341 2753 3100 2230 2657
Entering state 765
Reducing stack by rule 1575 (line 10698):
   $1 = token UPDATE_SYM ()
   $2 = nterm $@142 ()
   $3 = nterm opt_low_priority ()
   $4 = nterm opt_ignore ()
   $5 = nterm join_table_list ()
   $6 = token SET ()
   $7 = nterm update_list ()
   $8 = nterm $@143 ()
   $9 = nterm where_clause ()
   $10 = nterm opt_order_clause ()
   $11 = nterm delete_limit_clause ()
-> $$ = nterm update ()
Stack now 0
Entering state 90
Reducing stack by rule 58 (line 1771):
   $1 = nterm update ()
-> $$ = nterm statement ()
Stack now 0
Entering state 59
Reducing stack by rule 7 (line 1716):
   $1 = nterm statement ()
-> $$ = nterm verb_clause ()
Stack now 0
Entering state 58
Next token is token END_OF_INPUT ()
Shifting token END_OF_INPUT ()
Entering state 582
Reducing stack by rule 4 (line 1703):
   $1 = nterm verb_clause ()
   $2 = token END_OF_INPUT ()
-> $$ = nterm query ()
Stack now 0
Entering state 57
Reading a token: Now at end of input.
Shifting token $end ()
Entering state 581
Stack now 0 57 581
Cleanup: popping token $end ()
Cleanup: popping nterm query ()

《MySQL update语法SQL解析源码分析》上有3条评论

  1. 比如我有一张player 表。。里面有三个字段,id, name, sex
    我要更新这个人的性别但这样写了update player set id=1, name=’ss’, sex=0 where id=1
    其实改变的只有sex
    这样会全部更新么。。

回复 gpfeng 取消回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注