Apache Doris 源码分析3 - AST的语义分析

1
2
3
4
Apache Doris 源码分析3 - AST的语义分析
-- yakun
-- 2022-07-10 20:50:43
-- Apache Doris 源码基于 branch-0.15, commit: a1d1bd8965c3b97f2c4eaf4fd8405efd3b35368a

上回分析了 SQL 是如何从一个平平无奇的字符串, 变成了一个适合分析处理的对象的(AST 抽象语法树), 这次继续分析生成 AST 对象之后的经历, 故事的起点还是回到上上次的 handleQuery().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//位置: fe/fe-core/src/main/java/org/apache/doris/qe/ConnectProcessor.java

private void handleQuery() {

// 忽略了一些执行前的逻辑

// execute this query.
StatementBase parsedStmt = null;
List<Pair<StatementBase, Data.PQueryStatistics>> auditInfoList = Lists.newArrayList();
boolean alreadyAddedToAuditInfoList = false;
try {
List<StatementBase> stmts = analyze(originStmt); // 重点1: 词法分析和语法分析
for (int i = 0; i < stmts.size(); ++i) {
alreadyAddedToAuditInfoList = false;
ctx.getState().reset(); // 由于一个 ctx 要处理多个语句, 每个语句处理前要清理状态
if (i > 0) {
ctx.resetReturnRows();
}
parsedStmt = stmts.get(i); // 取出一个 StatementBase 对象
parsedStmt.setOrigStmt(new OriginStatement(originStmt, i));
parsedStmt.setUserInfo(ctx.getCurrentUserIdentity());
executor = new StmtExecutor(ctx, parsedStmt); // 创建一个 StmtExecutor
ctx.setExecutor(executor);
executor.execute(); // 重点2: 执行一条语句

if (i != stmts.size() - 1) {
ctx.getState().serverStatus |= MysqlServerStatusFlag.SERVER_MORE_RESULTS_EXISTS;
finalizeCommand();
}
auditInfoList.add(new Pair<>(executor.getParsedStmt(), executor.getQueryStatisticsForAuditLog()));
alreadyAddedToAuditInfoList = true;
}
} catch (IOException e) {
// 忽略了异常处理逻辑
}

// 忽略执行后的逻辑
}

通过上次的分析, 得知在经过词法和语法分析后, 得到了一个 AST 树的列表(每个树都对应一个完整的SQL语句).接下来就是遍历每一个 AST 的对象 StatementBase, 然后为每个对象初始化一个 StmtExecutor, 之后调用executor.execute() 去执行, 下面直接进入执行的逻辑:

1
2
3
4
5
6
7
8
// 位置: fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java

