Skip to content

Commit f7597ad

Browse files
docs: add tutorial 01-02 (#3)
* 01-02: write docs of background knowledges * 01-02: finish docs * 01-02: update docs Signed-off-by: Runji Wang <[email protected]>
1 parent 58308b3 commit f7597ad

File tree

3 files changed

+505
-14
lines changed

3 files changed

+505
-14
lines changed

docs/src/01-02-catalog.md

Lines changed: 223 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,149 @@
11
# Catalog
22

3-
数据库通过 Catalog 描述内部对象的结构信息。
4-
5-
在实际存储任何数据之前,我们需要首先定义好“元数据”。
3+
关系型数据库存储**结构化**的数据。其中有一部分描述这些结构的“元数据”就被称为 Catalog。
4+
在这个任务中我们将学习关系数据库的基本模型,为数据库定义好 Catalog 相关的数据结构,从而为后面插入数据做准备。
65

76
<!-- toc -->
87

98
## 背景知识
109

11-
### Catalog
10+
数据库中包含了各种各样的对象。
11+
12+
### 表(Table)
13+
14+
我们最熟悉的对象是 **表(Table)**,它是关系型数据库中存储数据的基本单位。
15+
16+
一张表可以理解为一个**二维数组**,它由纵向的****和横向的****组成。
17+
每一列表示数据的某种**属性(Attribute)**,每一行则表示一条数据**记录(Record)**
18+
19+
例如下面展示了一个简单的记录学生信息的表:
20+
21+
|id| name | age |
22+
|--|------|-----|
23+
| 1| Alice| 18 |
24+
| 2| Bob | 19 |
25+
| 3| Eve | 17 |
26+
27+
在偏理论的语境下,表还有一个更正式的名字叫做 **关系(Relation)**,因为它蕴含了数据属性之间的关系。
28+
其中,每一行又被称为 **元组(Tuple)**
29+
30+
![](img/01-02-relational_database_terms.svg)
31+
32+
表中的每一列都有自己的名字和类型。比如在上面的表中,第一列的名字是 `id`,数据类型是 `INTEGER`(整数)。而第二列的数据类型是 `VARCHAR`(字符串)。
33+
34+
各种数据库都会支持大量的数据类型,其中比较常见的有下面几种:
35+
36+
* `INTEGER`:有符号整数
37+
* `DOUBLE`:浮点数
38+
* `CHAR`:定长字符串
39+
* `VARCHAR`:变长字符串
40+
* `DECIMAL`:十进制定点数,用来做无误差的精确运算(常用在交易系统中表示账户金额)
41+
* `DATE`:日期和时间
42+
43+
部分类型还可以有自己的关联参数,例如:
44+
45+
* `CHAR(n)` 表示长度为 `n` 的字符串
46+
* `VARCHAR(n)` 表示最大长度为 `n` 的字符串
47+
* `DECIMAL(m, d)` 表示有效数字 `m` 位、小数点后 `d` 位的定点数。
48+
49+
表中每一列的数据都只能存储指定的类型,他们所有合法值的集合称为 **数据域(Data Domain)**
50+
在一般情况下,所有数据域都包含一个特殊的 **空值 `NULL`**。也就是说,表中的每个位置默认都可以为空。
51+
52+
除了名字和类型以外,每一列还有一些可选的属性:
53+
54+
* **非空** `NOT NULL`:表示这一列的值不能为 `NULL`
55+
56+
* **唯一** `UNIQUE`:表示这一列的值不能重复。
57+
58+
* **主键** `PRIMARY KEY`:主键能够唯一地表示表中的一行,一般用来作为索引。
59+
60+
因此主键一定是唯一且非空的,并且每个表只能有一个主键。例如在上面的学生表中,`id` 就可以作为主键。
61+
62+
在 SQL 语言中,**数据定义语言(Data Definition Language,DDL)** 可以用来描述一个表的模式信息。
63+
比如上面的学生表就可以通过以下语句来定义:
64+
65+
```sql
66+
CREATE TABLE student (
67+
id INTEGER PRIMARY KEY,
68+
name VARCHAR NOT NULL,
69+
age INTEGER
70+
);
71+
```
72+
73+
我们会在下一个任务中具体实现它:)
74+
75+
### 视图(View)
76+
77+
**视图(View)** 是一种虚拟的表,它表示一条 SQL 语句的查询结果。每次查看视图时,数据库都会执行一遍查询以获取最新的结果。
78+
79+
例如,我们可以用 DDL 创建一个关于学生名字的视图:
80+
81+
```sql
82+
CREATE VIEW student_name AS
83+
SELECT name FROM student;
84+
```
85+
86+
类似地还有另一个概念叫做 **物化视图(Materialized view)**。它将查询结果缓存下来,并在特定时期进行更新。
87+
88+
### 索引(Index)
89+
90+
**索引(Index)** 是对数据库中某一列或多列数据进行排序的结构,用来快速查询表中的记录。
91+
92+
关系型数据库一般会对主键自动创建索引。如果还有其它的列需要大量随机访问或范围查询,就可以手动为它们创建索引来加速。
93+
例如,我们可以用 DDL 创建学生名字的索引:
94+
95+
```sql
96+
CREATE INDEX student_name ON student (name);
97+
```
98+
99+
索引的经典实现是 B+ 树,这是一种适合存储在磁盘上的平衡树。
100+
101+
由于它的实现比较复杂,因此在 RisingLight 中我们暂时不会涉及索引。
102+
<!-- 以后可以加上? -->
103+
104+
### 模式(Schema)
105+
106+
**模式(Schema)** 是数据库对象的集合。上面提到的表、视图、索引等都可以被包含在一个 Schema 当中。
107+
对于有用户权限的数据库系统,在 Schema 上可以指定不同的用户权限。
108+
109+
在 DDL 中我们可以先创建一个 Schema,然后在这个 Schema 内部创建其它对象:
110+
111+
```sql
112+
CREATE SCHEMA school;
113+
CREATE TABLE school.student (...);
114+
```
115+
116+
部分数据库(比如 Postgres)在 Schema 之上还有一个层级 **Database**,一个 Database 可以包含多个 Schema。
117+
不过其它大部分数据库都没有这个额外的层级:在 MySQL 中 `DATABASE``SCHEMA` 是同义词,在 SQLite 或 DuckDB 这类简单的嵌入式数据库中则不存在 `DATABASE` 这个关键词。
118+
119+
总的来看,一个数据库内部对象的层次结构可以表示成这样的一棵树:
120+
121+
![](img/01-02-hierarchy.svg)
12122

