上一篇中,介绍了 TiDB 的入口,从依据配置启动 TiDB 到匹配 MySQL 协定,再到开始做 parser。那接下来咱们就简略理解下 SQL 解析解决这一块的内容。
当我还是萌新的时候,参加过 Java SQL 解析、优化器 demo 的编写,不过也只是聊到用的技术是 ANTRL,甚至不晓得为什么要做解析、优化,也不大理解是什么原理实现。
最新学习 TiDB 解析优化 SQL 的流程,深觉还是要先至多简略的理解 Lex & Yacc。
它们可能让你更容易的解析简单的语言,达成解析字符串的目标。
Lex & Yacc
输出字符流,发现某一段字符可能匹配一个关键字,就依据定义好的动作来执行。
例 1 打印
%%
begin printf("BEGIN;\n");
executeSql printf("SELECT * FROM t1;\n");
commit printf("COMMIT;\n");
%%
Lex 的每一段是通过 %% 宰割的,这里设置了 3 个关键字:
begin
executeSql
commit
读取字符流时,遇到关键字,就会依据前面的指令去执行动作。比方遇到 executeSql,会 print ” SELECT * FROM t1 ; ” 如果匹配不到关键字,会失常输入。
例 2 解析日志
[2020/07/31 09:43:01]
[INFO]
[server.go:391]
["connection closed"]
[conn=4]
依据日志中的元素,定义如下关键字
WORD > connection|conn|INFO|closed
DATE > 2020/07/31 09:43:01
FILENAME > server.go
NUM > 391|4
LEFTBRACKET > [RIGHTBRACKET >]
COLON > :
SLASH > /
EQUALSIGN > =
QUOTATIONMARK > "
Lex 分词器
%%
[a−zA−Z][a−zA−Z0−9]* return WORD
日期的正则表达式. 手动狗头 return DATE
\[a−zA−Z0−9\/.−]+ return FILENAME
\[0123456789]+ return NUM
\[ return LEFTBRACKET
\] return RIGHTBRACKET
\: return COLON
\/ return SLASH
\= return EQUALSIGN
\" return QUOTATIONMARK
%%
通过 Lex 分词的后果就是
LEFTBRACKET DATE RIGHTBRACKET
LEFTBRACKET WORD RIGHTBRACKET
LEFTBRACKET FILENAME COLON NUM RIGHTBRACKET
LEFTBRACKET QUOTATIONMARK WORD WORD QUOTATIONMARK RIGHTBRACKET
LEFTBRACKET WORD EQUALSIGN NUM RIGHTBRACKET
在 TiDB 中,相似的构造都寄存在 parser.y 中,
构造如下,
第一局部次要是定义 Token 的类型、优先级、联合性等。
%{
package parser
import (
"strings"
"github.com/pingcap/parser/mysql"
"github.com/pingcap/parser/types"
)
%}
%union {
offset int // offset
item interface{}
ident string
expr ast.ExprNode
statement ast.StmtNode
}
%token <ident>
%type <expr>
%precedence empty
%left join straightJoin inner cross left right full natural
%start Start
通过 %% 宰割,以上是第一局部,即定义段
%%
下局部是 SQL 语法的产生式和每个规定对应的 action,咱们找一个简略的看看,
这应该是 Drop Table 的 分词构造,生成 ast.DropTableStmt 语法树来执行
DropTableStmt:
"DROP" OptTemporary TableOrTables IfExists TableNameList RestrictOrCascadeOpt
{$$ = &ast.DropTableStmt{IfExists: $4.(bool), Tables: $5.([]*ast.TableName), IsView: false, IsTemporary: $2.(bool)}
}
这里有 5 个 Token,别离是
OptTemporary
TableOrTables
IfExists
TableNameList
RestrictOrCascadeOpt
别离看一下这些 Token 的定义,那两个 Table 巴拉巴拉就不看了
OptTemporary
// 应该是长期表的 Token,如果有这个 Token,则会被解析。// 但也如逻辑中写的,“TiDB 目前不反对长期表,尽管会被解析,然而不失效。”OptTemporary:
/* empty */
{$$ = false}
| "TEMPORARY"
{
$$ = true
yylex.AppendError(yylex.Errorf("TiDB doesn't support TEMPORARY TABLE, TEMPORARY will be parsed but ignored."))
parser.lastErrorAsWarn()}
if exists
// 是否有 if exists
IfExists:
{$$ = false}
| "IF" "EXISTS"
{$$ = true}
restrict: 确保只有不存在相干视图和完整性束缚的表能力删除
cascade: 任何相干视图和完整性束缚都将一并被删除
RestrictOrCascadeOpt:
{}
| "RESTRICT"
| "CASCADE"
所以能够看出,在 drop table 的时候,在这个语法结构中,” 饱满 ” 的语句大略是
drop temporary table Ifexists tablename restrict/cascade
之后就会生成一棵 ast 形象语法 ast.DropTableStmt。
ast/ddl.go
type DropTableStmt struct {
ddlNode
IfExists bool
Tables []*TableName
IsView bool
IsTemporary bool // make sense ONLY if/when IsView == false
}
这个具体的实现,比方这个
func (n *DropTableStmt) Restore(ctx *format.RestoreCtx) error {
if n.IsView {ctx.WriteKeyWord("DROP VIEW")
} else {
if n.IsTemporary {ctx.WriteKeyWord("DROP TEMPORARY TABLE")
} else {ctx.WriteKeyWord("DROP TABLE")
}
}
if n.IfExists {ctx.WriteKeyWord("IF EXISTS")
}
for index, table := range n.Tables {
if index != 0 {ctx.WritePlain(",")
}
if err := table.Restore(ctx); err != nil {return errors.Annotate(err, "An error occurred while restore DropTableStmt.Tables"+string(index))
}
}
return nil
}
先判断了 drop 的是 view 还是 table,如果走进了 table 分支,也就大略判断了是否是长期表,是否有各种非凡的语法。
到这里根本理解了 TiDB 中对于 SQL 解析的形式,当然,和行文的区别,TiDB 用的是 goyacc,不过如同区别也不是很大,这篇心愿能够给大家一个参考。