数据库原理这门课,我念书的时候觉得它是最枯燥的一门课——满页的范式、关系代数、封锁协议,期末考试前背得昏天黑地。后来真正写了业务系统、跟过数据迁移、排查过线上慢查询,才发现当年背的那些知识点,其实就是数据库系统设计最核心的骨架。面试新人时我特别喜欢问事务隔离级别和索引结构,原因很简单:这两个点能看出一个人是死记硬背还是真的理解了数据库在干什么。
这篇文章我想把数据库原理里那些最核心的知识点串起来讲一遍。不是教科书式的逐条罗列,而是按我自己的理解,把"数据怎么组织、怎么查、怎么保证不出错"这条主线理清楚。不管是准备考试、复习面试,还是工作中想补一补底层功底,这篇内容应该都能帮你省下不少翻书的时间。
1. 从数据模型到关系代数:数据库的"地基"逻辑
1.1 为什么最终是关系模型胜出
数据库发展早期,数据模型主要分成三类:层次模型、网状模型和关系模型。层次模型把数据组织成树状结构,像公司组织架构,每个节点只有一个父节点;网状模型允许节点有多个父节点,用指针把记录连成网状。这两个模型在当时都能解决数据存储问题,但它们最大的毛病是——访问路径是事先锁死的。你想查一条数据,得沿着指针一条条走,应用代码和存储结构绑得死死的,数据结构一改,应用基本就得重写。
关系模型完全不同。它把数据全部抽象成一张张二维表,表与表之间靠公共属性(字段)建立联系。你不需要关心数据在磁盘上到底是怎么存的,只需要用一套统一的操作语言去查。这个"逻辑数据独立于物理存储"的思想,是关系模型最值钱的地方。后来商业数据库、开源数据库全面倒向关系模型,不是因为关系模型效率最高,而是它把数据访问从"面向路径"变成"面向集合",开发效率高了一整个量级。
关系模型里有几个基本概念需要拎清楚:关系(表)、元组(行)、属性(列)、域(属性取值的合法范围)、主键(唯一标识一行)、外键(引用别的表的主键)。考试喜欢考"关系"和"表"的差异:关系是数学概念,要求每个属性不可分,行的顺序、列的顺序都无关紧要;表是实现概念,允许有重复行、字段可以再拆分。实际建表时,我们尽量让表符合关系定义的约束,这样后续的运算才不会出幺蛾子。
1.2 关系代数:SQL背后的数学肌肉
很多人觉得关系代数抽象、考试过去就算了,其实它才是SQL的灵魂。SQL里一句SELECT ... FROM ... WHERE ...,翻译成关系代数就是先σ(选择)把符合条件的行筛出来,再π(投影)把需要的列切出来,最后可能还要⋈(连接)把多张表拼起来。理解了这个映射关系,写复杂SQL时你会突然有底感——知道优化器大概想干什么、为什么某些写法能改写成更高效的形式。
关系代数的主要运算就这么几个:
- 选择(σ):按条件筛行,对应WHERE
- 投影(π):按指定列组成新表,对应SELECT的字段列表
- 连接(⋈):把两个表按条件拼接,对应JOIN
- 除法(÷):表示"包含所有"的语义,比如找出选修了全部课程的学生,这类需求活得比较拧巴,但面试里偶尔会有
- 并、交、差:集合操作,对应UNION、INTERSECT、EXCEPT
我当年学除法运算时死活绕不过来,后来想通了:除法的结果就是被除关系里那些与除关系所有元组都能匹配上的部分。举个例子,A表是"学生-课程"选课记录,B表是"全部课程"列表,A除以B的结果就是选了全部课程的学生。本质上就是"取那些覆盖了B集合的A中的分组"。
1.3 完整性约束:数据不敢乱来的底气
关系模型的另一个核心是三类完整性约束。实体完整性要求主键不能为空,而且不能重复——这个好理解,每行要能被唯一定位。参照完整性要求外键要么为空,要么等于被引用表里真实存在的主键值,这一条保证了表与表之间的关系不会张冠李戴。第三类是用户自定义完整性,比如年龄不能为负、金额不能小于零,这些由业务规则来约束。
实际开发里,我刚带团队时经常看到有人对外键不以为然,觉得业务层校验就够了。真等线上数据出现孤儿记录,join出来莫名多出几行,或者删了父表数据子表还在那孤零零挂着的时候,就知道数据库层面的约束有多重要了。约束的本质是让数据库自己守住底线,而不是依赖每个开发都记得在代码里写校验。
2. 三层模式与两级独立性:数据库的"隔离带"设计
2.1 外模式、模式、内模式分别管什么
数据库系统设计里最容易被忽略、但极具工程价值的思想,就是三级模式结构:外模式(External Schema)、模式(Conceptual Schema)、内模式(Internal Schema)。
用大白话来说:
- 内模式管物理存储——数据文件在磁盘上怎么排、索引怎么建、压缩和加密怎么做;
- 模式管逻辑结构——有多少张表、每张表有哪些字段、表之间的关系如何,它是数据库的"全局逻辑视图";
- 外模式管用户视角——每个用户或应用看到的那部分数据,可以是一张表的几个字段,也可以是多个表拼出来的视图。
为什么要分这么多层?直接在表上操作不行吗?答案是不行。分层最大的价值是隔离变化:底层存储换了引擎、加了索引,上层应用不用跟着改;上层应用的需求变了、视图调整了,底层存储也不需要动。这就是数据库的"中间层思维",和软件工程里的"封装"是一个道理。
2.2 两级独立性的现实意义
两级独立性分别是物理独立性和逻辑独立性。
物理独立性指内模式改了(比如换了存储引擎、重建了索引、调整了文件组织方式),模式不用变,应用更不用变。我实际经历过一次存储迁移,把数据库从一个存储方案迁到另一个,上层应用一行业务代码没动,只改了下层的存储配置,这就得益于物理独立性。
逻辑独立性指模式改了(比如新增字段、移除字段、调整表结构),外模式和应用尽量不受影响。不过这一级独立性在关系型数据库里实现得不如物理独立性彻底。你给一张核心表加字段,往app里select * 的地方还是可能出问题;你把某张表拆了,视图不重写,old查询一样废掉。所以实际开发中,逻辑独立性更多靠工程规范来兜底——比如新增字段用默认值、禁止在业务代码里select *。
2.3 视图与用户权限
外模式在实现层面的一个重要落地就是视图(View)。视图是一张虚拟表,它不存数据,只是把一条SQL查询的语义固定下来。我常用的场景有三类:第一类是权限控制,只给某角色开放视图里的几个字段,底层表的敏感字段不暴露;第二类是简化复杂查询,把多次join的结果封装成视图,业务代码里直接查视图;第三类是屏蔽表结构变化,表拆分了,视图帮你拼成原来的样子。
原理上视图分两种:简单视图(来自单表的行列子集)可以做更新操作,如果含聚合、distinct、连表这种,基本就只能查询了。面试常问"视图能不能更新",标准回答是:理论上有条件能,实际上很少拿视图做写操作。我自己的习惯是视图一律只读,写入就走底层表,视图想更新条件限制太多,线上随便用容易翻车。
3. 事务、锁与隔离级别:数据库的可靠性命门
3.1 事务的ACID:用转账场景说透
事务是数据库里最划时代的设计之一。所谓事务,就是一组不可分割的操作序列,要么全部成功,要么全部回滚。它保证的ACID四个特性,非要死记真记不住,但你把一个转账场景放在脑子里就全通了:A账户扣100,B账户加100。
- 原子性(Atomicity):扣钱和加钱必须同时发生,不能扣了A的钱B没到账;
- 一致性(Consistency):转账前后总金额不变(以及所有约束保持不变);
- 隔离性(Isolation):你转账过程中,别人不能看到中间状态——A已经扣了、B还没到的那个瞬间;
- 持久性(Durability):转账一旦提交成功,哪怕系统立刻断电,这笔操作也不能丢。
现在数据库产品动不动就谈"分布式事务",本质都是保证跨节点、跨库的ACID。而分布式事务比单机难得多,难就难在"原子性"在多个节点间无法靠本地日志实现,得引入协调者做两阶段提交。理解单机事务ACID,是你去理解分布式事务的前提,这个顺序不能反。
3.2 并发控制:为什么必须加锁
多个事务同时读写数据时,会出现三类经典问题。
脏读:事务T1改了数据但没提交,事务T2读到了这个未提交的数据。如果T1后来回滚,T2读到的就是凭空出现的假数据。这个最好理解,也叫"读到了即将消失的数据"。
不可重复读:事务T1读了一次数据,之后事务T2修改并提交了这条数据,T1再读一次,发现前后不一致。问题在于——同一条数据,在一个事务内两次读出来的值不同。
幻读:事务T1按条件查出一批记录,事务T2插入并提交了一条符合该条件的新记录,T1再次按同一条件查,发现多出一条"幻影"记录。幻读与不可重复读的区别:不可重复读是对已有记录的值发生变化,幻读是记录数量发生变化。
解决脏读最容易,把写操作加锁,读的时候等写锁释放就行。解决不可重复读要求读的时候也锁住那行,让别的事务改不了。解决幻读更麻烦,光锁已有的行没用,新插入的行会绕过锁,得用范围锁或间隙锁把整个条件范围锁住。数据库的隔离级别就是按这个递进关系设计出来的。
3.3 隔离级别的取舍与实现
标准SQL定义了四个隔离级别:
- 读未提交(Read Uncommitted):允许读未提交数据,能解决丢失更新,但脏读、不可重复读、幻读都可能;
- 读已提交(Read Committed):只读已提交数据,解决脏读;
- 可重复读(Repeatable Read):事务内多次读同一数据结果一致,解决不可重复读;
- 可串行化(Serializable):事务完全串行执行,以上问题全都解决,代价是并发能力大打折扣。
开发中最常见的部署选择是哪一档?很多开源关系数据库默认是可重复读,而一些商业数据库默认是读已提交。实际做业务系统,读已提交大概率够用,可重复读主要强在事务内读一致性稳定,但会引入间隙锁,锁范围变大,并发写入的冲突概率也升高。除非业务对一致性有明确要求,否则没必要把所有事务都升到可重复读。
这里有个经典的并发问题叫"丢失更新":两个事务同时读同一数据、各自修改、后提交的覆盖先提交的。任何隔离级别都不防丢失更新(只有行级锁或乐观锁版本号能防),这也是面试里容易踩的坑,可别只会说隔离级别能防止所有并发问题。
3.4 死锁:为什么会有、怎么解
死锁的经典定义是:两个事务各自持有一把锁,同时等着对方手里那把锁,谁也不让,两个事务就永远卡死。为什么会发生?本质是资源竞争加循环等待。实际数据库里最常见的死锁场景是:事务T1改了表A的行1又去改表B的行2,事务T2改了表B的行2又去改表A的行1,两边都在等对方释放锁,AB-BA式死锁。
数据库处理死锁一般不会像操作系统那些复杂策略,常用的手段是等待超时和死锁检测。等待超时简单粗暴:等待超过阈值就自己回滚;死锁检测更优雅,系统会维护一张锁等待图,检测到环路就选一个代价最小的事务回滚,释放它手里的锁让其他人继续跑。
开发侧减少死锁的思路我总结了几条:一是尽量让事务按固定顺序访问资源,比如所有事务都先改A再改B,环路自然消失;二是让事务尽量短小,锁持有时间短,冲突窗口就小;三是在允许的情况下降低隔离级别,减少锁粒度。还有一个非常实用的愚招——大事务里操作多张表时,把所有语句调成同一张表内的批量操作,避免事务内跨太多表。
4. 查询全过程与优化器:SQL为什么时快时慢
4.1 一条SQL从提交到返回的旅程
一条select语句提交到数据库,背后要经过完整流水线:词法分析、语法分析、语义检查、逻辑优化、物理优化、执行。
词法分析把SQL拆成token,语法分析检查"对不对"——FROM接表名、WHERE接条件、括号匹配等等。语义检查是查这张表存不存在、这个字段属不属于这张表、有没有操作权限。真正有意思的是优化器阶段:逻辑优化负责改写SQL,比如把等值连接改成内连接、把嵌套子查询改成连接、做谓词下推,让SQL在逻辑上更优;物理优化负责生成多种执行计划,估算每种计划的代价,挑一个开销最小的方案来执行。
你有没有遇到过这种情况:同一个查询,数据量小的时候秒回,数据量涨到百万级突然卡死?多半是执行计划变了——优化器在数据量小时选择全表扫描,数据量大时却没正确估算字段分布,还是走了索引,或者反过来索引选择性太差不如全表扫描,导致查询路径完全是错的。
4.2 优化器依赖的统计信息
优化器做代价估算靠的是统计信息:表的行数、列的基数(不同值个数)、直方图(数据分布)。统计信息过旧是线上慢查询最常见的元凶之一。表数据从10万涨到1000万,统计信息还停留在旧值,优化器就可能低估某个表参与连接时产生的行数,选出错误的连接顺序。
日常维护数据库,我有一条强烈建议:批量导入数据或者大表数据出现明显增长后,立刻更新统计信息。很多数据库系统默认开了自动统计信息更新,但自动更新有阈值,数据变化没到阈值它就不动。手动ANALYZE一条命令就能解决很多灵异级别的性能问题——有时候一条SQL昨天还是毫秒级,今天变成秒级,先别怀疑语句写错了,先看一眼统计信息是不是过期了。
4.3 如何读懂执行计划
执行计划是优化器的"最终决定",也是排查性能问题的第一手材料。我在看执行计划时重点看三件事:
第一,扫描方式。是全表扫描、索引扫描还是索引回表?全表扫描加数据量大,基本就是性能瓶颈。
第二,连接类型。嵌套循环连接(Nested Loop Join)适合小表驱动大表;哈希连接(Hash Join)适合两张大表等值连接,它先建哈希表再探测,在数据量大的场景下通常比嵌套循环快得多;合并连接(Merge Join)要求两边都已排序,适合排序好的场景。看到优化器选错连接类型,那是排查慢查询的高价值线索。
第三,估算行数 vs 实际行数。执行计划里会有优化器估算的行数,如果和实际执行行数差了几十上百倍,那说明统计信息有问题或优化器判断失误,这时候方向就很清楚了:更新统计信息、调整索引,或者改写SQL。
4.4 为什么索引不总是生效
优化器选择不用索引,常见原因有这几种:
- 返回行数占表比例太高。比如一张表1000万行,查出来的有500万行,优化器算了算,走索引要大量随机IO,加起来可能比直接全表顺序扫描还慢,索性全表扫。
- 条件列上做了函数运算。
WHERE DATE(created_at) = '2024-01-01'这种写法,对索引列套函数,索引就失效了,因为索引里存的是原始值不是函数结果。改写成created_at >= '2024-01-01' AND created_at < '2024-01-02'就可以走索引。 - 隐式类型转换。字符串字段和数字比较时,数据库隐式转换,索引同样失效。
- 最左前缀原则被破坏。联合索引里没从最左列开始查,索引就无法使用。这个点太常考、太常踩坑,值得展开说说。
5. 索引结构:B+树与哈希到底怎么选
5.1 B+树:数据库的默认王者
B+树为什么几乎成了关系数据库索引的默认选择?因为它太适合磁盘存储了。它是一种多路平衡查找树,矮胖、层数少,三层B+树就能支撑百万级甚至千万级数据量——也就是说,绝大多数情况下一次索引查找只需要三次磁盘IO就够了。对比一下二叉树,数据多了层数深,IO次数直线上升。
B+树的特征和B树最大的区别是:所有真实数据都挂在叶子节点,非叶子节点只存索引键;叶子节点之间用链表串起来。这个设计带来的好处立竿见影:范围查询特别流畅,从第一个满足条件的叶子开始,顺着链表一路往后扫就完了。B树每个节点都存数据,做范围查询时要不断回溯父节点,效率低很多。另外因为非叶子节点不存数据,单个节点能容纳更多键,树更矮,IO次数更少。
5.2 聚簇索引与非聚簇索引
聚簇索引的叶子节点直接就是整行数据,表数据本身按索引键的顺序物理排列。非聚簇索引的叶子节点存的是索引键+主键值(或者行指针),查询时还要拿着主键再回表查一次。
工作中一个直观感受:用聚簇索引查主键特别快,因为一次IO就能拿全整行数据。用非聚簇索引查数据,先找到主键,再回表查整行,多一次回表。所以设计索引策略时有几个约定俗成的思路:给经常范围查询或排序的列做主键或聚簇索引;给where条件频繁使用的列建非聚簇索引;如果查询的字段全都在某个非聚簇索引里,甚至可以直接走覆盖索引,一次索引扫描就能返回全部需要字段,连回表都省了。
5.3 哈希索引与它的局限
哈希索引用哈希函数把索引键映射到一个固定桶位,等值查询的速度理论上是O(1),比B+树快一个量级。但它的短板太明显:不支持范围查询,因为哈希值的排列顺序和原始键的顺序完全没关系;也不支持排序,不支持部分联合索引匹配(得所有索引列都用等值条件)。所以哈希索引在关系型数据库里基本只做辅助角色,比如内存型数据库、缓存层会大量用哈希结构。
开发中逻辑上类似哈希索引的场景是等值查询极频繁、几乎不涉及范围的列。比如用户ID、订单号这类精确匹配,如果有人为了折腾性能把用户表主键改成哈希索引,那范围查询和排序基本就废了。我自己的原则很简单:默认B+树,只有极端的等值高并发场景才考虑哈希。
5.4 联合索引与最左前缀
联合索引的数据结构还是B+树,但节点里的键是按多个列组合排序的:先按第一列排序,第一列相同再按第二列排序,依此类推。这就解释了最左前缀原则——查询条件必须从联合索引的第一列开始,否则用不上这个索引。
举个例子,联合索引是(a, b, c),那么:
WHERE a = 1能走索引WHERE a = 1 AND b = 2能走索引WHERE a = 1 AND b = 2 AND c = 3能走索引WHERE b = 2走不了索引,因为b不是最左列WHERE a = 1 AND c = 3能走a的部分,但c这一列无法利用索引,因为中间隔了个b
实际设计联合索引时把哪个列放第一位,取决于查询模式。我常用的经验是:维度筛选最强、区分度最高的列不一定放最前面,更关键的是看业务里哪些条件会固定出现。比如订单表查询永远是先带shop_id再带其他条件,那就把shop_id放第一列,这样绝大部分查询都能吃到索引。
6. 关系模式的规范化:表和表之间怎么设计才不乱
6.1 为什么需要范式
范式理论听起来很学术,本质就解决一个问题:表结构设计不合理会带来大量冗余,冗余会导致更新异常、插入异常、删除异常。举个例子,一个表把"学生、所在系、系主任"全塞在一起,系主任同时管几十个学生,那系主任的名字就得重复存几十遍;系主任换了,所有学生的记录都得跟着改,一不留神就改漏了——这就是更新异常;新成立一个系,还没有学生,那这条系信息因为主键是学生,就压根插不进去——这是插入异常。范式就是一套逐步消除这些问题的规则。
6.2 第一、第二、第三范式的递进关系
第一范式(1NF)的要求是:每个字段都是原子性的,不可再分。电话字段里存"手机+座机"两个值,就不满足1NF。这条现在基本不用操心,正规设计不会犯。
第二范式(2NF)在1NF基础上要求:消除部分依赖。什么叫部分依赖?就是当主键是联合主键时,某个非主属性只依赖于联合主键的一部分。比如选课表主键是(学号, 课程号),非主属性"课程名"只依赖于"课程号",不依赖学号——这就是部分依赖,会导致课程名重复存储,课程改名要改多条记录。解决办法是把选课表拆成选课表(学号+课程号)和课程表(课程号+课程名)。
第三范式(3NF)在2NF基础上要求:消除传递依赖。定义是:非主属性不能通过另一个非主属性依赖于主键。员工的"部门名"应该由"部门号"决定,而员工表里如果直接放部门名,形成"员工号 -> 部门号 -> 部门名"的传递依赖,部门名就会存储冗余,部门改名又得全表更新。拆法是把部门信息单独拆成部门表。
6.3 BC范式与反范式:理论不是越高越好
BC范式(BCNF)比3NF更严格,要求每一个决定因素都包含候选键。多数教科书里会说BCNF是3NF的修正版,消除了主属性对码的部分依赖和传递依赖。实际工作中能到3NF已经很规范,BCNF更多是考试题里的考点,业务设计很少死磕。
更要紧的是反范式设计。完全规范化的后果是表特别多,查询要join一堆表,性能自然就下来了。真正的高并发系统里,经常会有意识地做冗余——把热点的统计字段、展示字段冗余到某张表中,换取读性能。做法通常是保留一份冗余列,通过消息队列或定时任务保持同步。这不算"不规范",而是取舍。我总结的一个原则:写入低频、读高频、数据一致性要求可容忍微延迟的时候,优先考虑冗余设计;核心金融类数据,照着3NF老老实实设计,别玩花活。
7. 日志系统与故障恢复:数据丢了怎么救
7.1 日志先行原理(WAL)
数据库为了保证事务的持久性,不会每次修改都立刻把数据刷到磁盘——那样随机IO太廉价了,性能根本扛不住。它的做法是:先写日志,再改数据。日志记录的是"我准备做什么:把某页某偏移量的值从A改成B",日志是顺序追加写,直接把新内容写到日志文件尾部,顺序IO比随机IO快得多。这就是WAL(Write-Ahead Logging,预写日志)。系统崩溃后,数据页可能没落盘,但日志在,重放日志就能把修改还原回来。
这个设计是我觉得数据库原理里最优雅的一笔。它用"日志先行"把随机写换成了顺序写,同时保证任何时刻崩溃都能恢复。理解WAL之后,很多现象就都能解释了:为什么事务提交慢?因为要保证日志落盘(fsync)完成;为什么数据库启动时有时要等很久?因为日志重放需要时间。
7.2 检查点与恢复流程
日志无限增长不是办法,数据库引入了检查点(Checkpoint)机制。检查点做的事情是:把某时刻之前的所有脏页强制刷到磁盘,然后在日志中记录一个检查点位置。恢复时只需要从头或从最后一次检查点开始重放日志,检查点之前的数据已经落盘,不需要处理。
完整的崩溃恢复过程大概是:从日志中找到最近一次检查点记录,把它之后的日志全部读出来;正向扫描日志,把所有未提交事务的修改重新执行一遍(REDO);反向扫描日志,把未提交完成的把已写到磁盘但事务最终没提交的重置掉(UNDO)。有了检查点,崩溃后要重放的日志量大幅缩减,重启速度快很多。
我实际运维时特别关注检查点触发频率。检查点太频繁,刷盘占用大量IO,影响业务;检查点太少,崩溃后恢复时间太长。有的数据库在崩溃恢复时卡住很久,多半就是检查点间隔太长,积累了太多未刷盘的日志。
7.3 存储引擎与日志的协同
日志不只用于崩溃恢复,还是主从复制或集群间数据同步的重要基础设施。很多数据库的主从复制原理其实不复杂:主库产生日志,把日志传送到备库,备库重放日志达到同步状态。这也解释了为什么主从切换不是瞬间完成——备库要先把落后的日志追平,追得越多切换越慢。理解了这套原理,运维时就知道:主库负载很高、日志积压的时候,别贸然做主从切换,否则切换完备库还要追日志,服务可用性反而更糟。
故障恢复涉及的备份策略也离不开日志。恢复数据到某个历史时间点,基础全量备份加上增量备份的日志重新放,就能精确回到任意时间点。很多考试题喜欢问"介质故障怎么恢复",答案的主干就是:利用后备副本恢复基础数据,再重放日志到故障发生前。这套逻辑现在云数据库的PITR(按时间点恢复)功能也还在用,只是做成了自动化和可视化。
8. 知识点串讲与个人体会
把前面这些知识点串起来看,数据库原理其实就回答了三个问题:数据怎么组织不混乱(关系模型与范式)、查询怎么高效执行(索引与优化器)、系统怎么保证不丢不错(事务与恢复)。三个问题各自延展出一片知识树,面试和考试考察的范围,基本都在这个框架内。
给正在准备考试或者面试的朋友几条务实的建议:第一,不要死背ACID、范式定义,拿一个转账场景、一个选课表设计场景去推演,推着推着就记住了;第二,索引相关的题,试着直接从B+树叶子节点结构入手推导,绝大多数结论都能自己推出来(比如为什么覆盖索引快、为什么最左前缀成立);第三,事务隔离级别的题,把脏读、不可重复读、幻读三个概念吃透,隔离级别就是从"我允许哪种读异常"的角度去定义的,这一条想通了,级别列表根本不用背。
我在实际带项目过程中的体会是,业务代码写得好的人不一定懂数据库原理,但懂数据库原理的人遇到数据问题从来不慌。线上查询慢、数据对不上、集群挂了一个节点,这些问题表面五花八门,底层都能回到这篇文章讲到的某个原理。原理这东西,学的时候觉得虚,实践越深越会发现它其实非常实——它是在给工程决策兜底。希望这篇汇总能帮你把脑子里零散的知识点串成一张网,到真正需要的时候,随时织得起一张有用的网。