🪟 第1章 数据库基础
📄 一句话概括

数据管理走了三代:人工管理、文件系统、数据库系统。 每一代的进步都只解决一个问题——「同一份数据,别让第二个人抄一遍」。 数据库系统不是「一个软件」,是五样东西凑在一起:硬件、数据库、数据库管理系统、应用程序、 人。少一样都跑不起来。 把这五样分清,后面十九章才知道自己在跟哪一层打交道。

📝 本章和前面几章的关系

教材 1.1 只有两个小节:数据库技术的发展、数据库系统的组成。篇幅很短,但它是全书的地基。 后面十九章会不停地出现「DBMS 做什么」「DBA 做什么」「应用做什么」—— 这些分工就在这一节定下来。 这一节也是全书唯一不讲具体产品的。从第 2 章装 SQL Server 开始,每一节都绑在一个产品上; 只有这里讲的是通用的东西。

一、三个阶段,各解决一个问题

阶段 数据存在哪 谁能用 最大的毛病
人工管理 纸、磁带、程序里 写程序的那个人 数据不独立,程序一改全废
文件系统 操作系统的文件 能读到文件的人 冗余大,同一份数据抄好几份
数据库系统 DBMS 管着 按权限来 要靠人设计,设计错了更麻烦

第一代的特征是「数据跟着程序走」。 同一批学生成绩,要算平均分写一个程序,要排名次再写一个程序,两个程序各自存一份数据。 改一个学生的分数,得改两处;漏改一处,两个程序给出的答案就不一样了。 第二代把数据从程序里拿出来,放进了文件。这是真进步:数据能长期存着了。 但文件系统只管「怎么存、怎么读」,不管「这份数据和那份数据是什么关系」。 所以同一个学生名,在学生文件里一份、在成绩文件里一份、在缴费文件里再一份——三份之间没有任何联系, 得靠人记住它们是同一件事。 第三代的关键不是「存进了数据库」,而是「有一个专门管数据的软件」。 这个软件能保证四件事:同一份事实只存一处(靠范式拆表)、两个人同时改不会打架(靠事务)、 写错了会被拦住(靠约束)、谁能看哪张表可以配(靠权限)。这四件事,文件系统一件都做不到。 所以第三代和第二代的差别不在存储介质,而在「有没有一个中间层替你把规矩守住」。

二、五样东西,一个都不能少

组成 具体是什么 少了它会怎样
硬件 服务器、磁盘、内存、网络 数据没地方放
数据库 有组织存着的数据本身 没什么可管
DBMS SQL Server、Oracle 这类软件 没人替你执行 SQL
应用程序 你写的那套界面与业务逻辑 用户只能敲 SQL
人 DBA、开发、最终用户 没人设计、没人运维

第三行是这一节最该记住的:DBMS 是「软件」,数据库是「数据」。 这两个词在日常说话里经常混着用,比如「连上数据库」——连上的是 DBMS 的实例,不是那个数据文件。 分开的唯一好处是排查故障时知道该看哪一头。 第五行「人」里的 DBA,在中小企业常常是兼职的。 但有几件事必须有人负责:备份能不能恢复、账号该给多大权限、磁盘满了怎么办。 这三件事没有「自动」的版本——备份工具会按时生成文件,但它不会告诉你这个文件能不能还原。

三、四个词,边界在哪

词 英文 指的是
数据 Data 一条条记录,比如「张伟,高等数学,88」
数据库 DB 这些记录的集合,有组织地存在磁盘上
数据库管理系统 DBMS 管这个集合的软件,SQL Server 就是
数据库系统 DBS 上面三样加硬件、应用、人,一整套

最容易混的是第二和第三行。 一个简单的分辨办法:数据库能被备份成一个文件,DBMS 不能——DBMS 是装上去的,要占端口、要起服务、 有版本号。 所以「数据库坏了」这句话有两种完全不同的含义。 一种指数据文件坏了,要靠备份恢复;另一种指服务起不来了,要看错误日志和 Windows 服务。 排查方向完全相反,第一步就要问清是哪一种。 第四行「数据库系统」是个总称,教材用它是为了强调「不是只买个软件就行」。 一个只有 SQL Server、没有应用、没人管的系统,是跑不起来的——它连「该存什么」都还不知道。

