1
1
# Catalog
2
2
3
- 数据库通过 Catalog 描述内部对象的结构信息。
4
-
5
- 在实际存储任何数据之前,我们需要首先定义好“元数据”。
3
+ 关系型数据库存储** 结构化** 的数据。其中有一部分描述这些结构的“元数据”就被称为 Catalog。
4
+ 在这个任务中我们将学习关系数据库的基本模型,为数据库定义好 Catalog 相关的数据结构,从而为后面插入数据做准备。
6
5
7
6
<!-- toc -->
8
7
9
8
## 背景知识
10
9
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 )
12
122
13
- TODO
123
+ 当前任务的目标就是实现描述它的数据结构。
14
124
15
125
## 任务目标
16
126
17
127
实现 Catalog 相关数据结构,包括:Database,Schema,Table,Column 四个层级。
18
128
19
- 其中每一级都至少支持 插入、删除、查找 三种操作。
129
+ 能够准确描述上面提到的这种表:
20
130
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
+ 首先,我们提供一种可供参考的接口设计:
22
144
23
145
``` rust,no_run
146
+ // 整个数据库的 Catalog 根节点
24
147
pub struct DatabaseCatalog {...}
25
148
26
149
impl DatabaseCatalog {
@@ -29,37 +152,123 @@ impl DatabaseCatalog {
29
152
pub fn del_schema(&self, id: SchemaId) {...}
30
153
}
31
154
32
-
155
+ // 一个 Schema 的 Catalog
33
156
pub struct SchemaCatalog {...}
34
157
35
158
impl SchemaCatalog {
36
159
pub fn id(&self) -> SchemaId {...}
37
160
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 {...}
39
162
pub fn get_table(&self, id: TableId) -> Option<Arc<TableCatalog>> {...}
40
163
pub fn del_table(&self, id: TableId) {...}
41
164
}
42
165
43
-
166
+ // 一个表的 Catalog
44
167
pub struct TableCatalog {...}
45
168
46
169
impl TableCatalog {
47
170
pub fn id(&self) -> TableId {...}
48
171
pub fn name(&self) -> String {...}
49
- pub fn add_column(&self, name: &str) -> ColumnId {...}
50
172
pub fn get_column(&self, id: ColumnId) -> Option<Arc<ColumnCatalog>> {...}
51
- pub fn del_column(&self, id: ColumnId) {...}
52
173
pub fn all_columns(&self) -> Vec<Arc<ColumnCatalog>> {...}
53
174
}
54
175
55
-
176
+ // 一个列的 Catalog
56
177
pub struct ColumnCatalog {...}
57
178
58
179
impl ColumnCatalog {
59
180
pub fn id(&self) -> ColumnId {...}
60
181
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 {...}
61
191
pub fn datatype(&self) -> DataType {...}
62
192
}
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
+ }
63
272
```
64
273
65
- 除此之外,本节没有新增的 SQL 测试。
274
+ 主要的技巧就是这些,代码本身并不复杂。下一步我们就会基于这里定义的数据结构,来实现 ` CREATE TABLE ` 创建表操作了!
0 commit comments