13-
TODO
123+
当前任务的目标就是实现描述它的数据结构。
14124

15125
## 任务目标
16126

17127
实现 Catalog 相关数据结构,包括:Database,Schema,Table,Column 四个层级。
18128

19-
其中每一级都至少支持 插入、删除、查找 三种操作。
129+
能够准确描述上面提到的这种表:
20130

21-
一种可供参考的接口设计:
131+
```sql
132+
CREATE TABLE student (
133+
id INTEGER PRIMARY KEY,
134+
name VARCHAR NOT NULL,
135+
age INTEGER
136+
);
137+
```
138+
139+
除此之外,这个任务没有新增的 SQL 测试。
140+
141+
## 整体设计
142+
143+
首先,我们提供一种可供参考的接口设计:
22144

23145
```rust,no_run
146+
// 整个数据库的 Catalog 根节点
24147
pub struct DatabaseCatalog {...}
25148
26149
impl DatabaseCatalog {
@@ -29,37 +152,123 @@ impl DatabaseCatalog {
29152
pub fn del_schema(&self, id: SchemaId) {...}
30153
}
31154
32-
155+
// 一个 Schema 的 Catalog
33156
pub struct SchemaCatalog {...}
34157
35158
impl SchemaCatalog {
36159
pub fn id(&self) -> SchemaId {...}
37160
pub fn name(&self) -> String {...}
38-
pub fn add_table(&self, name: &str) -> TableId {...}
161+
pub fn add_table(&self, name: &str, columns: &[(String, ColumnDesc)]) -> TableId {...}
39162
pub fn get_table(&self, id: TableId) -> Option<Arc<TableCatalog>> {...}
40163
pub fn del_table(&self, id: TableId) {...}
41164
}
42165
43-
166+
// 一个表的 Catalog
44167
pub struct TableCatalog {...}
45168
46169
impl TableCatalog {
47170
pub fn id(&self) -> TableId {...}
48171
pub fn name(&self) -> String {...}
49-
pub fn add_column(&self, name: &str) -> ColumnId {...}
50172
pub fn get_column(&self, id: ColumnId) -> Option<Arc<ColumnCatalog>> {...}
51-
pub fn del_column(&self, id: ColumnId) {...}
52173
pub fn all_columns(&self) -> Vec<Arc<ColumnCatalog>> {...}
53174
}
54175
55-
176+
// 一个列的 Catalog
56177
pub struct ColumnCatalog {...}
57178
58179
impl ColumnCatalog {
59180
pub fn id(&self) -> ColumnId {...}
60181
pub fn name(&self) -> String {...}
182+
pub fn desc(&self) -> ColumnDesc {...}
183+
}
184+
185+
// 一个列的完整属性
186+
pub struct ColumnDesc {...}
187+
188+
impl ColumnDesc {
189+
pub fn is_nullable(&self) -> bool {...}
190+
pub fn is_primary(&self) -> bool {...}
61191
pub fn datatype(&self) -> DataType {...}
62192
}
193+
194+
// 一个列的数据类型,包含了“可空”信息
195+
pub struct DataType {...}
196+
197+
impl DataType {
198+
pub fn is_nullable(&self) -> bool {...}
199+
pub fn kind(&self) -> DataTypeKind {...}
200+
}
201+
202+
// 一个值的数据类型,不考虑空值
203+
// 为了方便,我们可以直接使用 sqlparser 中定义的类型
204+
pub use sqlparser::ast::DataType as DataTypeKind;
205+
```
206+
207+
为了代码结构清晰,可以把它们拆成多个文件:
208+
209+
```
210+
src
211+
├── catalog
212+
│ ├── mod.rs
213+
│ ├── database.rs
214+
│ ├── schema.rs
215+
│ ├── table.rs
216+
│ └── column.rs
217+
├── types.rs
218+
...
219+
```
220+
221+
由于 Catalog 会在数据库中多个地方被读取或修改,因此我们把它们设计为 可被共享访问 的数据结构(Send + Sync)。
222+
这种 struct 的一个特点就是所有方法都标记 `&self` 而不是 `&mut self`,即使对于修改操作也不例外。
223+
这种模式在 Rust 中被称为 **[内部可变性]**
224+
225+
[内部可变性]: https://kaisery.github.io/trpl-zh-cn/ch15-05-interior-mutability.html
226+
227+
实现这种模式通常需要定义两层 struct:内层是普通的可变结构,然后在外面包一层锁。
228+
229+
以顶层的 `DatabaseCatalog` 为例:
230+
231+
```rust,no_run
232+
use std::sync::Mutex;
233+
234+
// 外部 Sync 结构
235+
pub struct DatabaseCatalog {
236+
inner: Mutex<Inner>, // 对于读多写少的场景,也可以使用 RwLock
237+
}
238+
239+
// 内部可变结构
240+
struct Inner {
241+
schemas: HashMap<SchemaId, Arc<SchemaCatalog>>,
242+
// ...
243+
}
244+
```
245+
246+
当我们为外层结构实现方法的时候,需要先 lock 住内部结构,然后去访问 inner:
247+
248+
```rust,no_run
249+
impl DatabaseCatalog {
250+
pub fn get_schema(&self, schema_id: SchemaId) -> Option<Arc<SchemaCatalog>> {
251+
let inner = self.inner.lock().unwrap();
252+
inner.schemas.get(&schema_id).cloned()
253+
}
254+
}
255+
```
256+
257+
如果函数体过于复杂,也可以把它拆成多个 Inner 对象上的小函数:
258+
259+
```rust,no_run
260+
impl DatabaseCatalog {
261+
pub fn add_schema(&self, name: &str) {
262+
let inner = self.inner.lock().unwrap();
263+
let id = inner.next_id();
264+
inner.add_schema(id, name);
265+
}
266+
}
267+
268+
impl Inner {
269+
fn add_schema(&mut self, schema_id: SchemaId, name: &str) {...}
270+
fn next_id(&mut self) -> SchemaId {...}
271+
}
63272
```
64273

65-
除此之外,本节没有新增的 SQL 测试。
274+
主要的技巧就是这些,代码本身并不复杂。下一步我们就会基于这里定义的数据结构,来实现 `CREATE TABLE` 创建表操作了!

docs/src/img/01-02-hierarchy.svg

Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)