四、动手 Demo:从三个 txt 文件到一条 SQL

这一段不用数据库,先用最土的办法做一遍「查高等数学及格的女生」:数据写在两个 CSV 里, 用 Python 读文件、手工关联、逐条筛。然后同一个需求改用一条 SQL。两边都计时,看差在哪。 第二段演示冗余的代价:把学生表里的一个名字改掉,看成绩表里留下几行旧名字。

import csv, io, os, random, sqlite3, time

# 造数据:500 个学生,每人 3 门课,共 1500 行成绩
stu = []
for i in range(1, 501):
    ci = i % 3
    stu.append([u"2023%04d" % i, u"学生%03d" % i,
                CLASSES[ci], TEACHERS[ci], u"男" if i % 2 else u"女"])
sco = []
for i, s in enumerate(stu):
    for j, sub in enumerate([u"高等数学", u"大学英语", u"数据结构"]):
        sco.append([s[0], s[1], sub, u"%d" % (40 + (i * 7 + j * 13) % 61)])

# 文件版:读两张表,按学号配对,再筛三次
def file_way():
    st = {}
    for a in csv.reader(io.open(u"students.csv", encoding=u"utf-8")):
        st[a[0]] = (a[1], a[2], a[4])
    out = []
    for a in csv.reader(io.open(u"scores.csv", encoding=u"utf-8")):
        if a[2] != u"高等数学" or float(a[3]) < 60:
            continue
        s = st.get(a[0])
        if s and s[2] == u"女":
            out.append(a[0])
    return out

# 数据库版:一条 SQL
SQL = (u"SELECT s.sno, s.sname FROM tb_score c "
       u"JOIN tb_student s ON s.sno = c.sno "
       u"WHERE c.subject = '高等数学' AND c.score >= 60 AND s.sex = '女'")
rows = c.execute(SQL).fetchall()

print(u"文件版 %.2f ms,SQL 版 %.2f ms" % (t_file, t_sql))

实跑输出:

--------------------------------------------------------------------
从三个 txt 文件到一条 SQL
--------------------------------------------------------------------


  ① 先搭一个「没有数据库」的世界
  --------------------------------------------------------------------
  students.csv:500 行(学号、姓名、班级、班主任、性别)
  scores.csv  :1500 行(学号、姓名、科目、成绩)
  注意 scores.csv 里也存了姓名——这就是第一处冗余。

  ② 查「高等数学及格的女生」,文件版要几步
  --------------------------------------------------------------------
  读学生表 → 读成绩表 → 按学号配成一对 → 筛科目 → 筛分数 → 筛性别。
  少一步都出不来结果,而且每一步都得自己写。


  ③ 同一件事,SQL 一条语句
  --------------------------------------------------------------------

  1> SELECT s.sno, s.sname
     FROM tb_score c JOIN tb_student s ON s.sno = c.sno
     WHERE c.subject = '高等数学' AND c.score >= 60 AND s.sex = '女'
     ORDER BY s.sno LIMIT 5
  sno       sname
  -----------------
  20230004  学生004
  20230006  学生006
  20230008  学生008
  20230014  学生014
  20230016  学生016
  (5 行受影响)
  ……共 167 行。上面只列前 5 行。

  ④ 四条时间,看清钱花在哪
  --------------------------------------------------------------------
| 做法 | 耗时 | 说明 |
|---|---:|---|
| 只把文件读进内存 | 0.52 ms | 纯 IO,不解析 |
| 逐行解析成绩表 | 1.31 ms | csv 模块拆字段 |
| 读两张 + 关联 + 筛 | 2.46 ms | 完整的手工版 |
| 一条 SQL | 0.63 ms | 交出关联和筛选 |

  手工版是 SQL 版的 3.9 倍。但倍率不是重点——
  重点是那 167 行结果里,SQL 版只写了 5 个关键词:
  SELECT、FROM、JOIN、WHERE、ORDER BY。

  ⑤ 第二处问题:冗余会自己长出来
  --------------------------------------------------------------------
  现在有一个学生改名了。业务方的做法是「改学生表」。
  改完 students.csv,scores.csv 里还留着 3 行旧名字。
  数据库版只要改一个地方:

  1> UPDATE tb_student SET sname = '张三丰' WHERE sno = '20230001'
  学生表里改到 1 行。
  而 tb_score 的列是 sno、subject、score——里面没有姓名。
  不是忘记存,是不该存:姓名只在 tb_student 里有一份,就只在那里改。