// query with a random sql
public void execute() throws Exception {
UUID uuid = UUID.randomUUID();
TUniqueId queryId = new TUniqueId(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
execute(queryId);
}

上面的逻辑中分配了一个唯一标识, 然后继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 位置: fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java

public void execute(TUniqueId queryId) throws Exception {
context.setStartTime();

plannerProfile.setQueryBeginTime();
// 这个细节可以关注一下:
// QueryPlannerProfile 里面一共有如下的几个时间点记录:
// - queryBeginTime : 查询开始时间点
// - queryAnalysisFinishTime : 查询分析完成时间点
// - queryPlanFinishTime : 查询计划完成时间点
// - queryScheduleFinishTime : 调度完成时间点
// - queryFetchResultFinishTime : 获得结果完成时间点
// 当前是第一个时间点: 开始时间点
// 通过这几个时间点, 可以猜到后面会经历如下几个阶段: 1.查询分析, 2.查询计划生成, 3.调度完成, 4.获得结果完成
// 在下面的两个重点函数调用, 重点函数1对应前两阶段, 重点函数2对应后两阶段

context.setStmtId(STMT_ID_GENERATOR.incrementAndGet());

context.setQueryId(queryId);

try {
if (context.isTxnModel() && !(parsedStmt instanceof InsertStmt)
&& !(parsedStmt instanceof TransactionStmt)) {
// 只有在 insert 语句中才能使用事务(transaction简称Txn)
throw new TException("This is in a transaction, only insert, commit, rollback is acceptable.");
}

// support select hint e.g. select /*+ SET_VAR(query_timeout=1) */ sleep(3);
// 分析并获取语句中的所有 hint 变量, hint 变量可以理解为一些指令
// 场景比如写 SQL 的人对实际的数据更了解, 可以下发指令去执行特定的 join, 而不是优化器自己选
// 比如说 SELECT /*+ SET_VAR(query_timeout = 1) */ * FROM A; 表示这个语句超过1秒就超时
// 注意注释必须以 /*+ 开头,并且只能跟随在SELECT之后
// 支持的变量有很多, 可以参考: fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
analyzeVariablesInStmt();

if (!context.isTxnModel()) {
// analyze this query
// 重点函数1: 分析 AST 并产生执行计划
analyze(context.getSessionVariable().toThrift());
if (isForwardToMaster()) {
forwardToMaster();
if (masterOpExecutor != null && masterOpExecutor.getQueryId() != null) {
context.setQueryId(masterOpExecutor.getQueryId());
}
return;
} else {
LOG.debug("no need to transfer to Master. stmt: {}", context.getStmtId());
}
} else {
analyzer = new Analyzer(context.getCatalog(), context);
parsedStmt.analyze(analyzer);
}

if (parsedStmt instanceof QueryStmt) { // 只看查询语句的部分
context.getState().setIsQuery(true);
MetricRepo.COUNTER_QUERY_BEGIN.increase(1L);
int retryTime = Config.max_query_retry_time;
for (int i = 0; i < retryTime; i ++) {
try {
//reset query id for each retry
if (i > 0) {
UUID uuid = UUID.randomUUID();
TUniqueId newQueryId = new TUniqueId(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
AuditLog.getQueryAudit().log("Query {} {} times with new query id: {}", DebugUtil.printId(queryId), i, DebugUtil.printId(newQueryId));
context.setQueryId(newQueryId);
}
// 重点函数2: 调度并获得执行结果
handleQueryStmt();
// explain query stmt do not have profile
if (!((QueryStmt) parsedStmt).isExplain()) {
writeProfile(true);
}
break;
} catch (RpcException e) {
if (i == retryTime - 1) {
throw e;
}
if (!context.getMysqlChannel().isSend()) {
LOG.warn("retry {} times. stmt: {}", (i + 1), parsedStmt.getOrigStmt().originStmt);
} else {
throw e;
}
} finally {
QeProcessorImpl.INSTANCE.unregisterQuery(context.queryId());
}
}
} else {
// 忽略其它类型语句的处理逻辑
}
} catch (IOException e) {
// 忽略异常处理逻辑
} finally {
// 忽略善后处理逻辑
}

上述代码中标记了2个重点函数, 分别是:

  • 重点函数1: 分析 AST 并产生执行计划
  • 重点函数2: 调度并获得执行结果

下面依次分析这2部分内容.

分析 AST 并产生执行计划

先分析一下上面的 analyze(context.getSessionVariable().toThrift()); 函数的内部逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 位置: fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java

// Analyze one statement to structure in memory.
public void analyze(TQueryOptions tQueryOptions) throws UserException {
LOG.info("begin to analyze stmt: {}, forwarded stmt id: {}", context.getStmtId(), context.getForwardedStmtId());

// 由于已经做过了词法和语法分析, 这个函数进去后会很快跳出来
// 为什么需要这个方法: 主要目的是会存在一些情况, 非 Master 的 FE 收到到了语句, 需要转交 Master 处理
// Master 在收到消息后, 还需要再重新走一遍词法和语法分析
parse();

// yiguolei: insert stmt's grammar analysis will write editlog, so that we check if the stmt should be forward to master here
// if the stmt should be forward to master, then just return here and the master will do analysis again
if (isForwardToMaster()) {
return;
}

analyzer = new Analyzer(context.getCatalog(), context);
// Convert show statement to select statement here
if (parsedStmt instanceof ShowStmt) { // 把 ShowStmt 类型转换为 SelectStmt
SelectStmt selectStmt = ((ShowStmt) parsedStmt).toSelectStmt(analyzer);
if (selectStmt != null) {
parsedStmt = selectStmt;
}
}

if (parsedStmt instanceof QueryStmt
|| parsedStmt instanceof InsertStmt
|| parsedStmt instanceof CreateTableAsSelectStmt) {
Map<Long, Table> tableMap = Maps.newTreeMap();
QueryStmt queryStmt;
Set<String> parentViewNameSet = Sets.newHashSet();
if (parsedStmt instanceof QueryStmt) {
queryStmt = (QueryStmt) parsedStmt;

// 从 queryStmt 内部的 fromClause_ 对象中解析出来所有的表放到 tableMap 中
queryStmt.getTables(analyzer, tableMap, parentViewNameSet);
} else if (parsedStmt instanceof CreateTableAsSelectStmt) {
CreateTableAsSelectStmt parsedStmt = (CreateTableAsSelectStmt) this.parsedStmt;
queryStmt = parsedStmt.getQueryStmt();
queryStmt.getTables(analyzer, tableMap, parentViewNameSet);
} else {
InsertStmt insertStmt = (InsertStmt) parsedStmt;
insertStmt.getTables(analyzer, tableMap, parentViewNameSet);
}
// table id in tableList is in ascending order because that table map is a sorted map

// 将 tables 将 table id 排序转换为 List 结构的 tables
List<Table> tables = Lists.newArrayList(tableMap.values());

// 为这些 tables 内的每个 table 对象加上读锁
MetaLockUtils.readLockTables(tables);
try {
analyzeAndGenerateQueryPlan(tQueryOptions); // 重点: 分析并生成查询计划
} catch (MVSelectFailedException e) {
// 忽略异常处理代码
} finally {
// 释放所有读锁
MetaLockUtils.readUnlockTables(tables);
}
} else {
try {
parsedStmt.analyze(analyzer);
} catch (UserException e) {
throw e;
} catch (Exception e) {
LOG.warn("Analyze failed because ", e);
throw new AnalysisException("Unexpected exception: " + e.getMessage());
}
}
}

这里继续看一下新的重点工作: 分析并生成查询计划 analyzeAndGenerateQueryPlan(tQueryOptions);函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 位置: fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java

private void analyzeAndGenerateQueryPlan(TQueryOptions tQueryOptions) throws UserException {
// 重点a: 调用 parsedStmt 的 analyze 方法
parsedStmt.analyze(analyzer);
if (parsedStmt instanceof QueryStmt || parsedStmt instanceof InsertStmt) {
ExprRewriter rewriter = analyzer.getExprRewriter();
rewriter.reset();
if (context.getSessionVariable().isEnableFoldConstantByBe()) {
// fold constant expr
parsedStmt.foldConstant(rewriter);

}
// Apply expr and subquery rewrites.
ExplainOptions explainOptions = parsedStmt.getExplainOptions();
boolean reAnalyze = false;

// 重点b: 调用 parsedStmt 的 rewriteExprs 方法
parsedStmt.rewriteExprs(rewriter);
reAnalyze = rewriter.changed();
if (analyzer.containSubquery()) {
parsedStmt = StmtRewriter.rewrite(analyzer, parsedStmt);
reAnalyze = true;
}
if (reAnalyze) {
// The rewrites should have no user-visible effect. Remember the original result
// types and column labels to restore them after the rewritten stmt has been
// reset() and re-analyzed.
List<Type> origResultTypes = Lists.newArrayList();
for (Expr e: parsedStmt.getResultExprs()) {
origResultTypes.add(e.getType());
}
List<String> origColLabels =
Lists.newArrayList(parsedStmt.getColLabels());

// Re-analyze the stmt with a new analyzer.
analyzer = new Analyzer(context.getCatalog(), context);

// query re-analyze
parsedStmt.reset();
parsedStmt.analyze(analyzer);

// Restore the original result types and column labels.
parsedStmt.castResultExprs(origResultTypes);
parsedStmt.setColLabels(origColLabels);
if (LOG.isTraceEnabled()) {
LOG.trace("rewrittenStmt: " + parsedStmt.toSql());
}
if (explainOptions != null) parsedStmt.setIsExplain(explainOptions);
}
}

// 参考前面说过的后面会经历的几个阶段: 1.查询分析, 2.查询计划生成, 3.调度, 4.获得结果
// 至此为止, 完成了第1个阶段:查询分析, 下面这条语句记录了完成时间点
plannerProfile.setQueryAnalysisFinishTime();
// 后面的部分, 就进入了第2个阶段:查询计划生成

// create plan
planner = new Planner();
if (parsedStmt instanceof QueryStmt || parsedStmt instanceof InsertStmt) {
// 重点c: 生成执行计划
planner.plan(parsedStmt, analyzer, tQueryOptions);
}
// TODO(zc):
// Preconditions.checkState(!analyzer.hasUnassignedConjuncts());

plannerProfile.setQueryPlanFinishTime();
}

上面的代码片段中, 有几个重点工作需要继续跟进分析一下:

  • 重点a: 分析: 调用 parsedStmt 的 analyze 方法
  • 重点b: 重写: 调用 parsedStmt 的 rewriteExprs 方法
  • 重点c: 生成执行计划

下面分别跟进一下.

分析: 调用 parsedStmt 的 analyze 方法

上次源码学习的最后提到过的 analyze(Analyzer analyzer) 方法, 在这里终于被调用了, 这个方法名字叫 analyze 但是我觉得叫 prepare 也很合适, 主要包含一系列的准备工作, 下面咱们以 SelectStmt 语句的这个方法为入口, 继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// 位置: fe/fe-core/src/main/java/org/apache/doris/analysis/SelectStmt.java

public void analyze(Analyzer analyzer) throws UserException {
if (isAnalyzed()) {
return;
}

super.analyze(analyzer);
fromClause_.setNeedToSql(needToSql);
fromClause_.analyze(analyzer); // 对于 from 语句进行分析, 会取出依赖的表

// Generate !empty() predicates to filter out empty collections.
// Skip this step when analyzing a WITH-clause because CollectionTableRefs
// do not register collection slots in their parent in that context
// (see CollectionTableRef.analyze()).
if (!analyzer.isWithClause()) {
registerIsNotEmptyPredicates(analyzer);
}
// populate selectListExprs, aliasSMap, groupingSmap and colNames
for (SelectListItem item : selectList.getItems()) { // 填充要选择的字段 (其实就是结果字段)
if (item.isStar()) { // 像 select * 这种情况要把 * 展开
TableName tblName = item.getTblName();
if (tblName == null) {
expandStar(analyzer);
} else {
expandStar(analyzer, tblName);
}
} else {
// Analyze the resultExpr before generating a label to ensure enforcement
// of expr child and depth limits (toColumn() label may call toSql()).
item.getExpr().analyze(analyzer);
if (!(item.getExpr() instanceof CaseExpr) &&
item.getExpr().contains(Predicates.instanceOf(Subquery.class))) {
throw new AnalysisException("Subquery is not supported in the select list.");
}

// 这里会根据预置的几条规划, 进行重写, 主要目的为了提升执行效率
// 比如, 把 count(distinct k2) 重写为 bitmap_union_count(to_bitmap(k2))
Expr expr = rewriteQueryExprByMvColumnExpr(item.getExpr(), analyzer);
resultExprs.add(expr);
SlotRef aliasRef = new SlotRef(null, item.toColumnLabel());
Expr existingAliasExpr = aliasSMap.get(aliasRef);
if (existingAliasExpr != null && !existingAliasExpr.equals(item.getExpr())) {
// If we have already seen this alias, it refers to more than one column and
// therefore is ambiguous.
ambiguousAliasList.add(aliasRef);
}
aliasSMap.put(aliasRef, item.getExpr().clone());
colLabels.add(item.toColumnLabel());
}
}

// 对 group by 语句进行分析, 如果有问题就直接抛出异常
if (groupByClause != null && groupByClause.isGroupByExtension()) {
for (SelectListItem item : selectList.getItems()) {
if (item.getExpr() instanceof FunctionCallExpr && item.getExpr().fn instanceof AggregateFunction) {
for (Expr expr: groupByClause.getGroupingExprs()) {
if (item.getExpr().contains(expr)) {
throw new AnalysisException("column: " + expr.toSql() + " cannot both in select list and "
+ "aggregate functions when using GROUPING SETS/CUBE/ROLLUP, please use union"
+ " instead.");
}
}
}
}
groupingInfo = new GroupingInfo(analyzer, groupByClause.getGroupingType());
groupingInfo.substituteGroupingFn(resultExprs, analyzer);
} else {
for (Expr expr : resultExprs) {
if (checkGroupingFn(expr)) {
throw new AnalysisException(
"cannot use GROUPING functions without [grouping sets|rollup|cube] "
+ "clause or grouping sets only have one element.");
}
}
}

if (valueList != null) {
if (!fromInsert) {
// 如果不来自 insert 语句,比如 SELECT DISTINCT * FROM (VALUES (1), (5), (1), (6)) AS X(a)
// 对 valuesList 中的每个 value 进行分析
valueList.analyzeForSelect(analyzer);
}
for (Expr expr : valueList.getFirstRow()) {
if (expr instanceof DefaultValueExpr) {
resultExprs.add(new IntLiteral(1));
} else {
resultExprs.add(expr);
}
colLabels.add(expr.toColumnLabel());
}
}
// analyze valueList if exists
if (needToSql) {
originalExpr = Expr.cloneList(resultExprs);
}

// analyze selectListExprs
Expr.analyze(resultExprs, analyzer);
if (TreeNode.contains(resultExprs, AnalyticExpr.class)) {
// 如果语句中包含分析函数语法: func(arg1,arg2,...) over ([partition by] [order by] [windowing-clause])
// 就再执行下面的两个分析
if (fromClause_.isEmpty()) {
throw new AnalysisException("Analytic expressions require FROM clause.");
}

// do this here, not after analyzeAggregation(), otherwise the AnalyticExprs
// will get substituted away
if (selectList.isDistinct()) {
throw new AnalysisException(
"cannot combine SELECT DISTINCT with analytic functions");
}
}

whereClauseRewrite(); // 简单的的改写: 如果是整数表达式的话就改为bool表达式
if (whereClause != null) {// 如果 where 语句不为空, 就进行下面的句法校验
if (checkGroupingFn(whereClause)) {
throw new AnalysisException("grouping operations are not allowed in WHERE.");
}
whereClause.analyze(analyzer);
if (whereClause.containsAggregate()) {
ErrorReport.reportAnalysisException(ErrorCode.ERR_INVALID_GROUP_FUNC_USE);
}

whereClause.checkReturnsBool("WHERE clause", false);
Expr e = whereClause.findFirstOf(AnalyticExpr.class);
if (e != null) {
throw new AnalysisException(
"WHERE clause must not contain analytic expressions: " + e.toSql());
}
analyzer.registerConjuncts(whereClause, false, getTableRefIds());
}

// 根据 order by 语句来生成排序信息
createSortInfo(analyzer);
// 上面这个函数会生成一个 sortInfo 对象, 主要包含三个成员:
// 1. 按哪些表达式来排序
// 2. 升序还是降序: ASC / DESC
// 3. 空值在前还是在后: NULLS FIRST / NULLS LAST / 不声明
if (sortInfo != null && CollectionUtils.isNotEmpty(sortInfo.getOrderingExprs())) {
if (groupingInfo != null) {
// List of executable exprs in select clause has been substituted, only the unique expr in Ordering
// exprs needs to be substituted.
// Otherwise, if substitute twice for `Grouping Func Expr`, a null pointer will be reported.
List<Expr> orderingExprNotInSelect = sortInfo.getOrderingExprs().stream()
.filter(item -> !resultExprs.contains(item)).collect(Collectors.toList());
groupingInfo.substituteGroupingFn(orderingExprNotInSelect, analyzer);
}
}

// 分析所有与聚合相关的组件, 包括 having 语句, group by, distinct, order by 等, 这个函数很长, 有兴趣可以自己看看
analyzeAggregation(analyzer);
createAnalyticInfo(analyzer);
if (evaluateOrderBy) {
// 判断 order by 里面是不是复杂类型
createSortTupleInfo(analyzer);
}

if (needToSql) {
// 把语法树结构再重新生成 sql 字符串
sqlString_ = toSql();
}
if (!analyzer.safeIsEnableJoinReorderBasedCost()) {
// 在 select A,B,C from .. 这种语句中,
// 如果不是基于代价的重排各个表 join 的顺序,
// 使用下面的基于各个表的总行数的排序作为 join 数据
LOG.debug("use old reorder logical in select stmt");
reorderTable(analyzer);
}

// An inline view is a query statement with an alias.
resolveInlineViewRefs(analyzer);

// Indicates whether the select-project-join (spj) portion of this query block
// is guaranteed to return an empty result set.
// Set due to a constant non-Having conjunct evaluating to false.
if (analyzer.hasEmptySpjResultSet() && aggInfo == null) {
analyzer.setHasEmptyResultSet();
}

if (aggInfo != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("post-analysis " + aggInfo.debugString());
}
}

// 如果有 INTO OUTFILE 语句
if (hasOutFileClause()) {
// 分析一下目标路径是否合法
// 分析一下目标文件类型是什么: 目前只支持 csv, parquet 两种类型, 其它类型会报错
outFileClause.analyze(analyzer, resultExprs);
}
}

  File "<ipython-input-1-1f43be866131>", line 1
    (/, 位置:, fe/fe-core/src/main/java/org/apache/doris/analysis/SelectStmt.java)
     ^
SyntaxError: invalid syntax

以上就是第一步, 对抽象语法树的全部分析过程了, 可以看到, 针对每个语句能支持的语法情况, 对它内部的各个成员对象都进行了分析, 很多成员对象本身也是由内部的多个成员组成的复合型对象, 针对这种情况也会递归的调用其内部的 analyze.

以上就是语义分析的全部内容了, 想了解更多语句的语义分析细节, 可以自行到源码中去检阅.

关于词法, 语法, 语义分析的对比

这里咱们总结一下 词法分析, 语法分析, 语义分析 的区别:

  • 词法分析: 把一个 sql 字符串按照规则生成一系列的单词 (或本身是一个用来吐出单词的扫描器)
  • 语法分析: 把一系列的单词按照句子语法的规划生成一系列的语句
  • 语义分析: 对一个句子的成员细节进行判断, 是否合理

分别举个例子会更好理解一些:

  • “我 一明比 年小 岁年今”: 这句话在词法分析阶段就会返回异常, 主要原因是有些词, 比如”一明比”不是一个合法的单词.
  • “小 我 明年 今年 一岁 比”: 这句话在语法分析阶段返回异常, 虽然每个单词都合法, 但是单词组合起来不是符合任何主谓宾这样的语法.
  • “我 明年 比 今年 小 一岁”: 这句话可以通过词法和语法的检查, 但是在语义分析时会报异常, 主要原因是这句话的语义是不成立的.
  • “我 今年 比 明年 小 一岁”: 这句话可以通过所有的检查.

如果学过编译原理, 会发现这面的这三步都是经典的编译原理的部分, 感兴趣也可以自己去学习一下.

后面的章节, 咱们会讨论语句的重写, 这一步非常关键, 之所以 sql 可以如此普及, 很大一部分的原因还是在于用户的 sql 代码可以随便写, 底层会在保证结果正确的前提之下, 会以性能更优的方式去进行重写和执行.