当我聊到搜索引擎提升技巧时,我都在关注啥(1) - 信息表示语言设计
作者:雷宇
Databend 优化器负责人
https://github.com/leiysky
这几天和迟先生 (github@skyzh) 聊天时偶然聊到他最近在 CMU 做的 optd 项目(一个基于 Cascades 框架设计的查询优化器库),一起吐槽了各种数据库优化器的设计与实现。这时我突然意识到有些技术上的东西聊起来还是挺有意思的,值得记录下来。
因此我决定开个坑,聊聊查询优化器相关的一切——从算法基础到工程实践,从技术演进到项目落地,甚至是一些行业八卦。
今天先将“IR 设计”相关的内容作为开篇,聊一聊优化器中常见的设计模式,并且讨论其中的设计考量。
什么是查询优化器
在正式开始聊之前,我想先明确一下查询优化器的定义。
在一般语境中,查询优化器特指对查询的执行计划进行优化的一个数据库组件。但是由于各个数据库实现上的不同,查询的优化方式也变得五花八门,比如在 AST 上直接进行 Rewriting,在 AST Lowering 的时候做些转换,在查询执行的时候进行动态的 Rewriting 等等。
为了统一概念,我将从 SQL 解析器 (Parser) 到执行器 (Executor) 之间的所有部分统称为查询优化器 (Query Optimizer)。
什么是 IR
熟悉编译技术的朋友的应该很熟悉 IR 这个词。IR 全称为中间语言 (Intermediate Representation) ,常见于各种编程语言的编译器中,比如 Rust 的 HIR & MIR,LLVM 中的 LLVM IR。IR 被用于结构化表示编程语言,方便编译器进行各种分析与优化。
如果将 SQL 也看成一种编程语言的话,关系型数据库就是运行 SQL 程序的虚拟机,正如 JVM 之于 Java。而查询优化器在其中负责的就是绝大多数的编译工作,将 SQL 语句 (Java code) 翻译成执行计划 (Java bytecode) 交给执行器 (Java runtime) 执行。因此在设计查询优化器时也离不开设计 SQL 的各种 IR。
SQL IR 都长啥样
一般的数据库项目会分为几个模块:解析器 (Parser) ,分析器 (Analyzer/Binder) ,优化器 (Optimizer) ,执行器 (Executor)。SQL 语句会依次经由各组件的处理,最后转化成查询结果。在我们的语境里,优化器囊括了上面提到的分析器和优化器两个模块。
SQL 本身是一种模仿自然语言语法设计的声明式语言,基于关系代数,可以描述集合上的各种运算,并将其映射为对表格数据 (Table) 的查询。
AST
为了方便处理,我们会像绝大多数编译器那样,首先将 SQL 语言解析为 AST(抽象语法树,即 Abstract Syntax Tree)。一个 SQL AST 一般如下图所示:
在 SQL AST 中,我们一般将 node 分为两种:Statement(Stmt) 和 Expression(Expr)。每个 SQL AST 的 root node 总是一个 Statement,其中可能包含一些子句 (Clause) 以及 Expression。Expression 是一种递归的结构,其中包含了各种运算符和函数调用,甚至还有嵌套的 Statement (Subqueries)。
在 SQL 中比较有意思的一点是,对于 SELECT Statement 来说 Statement 与 Expression 的界限会比较模糊。因为 SELECT Statement 本身也是递归的,甚至还需要处理运算符优先级的问题 (UNION/EXCEPT/INTERSECT)。与此同时,在 Statement 中也仅有 SELECT Statement 可以与 Expression 互相递归,因此在设计 SQL AST 时需要注意这点。
关系代数
SQL 语言的理论基础是关系代数,每一条查询语句都有对应的关系代数表示,比如:
由于关系代数的表达式也是一个递归的树状结构,因此很多系统也很自然地将 SQL AST 转换成了类似下图的执行计划,我们将每个 node 称作算子 (Operator),将整个算子树成为查询计划 (Query plan)。
当然众多系统中也有异类,比如身为祖师爷的 IBM 在 Starburst 系统中引入了 Query Graph Model 这种表示方式。这种表示方式相当的抽象,将许多 property 给 hardcode 在了 QGM 中,导致理解起来异常困难,其宣称的可扩展性也令人怀疑。篇幅原因就不在这里展开讲解,有兴趣的话可以去阅读相关论文《Extensible Query Processing in Starburst》以及《Extensible/Rule Based Query Rewrite Optimization in Starburst》。
目前主流的数据库基本都采用了关系代数的表示方式(比如 IBM 的 System R 和 DB2,Oracle 的各种数据库产品,微软的 SQL Server 系列,开源的 PostgreSQL 和 MySQL 8.0),也在此基础上衍生出了丰富的优化框架和执行框架,因此在设计 SQL IR 时使用关系代数的抽象是一种不会错的选择。
利用关系型代数的各种公理和定理我们可以对 SQL IR 进行各种转换,在保证正确性的同时达到优化的效果。关于具体的优化规则和算法会在后续的文章中进行讨论。
最佳工程实践(待定)
有一种认知偏差叫做“知识的诅咒”,意指在与他人交流时预设了对方拥有与自己同等的知识背景。
在软件开发领域这种现象非常常见。写过某类代码的人和没写过某类代码的人在交流时总会变成鸡同鸭讲,即便双方已经具备了相同的理论基础(算法,编程语言,甚至是领域知识)依然无法避免。原因在于软件工程具有相当的灵活性,同样的功能可以有许多种实现方式,而不同的实现方式又会有各自的问题。
为了扫除这种沟通上的障碍,各种技术领域都会发展出自己的一系列 idiom 或者 design pattern,新的项目基于这些实践构建可以省去很多不必要的麻烦。数据库领域也是如此,但是由于比较小众,且商业化程度较高,导致坊间流传的知识非常稀少,工程实践也散落在各种开源项目中。
在这篇文章里我会根据我自己的最佳实践从零构建一套 SQL IR,方便渐进式地分享一些设计考量。
因为个人习惯,我会用 Rust 语言来编写代码。不熟悉 Rust 语言的朋友也不用担心,只需要有一定的 C/C++ 基础就可以看懂 Rust 的代码逻辑。
Hello, world!
当我们学习一门新的编程语言时,第一个接触的程序一般都是 hello world。
fn main() {
println!("Hello, world!");
}
因此我们也先从 SQL 的 hello world 开始构建我们的 IR。
create table t(a int);
select * from t;
这条 SQL 语句翻译成关系代数非常简单,我们将其记为 Get(t),意为返回集合 t 中的所有数据。要表示这样的查询我们可以定义一个简单的 struct 类型。
pub struct Get {
pub table: String,
}
fn plan() -> Get {
// select * from t;
Get {
table: "t".to_string(),
}
}
这样简单的一个 SQL IR 就完成了,有了 Get 之后我们就能表示所有类似 select * from xxx 的查询了,是不是很简单?
Select & Project
接下来我们可以为这个 IR 添加更多的功能,支持更多的 SQL 子句。比如:
create table t(a int, b int);
select a from t where a = 1;
这条 SQL 查询翻译成关系代数可以记为 Project(Select(Get(t), a = 1), a),Select 算子可以根据提供的谓词对数据进行过滤,Project 算子可以对集合进行裁剪以获得需要的 attribute。为了表示这样的查询我们需要添加更多的 struct 定义。
pub struct Get {
pub table: String,
}
pub struct Select {
pub get: Get,
pub predicate: String,
}
pub struct Project {
pub select: Select,
pub project: String,
}
fn plan() -> Project {
// select a from t where a = 1;
Project {
select: Select {
get: Get {
table: "t".to_string(),
},
predicate: "a = 1".to_string(),
},
project: "a".to_string(),
}
}
来到这里我们就面临了几个问题:按照关系代数的定理,Project 是不是可以作为 Select 的 child?Select 对于一条 SQL 查询来说是可选的,代码上该如何体现这点?
为了解决这些问题,我们可以引入一些动态分发的特性。在 C++/Java 中一般会使用继承来表示 Opeartor,比如:
class Operator {};
class Get : public Operator {};
class Select : public Operator {
Operator* _child;
};
class Project : public Operator {
Operator* _child;
};
在 Rust 中,我们有一个更方便的选择,可以同时享受静态类型和动态分发的优点,那就是 enum。Rust 的 enum 是一种 ADT(Algebraic Data Type),也被称作 tagged union,可以非常方便地表示我们的 operators:
pub enum Operator {
Get {
table: String,
},
Select {
child: Box<Self>,
predicate: String,
},
Project {
child: Box<Self>,
projects: String,
},
}
fn plan() -> Operator {
// select a from t where a = 1;
Operator::Project {
child: Box::new(Operator::Select {
child: Box::new(Operator::Get {
table: "t".to_string(),
}),
predicate: "a = 1".to_string(),
}),
project: "a".to_string(),
}
}
由此一来我们就可以*表示各种形状的算子树了,IR 的设计开始步入正轨。
Scalar expression
虽然我们已经引入了 Select 和 Project 的算子,但是对于 Select predicate 和 Project expression 还是以字符串的形式存在的,不能满足分析和优化的需求。因此我们需要为这些 expression 也设计一种 IR。
回想一下,SQL 字符串在经过 Parser 处理后就被转换成了 AST,而其中的表达式会变成 Expr node,大概长这样:
pub enum Expr {
ColumnRef(ColumnRef),
Literal(Literal),
Function(Function),
BinaryOp(BinaryOp),
UnaryOp(UnaryOp),
Subquery(SelectStmt),
}
expression 本身是一种递归的结构,AST 的 Expr node 也是一种递归结构,我们是否可以偷懒直接使用 Expr node 作为我们的 SQL IR 的一部分呢?我们可以先试试。
使用 Expr 替换 String 后,我们可以得到:
pub enum Operator {
Get {
table: String,
},
Select {
child: Box<Self>,
predicate: Expr,
},
Project {
child: Box<Self>,
projects: Vec<Expr>,
},
}
接下来给定一条 SQL,让我们来试试常用的一些分析,看看好不好使:
select a from t
where exists (select * from t1 where t.a = t1.a)
- Q: Project 中的 Expr 依赖了哪些 table 的哪些 columns?A: 使用了一个叫做 a 的 column,但我不知道它是哪个 table 的,或许根本就不存在这个 column
- Q: Project 中的 Expr 的返回类型是什么?A: 不知道,Expr 中没有包含任何类型信息
- Q: Select 中的 subquery 是 correlated subquery 吗?A: 不知道,Expr 中的 subquery 只是一个未处理过的 AST
Ok,看起来 Expr 并没有我们想象中的那么好用。为了进行上面的这些分析,我们需要设计一套信息更加丰富的 IR。为了与 Expr 进行区分,我们将其命名为 ScalarExpr。
将以上的分析归纳一下,我们对 ScalarExpr 的要求是:
- 所有的 identifier 都要 resolve 成 fully qualified name
- 类型信息需要被注入,并且需要经过 type check
- 所有的 subquery 都要被转换成 SQL IR 的形式
结合以上需求,再加上一些 desugar,ScalarExpr 大概长这样:
pub enum ScalarExpr {
ColumnRef(Vec<Identifier>, Type),
Literal(Value, Type),
Function(Signature, Vec<Self>),
Subquery(Quantifier, Box<Operator>),
}
如此一来,表达式的 IR 设计也成型了,让我们把整套 SQL IR 整合起来吧。
The IR
经过以上的设计,我们拥有了:
- 能够灵活表达各种 SQL 查询的算子树结构 Operator
- 能够提供丰富语义信息的 ScalarExpr
尽管还缺少一些关键的算子,比如 Join, Union, Aggregate 等。但是由于整体框架已经十分清晰,我们可以照葫芦画瓢地把它们也加上。
整合之后我们就有了一套相当完美的 SQL IR:
pub enum ScalarExpr {
ColumnRef(Vec<Identifier>, Type),
Literal(Value, Type),
Function(Signature, Vec<Self>),
Subquery(Quantifier, Box<Operator>),
}
pub enum Operator {
Get {
table: String,
},
Select {
child: Box<Self>,
predicate: ScalarExpr,
},
Project {
child: Box<Self>,
projects: Vec<ScalarExpr>,
},
Join {
kind: JoinKind,
condition: ScalarExpr,
left: Box<Self>,
right: Box<Self>,
},
UnionAll {
left: Box<Self>,
right: Box<Self>,
},
Aggregate {
group_by: Vec<ScalarExpr>,
aggr_exprs: Vec<ScalarExpr>,
child: Box<Self>,
},
}
因为过于完美,我决定给这个 IR 起个霸气的名字——The IR。
Property derivation
当我们想要对 IR 进行分析和优化时,我们总是需要获取一些 IR 的 property。我们可以通过编写 analyzer 遍历整个 IR 来计算出这些 property,但是这样需要耗费大量精力维护 IR 所处上下文的状态。
幸运的是 SQL 作为一个声明式的查询语言数据流相当简单,我们可以利用其特性计算 property。
The IR 中的数据流向和 operator 之间的父子关系紧密相关,整体呈现为一个有向无环图 (DAG),所有的数据都从子节点流向父节点。
在这种特性下,计算某个 The IR 节点的 property 要做的事情很简单,只需递归计算其每个子节点的 property,再根据这些 property 计算出其本身的 property,我们称这个过程为 property derivation。
pub struct Property;
fn derive_property(op: &Operator) -> Property {
// Calculate the properties of the children operators.
let children_property: Vec<Property> = op
.children()
.map(derive_property)
.collect();
// Calculate property with the children properties.
op.calculate_property(&children_property)
}
在 SQL 优化中,常用的 property 可以分为两类,分别是描述数据集特征的 relational/logical property 和描述数据物理特征的 physical property。
常见的 relational property 有:
- 数据集中包含的 attributes/columns 信息
- 数据集的基数 (cardinality),表示数据集中的 record 数量
- 统计信息 (statistics),表示 attributes 的数据分布
- 数据约束 (constraints),表示 attributes 的约束,比如 NOT NULL
- 函数依赖 (functional dependency),表示 attributes 之间的函数依赖关系
常见的 physical property 有:
- 有序性 (order)
- 并行度 (DOP)
- 数据分布 (distribution)
- 数据分区 (partition)
结合关系代数的性质,我们可以描述 property 种类之间的区别。
假设有关系 $R$ 和 $S$:$R$ 的 relational property 为 $RP_R$,physical property 为 $PP_R$;$S$ 的 relational property 为 $RP_S$,physical property 为 $PP_S$。
我们可以得到:
$$ \forall R, S: R = S \implies RP_R = RP_S $$
不难看出两个 relation 的等价关系可以决定 relational property 的等价关系,但是 physical property 的等价关系却不受 relation 的等价关系影响。
关于 property 与具体的查询优化算法结合的内容将在后续的文章里展开讨论。
有了 property derivation 之后,我们就能利用关系代数的定理在保证正确性的情况下对 The IR 进行优化了。
那么接下来的问题就是,property 该长啥样?
Relational properties
在 relational property 中最重要的部分莫过于 attributes 的表示方式。朴素的关系代数中,每一个 relation 都是由 tuples 组成的 set ,tuple 中的每个 attribute 都有自己的 unique name,我们很自然地可以想到直接将 tuple schema 作为 attributes 的表示方式。
我们先回忆一下一个 table 是如何创建的。
create table t(a int);
在 SQL 中我们使用 DDL(Data Definition Language) 来创建和管理各种 table。在创建 table 时我们需要为其指定 table schema,里面包含了 table 中每个 column 的具体定义,对应了关系代数中的 attributes。table schema 的结构大概会长这样:
pub struct TableSchema {
pub name: String,
pub columns: Vec<ColumnDefinition>
}
pub struct ColumnDefinition {
pub name: String,
pub column_type: Type,
pub not_null: bool,
}
既然 ColumnDefinition 与 attribute 是一一对应的关系,我们可不可以直接用 ColumnDefinition 来表示 attribute 的 property?
我们可以先来试试,在 The IR 中加上对于 attributes 的支持。
fn derive_attributes(op: &Operator) -> Vec<ColumnDefinition> {
// Calculate the attributes of the children operators.
let children_attributes: Vec<Vec<ColumnDefinition>> =
op.children().iter().map(derive_attributes).collect();
// Calculate attributes with the children attributes.
op.calculate_attributes(&children_attributes)
}
我们首先需要对 The IR 做一些修改,为 Get 算子加上 table schema 信息。
pub enum Operator {
Get {
table: String,
schema: Vec<ColumnDefinition>,
},
// Nothing changed for other variants
}
然后我们为 Operator 实现 attributes derivation。
impl Operator {
fn calculate_attributes(&self, children: &[Vec<ColumnDefinition>]) -> Vec<ColumnDefinition> {
match self {
Operator::Get { schema, .. } => {
let attributes = schema.clone();
attributes
}
Operator::Select { .. } => children[0].clone(),
Operator::Join { .. } => {
let mut attributes = children[0].clone();
attributes.extend(children[1].clone());
attributes
}
Operator::UnionAll { .. } => children[0].clone(),
Operator::Project { .. } => todo!(),
Operator::Aggregate { .. } => todo!(),
}
}
}
大部分的算子实现还是很顺利的,但是可以看到 Project 和 Aggregate 被标成了 todo。我们这时会发现,Project 和 Aggregate 没法直接利用 children attributes 生成他们自己的 attributes。再回到关系代数上,Project 的作用是裁剪 tuple 的形状,抑或是修改 attribute 的 name, SELECT a + 1 AS b FROM t
这种 SQL 根本无法表达为朴素的 Project;至于 Aggregate,朴素的关系代数中根本没有这个运算,这是对关系代数的扩展内容。
关系代数理论不存在了!
但是尽管如此,工程还是得继续进行,我们需要引入一些“村规”来扩展关系代数的定义。我们在此给出 The IR 中的 Project 和 Aggregate 的形式化定义:
- $Project(R, (f_1, …, f_n)) → {(x_1, …, x_n)}$ 表示将关系 $R$ 中的 attributes 作为输入,输出 $f_1$ 到 $f_n$ 的 n 个函数映射组成的 tuple
- $Aggregate(R, (k_1, …, k_m), (f_1, …, f_n)) → {(x_1, …, x_m, x_m+1, …, x_m+n)}$ 表示将关系 R 中的 tuples 按照 $k_1$ 到 $k_m$ 的 m 个 attributes 进行分组,并且对每个分组执行 $f_1$ 到 $f_n$ 的 n 个函数映射,最终输出分组后的 tuples
这个村规最大的改变是引入了 derived column。对于 SQL 中直接来自于 table 的 column,我们称之为 base table column;对于通过 Project/Aggregate 计算出的 column,我们称之为 derived column。在引入 derived column 概念之前我们可以保证所有的数据来源最终都会指向 Get 算子,但是引入之后这个约定就被打破了,出现了类似编程语言中作用域 (scope) 的概念,我们在进行优化时需要更加的注意。
有了村规后我们就可以给 Project 和 Aggregate 也实现 attributes derivation,但与此同时我们也需要对 The IR 的结构做一些修改:
pub enum Operator {
Project {
child: Box<Self>,
projects: Vec<(ScalarExpr, String)>,
},
// Others
}
impl Operator {
fn calculate_attributes(&self, children: &[Vec<ColumnDefinition>]) -> Vec<ColumnDefinition> {
match self {
Operator::Project { projects, .. } => {
let attributes: Vec<ColumnDefinition> = projects
.iter()
.map(|(expr, alias)| ColumnDefinition {
name: alias.clone(),
column_type: expr.column_type(),
not_null: expr.nullable(),
})
.collect();
attributes
}
Operator::Aggregate {
group_by,
aggr_exprs,
..
} => {
let mut attributes: Vec<ColumnDefinition> = group_by
.iter()
.map(|expr| ColumnDefinition {
name: expr.name(),
column_type: expr.column_type(),
not_null: expr.nullable(),
})
.collect();
attributes.extend(aggr_exprs.iter().map(|expr| ColumnDefinition {
name: expr.name(),
column_type: expr.column_type(),
not_null: expr.nullable(),
}));
attributes
}
// Others
}
}
}
这样一来对于所有的算子我们都可以计算 attributes property 了,赶紧来试用一下吧。
先来看看 SQL 中最常见也最有效的优化——谓词下推。这个优化可以通过将 Select 算子下推到其他算子内以减少其他算子的计算量,同时还可以保证整个查询的结果不会改变,非常的简洁优雅。
我们来尝试在 The IR 上实现这个优化。思路上非常简单,根据关系代数定理直接调换 Select 与 Project 的位置即可。但是由于我们引入了 derived column,我们必须检查 Select 中的 predicate 是否依赖了 Project 生成的 column。
fn push_down_select_project(op: &Operator) -> Option<Operator> {
match op {
Operator::Select {
child: project @ box Operator::Project { child, projects },
predicate,
} => {
let project_attributes: Vec<ColumnDefinition> = derive_attributes(&project);
let predicate_used_columns: Vec<String> = predicate.used_columns();
// Check if the predicate uses any column from the project.
let used_derived_columns = predicate_used_columns.iter().any(|used_column| {
project_attributes
.iter()
.any(|attr| attr.name == *used_column)
});
if used_derived_columns {
None
} else {
Some(Operator::Project {
child: Box::new(Operator::Select {
child: child.clone(),
predicate: predicate.clone(),
}),
projects: projects.clone(),
})
}
}
_ => None,
}
}
看起来已经基本可用了,可喜可贺。我们再来试试更复杂的例子,比如试试有 Join 的 SQL:
因为 Join 不像 Project 那样会产生额外的 derived column,因此检查的逻辑会相对简单一些。我们先实现一个尝试将 Select 下推到 Join 的 left child 的优化:
fn push_down_select_join_left(op: &Operator) -> Option<Operator> {
match op {
Operator::Select {
child: join @ box Operator::Join { left, right, .. },
predicate,
} => {
let left_attributes: Vec<ColumnDefinition> = derive_attributes(&left);
let predicate_used_columns: Vec<String> = predicate.used_columns();
// Check if the predicate only uses column from left.
let only_left = predicate_used_columns
.iter()
.all(|used_column| left_attributes.iter().any(|attr| attr.name == *used_column));
if only_left {
Some(Operator::Join {
left: Box::new(Operator::Select {
child: left.clone(),
predicate: predicate.clone(),
}),
right: right.clone(),
..join.clone()
})
} else {
None
}
}
_ => None,
}
}
一切看起来都很美好,但是魔鬼往往藏在细节里。我们来看这个例子在 PostgreSQL 中的输出:
leiysky=# create table t(a int);
CREATE TABLE
leiysky=# create table t1(a int);
CREATE TABLE
leiysky=# insert into t values(1);
INSERT 0 1
leiysky=# insert into t1 values(1);
INSERT 0 1
leiysky=# select * from t, t1 where t.a = 1;
a | a
---+---
1 | 1
(1 row)
最后返回的结果有两个叫做 a 的 attributes。在 The IR 目前的实现中,我们无法知道这个 Select 该下推到哪边。因为当我们检查依赖了 a 的 predicate 能被下推到哪一侧时,我们会发现 Join 的两侧都可以满足。虽然同一个 table 中不允许存在多个具有相同 name 的 columns,但是不同 table 之间并没有这样的限制。
PostreSQL 作为对 ANSI SQL 支持度最高的开源数据库产品,自然也能很好地处理这种问题。通过 EXPLAIN 语句我们可以看到它将 Select 下推到了正确的地方:
leiysky=# explain(verbose) select * from t, t1 where t.a = 1;
QUERY PLAN
----------------------------------------------------------------------
Nested Loop (cost=0.00..491.78 rows=33150 width=8)
Output: t.a, t1.a
-> Seq Scan on public.t1 (cost=0.00..35.50 rows=2550 width=4)
Output: t1.a
-> Materialize (cost=0.00..41.94 rows=13 width=4)
Output: t.a
-> Seq Scan on public.t (cost=0.00..41.88 rows=13 width=4)
Output: t.a
Filter: (t.a = 1)
(9 rows)
The IR 作为完美的 SQL IR,也必须有自己的解决方案。我们仔细观察这条查询的话,会发现 Select 的谓词是用 qualified name 表示的,假如使用 unqualified name 的话 PostgreSQL 会抛出这样的报错:
leiysky=# select * from t, t1 where a = 1;
ERROR: column reference "a" is ambiguous
LINE 1: select * from t, t1 where a = 1;
因为在当前的上下文中,a 是存在歧义的,但是 t.a 就不存在歧义。我们来试试用 qualified name 表示 attribute property 来解决这个问题,为此我们需要改动一些代码:
pub struct QualifiedName(pub Vec<String>);
impl QualifiedName {
/// If the current name can be used to refer another name
pub fn can_refer(&self, other: &Self) -> bool {
self.0.len() <= other.0.len()
&& self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b)
}
}
pub struct ColumnDefinition {
/// Use qualified name
pub name: QualifiedName,
pub column_type: Type,
pub not_null: bool,
}
fn resolve_attribute(
attributes: &[ColumnDefinition],
name: &QualifiedName,
) -> Option<ColumnDefinition> {
let candidates: Vec<ColumnDefinition> = attributes
.iter()
.filter(|attr| attr.name.can_refer(name))
.collect();
if candidates.len() == 1 {
Some(candidates[0].clone())
} else if candidates.len() > 1 {
panic!("Watch out, ambiguous reference found!")
}else {
None
}
}
fn push_down_select_join_left(op: &Operator) -> Option<Operator> {
match op {
Operator::Select {
child: join @ box Operator::Join { left, right, .. },
predicate,
} => {
let left_attributes: Vec<ColumnDefinition> = derive_attributes(&left);
let predicate_used_columns: Vec<QualifiedName> = predicate.used_columns();
// Check if the predicate only uses column from left.
let only_left = predicate_used_columns
.iter()
.all(|used_column| resolve_attribute(&left_attributes, used_column).is_some());
if only_left {
Some(Operator::Join {
left: Box::new(Operator::Select {
child: left.clone(),
predicate: predicate.clone(),
}),
right: right.clone(),
..join.clone()
})
} else {
None
}
}
_ => None,
}
}
这样一来上面的问题就解决了,我们有了处理复杂 attribute 引用的能力,但是距离一劳永逸仍有很大的距离。我们再来看一个例子:
leiysky=# select * from (select * from t1) as t, t1 where t.a = 1;
a | a
---+---
1 | 1
(1 row)
虽然 SQL 中不允许在同一个 FROM clause 中使用多个同样的 table name,但是我们可以使用 inlined view 或者 CTE 绕过这点。按照我们现在的实现,在处理 t.a = 1 时我们拿到的 attributes 里面有两个 t1.a 而没有 t.a,这是因为我们没有处理 inlined view 的 alias。为此,我们需要增加一个 Project 专门用于给 attributes 做 renaming。
那么问题又来了,由于我们仅仅是为一些 columns 做了 renaming 就将它们当成了 derived columns 来处理,为我们的 Select 下推徒增了很多负担。为此我们必须修改 The IR 的定义和各种相关代码,服务于 name 的 mapping:
pub enum Operator {
Project {
child: Box<Self>,
// (Expression, Source name, Alias)
projects: Vec<(ScalarExpr, QualifiedName, QualifiedName)>,
},
// Others
}
这些问题通过稍微多写点代码还能解决,但是看看接下来的这个例子,我相信大部分人会像我一样直接抓狂:
leiysky=# select a from t natural join t1;
a
---
1
(1 row)
leiysky=# select t.a from t natural join t1;
a
---
1
(1 row)
leiysky=# select t1.a from t natural join t1;
a
---
1
(1 row)
leiysky=# select * from t natural join t1;
a
---
1
(1 row)
leiysky=# select a from t join t1 on t.a = t1.a;
ERROR: column reference "a" is ambiguous
LINE 1: select a from t join t1 on t.a = t1.a;
我们当然可以为代码中再加上各种奇奇怪怪的限制,开各种难以维护的洞来维护这种 property,并且在进行优化的同时保证这些 property 的正确性。但是对于懒惰的程序员们来说,寻找一种更简单的设计才是更好的选择。
欢迎来到深水区。
The IR made simple
最开始的 The IR 是非常简洁而优雅的,但是为了实现更多的功能,支持更复杂的需求,我们为其添加了许多我们不想关注的信息。总的来说,理想状态的 The IR 应该是:
- 拥有简洁的代数结构
- Operator 节点之间完全独立
- 不用处理 names(仅用于 debug 和展示)
让我们在回过头来思考一下,The IR 真的离不开 name 吗?我们最开始使用 name 来表示 attribute 主要是出于直觉,复用了 table schema。但是 name 中融入了许多无用的信息,对我们的优化毫无帮助,就像编程语言中的各种 symbol name 一样,到了程序运行时都会变成内存地址和寄存器编号。
没有 name 便无法区分 attributes,name 岂是如此不便之物?
归根结底,我们需要的是为每个 attribute 附上一个 unique id,无论是 integer 还是 string,总之我们的唯一目的就是用 id 来区分和引用 attribute。所有的 name resolution 统统丢进 AST lowering,我只想要 attribute id!
在进行了重新设计之后我们改变了 attribute 的表示方式,同时也更改了一些 The IR 的定义。默认情况下,我们使用 int64 类型作为 attribute id。
pub type Id = i64;
pub struct Attribute {
pub id: Id,
pub column_type: Type,
pub nullable: Type,
}
pub enum ScalarExpr {
ColumnRef(Id),
// Others
}
id 的设计一般离不开对应的上下文 (context),在 SQL IR 中 attribute id 的常见设计方式主要可以分为两类:
- 一种是基于我们之前用到的 tuple attribute 的抽象,将 attribute 在 tuple 中的 index 作为 attribute id,我们将这种 id 称为 local id。这种设计的特点是逻辑上的同一个 attribute 的 id 会随着其所处的 operator 的不同而发生改变。这种 id 的好处是可以从 operator tree 中推理出来,不需要靠外部的状态进行维护。但是缺点就是在对 operator 进行转换时需要频繁的对 id 进行 remapping。
- 另一种是通过维护一个全局的 id generator,给 SQL IR 中的所有 attribute 赋予一个 unique id,我们将这种 id 称为 global id。这种设计的优点是将 attribute 与 tuple schema 进行了解耦,可以使用
HashMap<Id, Attribute>
这种无序集合结构表示 attributes。同时也可以利用集合运算帮助 property derivation,降低维护复杂度。但是缺点是使用 global id 的 operator tree 需要依赖外部状态,无法独自存在。
使用这两种不同的设计会对优化器的具体实现造成非常大的影响。
比如说对于这个优化:
在有合适的索引可用时,通过这个优化可以避免 full table scan 从而提升性能。
如果使用 local id 的设计,实现这个优化会非常简单,只需要将整个 operator tree 复制一份,最后使用 UnionAll 连接起来即可。
但是如果使用 global id 的设计,这就是一个 non-trivial 的操作,甚至可以说是十分痛苦。为了区分不同的 attribute,我们必须在复制 operator tree 的同时为所有的 attribute 生成新的 id,再将所有引用这些 attribute 的地方替换成新的 id,这在查询较为复杂时会造成很多麻烦。
再比如说进行 join order 优化时:
根据 Join 算子的交换律,我们可以合法交换 Join 的左右 child。使用 global id 的设计时,因为 attributes 可以表示为无序集合,因此这个操作对于 property derivation 毫无影响。但是使用 local id 的设计时这个操作就会让人痛苦不堪。
除去优化相关的部分,在对于 correlated subquery 的表示上他们的差异也非常巨大。correlated subquery 是一种特殊的 subquery,它可以访问自己的 scope 以外的 attribute,对于这类特殊的 attribute 访问我们称之为 outer reference。
许多编程语言中也支持类似的操作,可以从函数中访问定义在函数内没有定义的变量,通过与特定环境 (environment) 进行绑定 (bind) 后才能执行。这种特殊的函数叫做闭包 (Closure)。
fn main() {
let a = 1;
let f = || {
let b = a; // a is captured from outside
println!("{}", b);
}; // f is a closure
f(); // stdout: 1
}
使用 global id 的设计可以通过 attribute property 计算出 subquery 是否为 correlated。但是使用 local id 的设计时我们一般会在 scalar expression 的 ColumnRef
中额外维护一个 scope id,实现起来非常麻烦。
correlated subquery 是一个非常大的话题,我们也许会在后续的文章中聊到。
由此可见两种设计各有优缺点,在工程实践中我们要结合自己的需求选择适合自己的设计。就我个人而言,global id 是一种更好的设计,因为它在绝大多数的情况下都能很轻松地解决问题。
使用 global id 进行改造后,The IR 的代码可以得到大幅的简化:
pub type Id = i64;
pub struct Context {
pub id_gen: Id,
}
pub struct Attribute {
pub id: Id,
pub column_type: Type,
pub nullable: Type,
}
pub type AttributeSet = HashMap<Id, Attribute>;
pub enum ScalarExpr {
ColumnRef(Id),
Literal(Value, Type),
Function(Signature, Vec<Self>),
Subquery(Quantifier, Box<Operator>),
}
pub enum Operator {
Get {
table: String,
output_columns: AttributeSet,
},
Select {
child: Box<Self>,
predicate: ScalarExpr,
},
Project {
child: Box<Self>,
projects: Vec<(ScalarExpr, Id)>,
},
Join {
kind: JoinKind,
condition: ScalarExpr,
left: Box<Self>,
right: Box<Self>,
},
UnionAll {
left: Box<Self>,
right: Box<Self>,
},
Aggregate {
group_by: Vec<ScalarExpr>,
aggr_exprs: Vec<(ScalarExpr, Id)>,
child: Box<Self>,
},
}
在把复杂度转嫁给 AST lowerer 之后,我们可以自信地说,The IR 已经是个 production ready 的 SQL IR 了。它可以支持所有的 SQL 运算和常用的优化,拥有易用的 API,同时也非常易于理解。更重要的是,没有人比这篇文章的读者更懂 The IR,任何读者都可以根据自己的需求轻松扩展 The IR。
后记
终于到了这篇文章的尾声。
作为系列的开篇,我在这篇文章里只是简单地聊了聊 SQL IR 设计中的一些关注点,并没有深入讨论各种算法的细节。
但是分享 IR 的设计过程是一件很有意思的事情。很多 IR 就像路边的一棵长得歪歪扭扭的树,第一次路过的人都不知道它为什么会长成那样,只有从小住在这里的人才知道在这棵树很小的时候人们总是喜欢在它的树枝上挂腊肉。这件小事是导致最终结果的重要原因,但是它过于微不足道,导致知道的人从来不会主动分享——当然现实中往往也没人关心背后的原因。
数据库开发是一个小众领域,同时又有很多工程化的实践经验。这些经验在坊间少有流传,我不希望它像美国的登月技术一样随着时代的变化而消失,这也是我想到写这个系列文章的初衷。
在下一篇文章里我会分享关于优化器架构的相关内容,敬请期待。
关于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
???????? Databend Cloud:https://databend.cn
???? Databend 文档:https://docs.databend.cn/
???? Wechat:Databend
✨ GitHub:https://github.com/datafuselabs/databend
推荐阅读
-
当我聊到搜索引擎提升技巧时,我都在关注啥(1) - 信息表示语言设计
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,