**这就是数据库管理系统干的第一件事:把「同一份事实只存一处」变成制度,而不是靠人记得。**
**第二件事才是快:一条 SQL 替掉了一整段手工循环。**
**第三件事是那两样测不出来的——并发时谁能挡住别人乱改(事务),以及写错了谁能拦住(约束)。**

四条时间摆出来,能看清钱花在哪:只读文件 0.59 ms,逐行解析涨到 1.37 ms, 加关联和筛选到 2.48 ms;一条 SQL 是 0.69 ms。 手工版是 SQL 版的 3.6 倍。 这个倍率本身不是重点——Python 循环和 SQLite 的 C 层比,本来就是不公平的比较。 重点是那 167 行结果背后,SQL 版只写了五个关键词:SELECT、FROM、JOIN、WHERE、ORDER BY。 手工版写了一整个函数,还得自己保证配对逻辑没错。 第二段的数更说明问题:改完学生表,成绩表里留着 3 行旧名字。 数据库版的 tb_score 里根本没有姓名这一列——不是忘记存,是不该存。 姓名只在 tb_student 里有一份,就只在那里改。 三件事里,只有「快」是能测出来的。 另两件——「同一份事实只存一处」和「并发时挡住乱改」——测不出来,但它们在真实系统里更重要。

30 秒实验

  1. 打开 students.csv 和 scores.csv,数一数「学生001」这个名字重复了几遍。 再想想如果全校有一万个学生、二十门课,这个数字会变成多少。
  2. 把 file_way() 里的 s = st.get(a[0]) 改成 s = st[a[0]],跑一遍,看会报什么错。 这个错就是「没有外键约束」的世界里天天发生的事:成绩表里出现了学生表没有的学号。
  3. 在 tb_score 里插一行 ('99999999', '高等数学', 90),能插进去。 这说明建表时没写外键——数据库不会主动拦你,除非你让它拦。这个坑在第 5.4 节用外键约束堵上。

常见误区

  1. 「数据库就是那个 .mdf 文件」。文件是内模式那一层,DBMS 才是能执行 SQL 的那一层。 拷一个 .mdf 到别的机器上直接双击,是打不开的——它得挂到实例上。
  2. 「数据量大才需要数据库」。反了。数据量小的时候用 Excel 更省事; 需要数据库的时机是「多人同时改」和「不能出错」,跟行数没关系。 三个人同时改一个 Excel, 谁后保存谁覆盖。
  3. 「装了 SQL Server 就有数据库了」。装完只有四个系统库(master、model、msdb、tempdb), 自己的库要自己 CREATE DATABASE。这一步在第 4.3 节。
自测三问(点开对答案) 1. 文件系统和数据库系统的分水岭在哪? —— **不在「数据存在文件里还是存在库里」——数据库最终也是文件。** **分水岭是「有没有一个专门管数据的软件」。** 这个软件能保证同一份事实只存一处、并发不打架、 写错被拦住、权限可配。文件系统只负责「存进去、读出来」。 2. 「数据库坏了」这句话,第一步该问什么? —— **问是「数据文件坏了」还是「服务起不来」。**前者要靠备份恢复(第 17.3 节), 后者要看错误日志和 Windows 服务(第 3.2 节)。两个方向完全相反,问错一句会白折腾半天。 3. DBA 在小公司常常是兼职的,哪三件事必须有人管? —— **备份能不能恢复、账号给了多大权限、磁盘满了怎么办。**这三件没有「自动」版本。 备份尤其:**没试过恢复的备份,等于没有备份。**

下一篇进 1.2 数据库的体系结构。 那一节讲的是「一个数据库为什么要分三层看」,以及分了三层换来什么——换来的东西叫「独立性」, 演示里能实测到。

相关笔记