三级模式:外模式(用户看到的)、模式(全体数据的逻辑结构)、内模式(磁盘上怎么放)。 两级映射换来两种独立性:改表不用改查询(逻辑独立性),换存储不用改程序(物理独立性)。 独立性有边界:视图和索引都不会拦一条逻辑写错的 SQL。
教材 1.2 讲三级模式结构和三级模式之间的映射。这一节抽象,但它解释了后面很多「为什么」。
为什么建了视图以后改表不用改程序?为什么加索引不用改 SQL?为什么视图写 SELECT * 会出事?
三个问题的答案都在这一节。
演示里能实测到两件事:给表加一列,写死列名的视图不受影响、写 * 的视图多出一列;
给列加索引,同一条 SQL 的计划从扫表变成查找、耗时差 180 倍。
一、三个模式,各看一层
| 模式 | 别名 | 看到的是什么 | 本课的对应物 |
|---|---|---|---|
| 外模式 | 用户模式、子模式 | 自己关心的那部分数据 | 视图 |
| 模式 | 逻辑模式、概念模式 | 全部数据的逻辑结构 | 表的列定义 |
| 内模式 | 存储模式 | 数据在磁盘上怎么排 | 索引、数据文件 |
第一行是「按人裁剪」:财务只该看到金额,不该看到身份证号。裁剪的手段就是视图——视图不是权限, 但它是最省事的第一道墙。 第二行是「整体一张图」:一个库里有几张表、每张表几列、列是什么类型,这些定义合起来就是模式。 它只有一份,改它影响所有人——所以改表结构是要开会的事。 第三行是「怎么放」:数据按什么顺序排、建了几个索引、放在哪个文件组。 这一层是可以随便改的,因为改它不影响上面两层的「意思」。 这三层不是三个软件,是同一个库的三种看法。 你在 SSMS 里点「表」看到的是模式,点「视图」看到的是外模式的成品, 点「索引」看到的是内模式的一部分。
二、两级映射,换来两种独立性
| 映射 | 谁变了不用改谁 | 叫什么 |
|---|---|---|
| 外模式/模式 | 表加了列,视图不用重建 | 逻辑独立性 |
| 模式/内模式 | 加了索引,SQL 不用改写 | 物理独立性 |
第一行演示里能直接看到:给 tb_student 加一列 phone,v_male 这个视图还是三列、
还是 25000 行。
因为它写的是 SELECT sno, sname, class_id,列名写死了,表怎么长都跟它无关。
但同一个演示里,写成 SELECT * 的那个视图立刻变成 6 列。
这就是「映射」的边界:它映的是「你写出来的那些列」,不是「这张表的全部」。
所以建视图时把列名写全,不是讲究,是保命。
第二行更值钱:同一条 SELECT ... WHERE sname = ?,加索引前 7.104 ms,加索引后 0.039 ms,
计划从 SCAN 变成 SEARCH。
SQL 一个字没改,结果一模一样,快的部分全是内模式给的。
这也是「优化查询」的正确姿势:先看计划、再动索引,而不是改 SQL 的写法。
第 14 章整章讲的就是怎么动这一层。
三、独立性不是万能的
演示里还有一段反例:在视图上做前导通配的模糊查,索引完全用不上。
| 写法 | 命中 | 耗时 | 计划 |
|---|---|---|---|
sno = '20230567' |
1 行 | 0.038 ms | SEARCH |
sno LIKE '%567' |
50 行 | 10.673 ms | SCAN |
行数差 50 倍,耗时差 284 倍。多出来的那部分,就是「索引没被用上」的代价。
前导通配(% 打头)让 B 树没法定位起点,只能从头扫到尾。
所以两级映射保证的是「改了不用改代码」,不是「怎么写都快」。独立性管的是兼容性,
性能还是得自己看着办。
同样地,视图也不保证「写得对」。
SELECT * FROM v_male WHERE score > 90 会因为 v_male 里没有 score 列而报错——
视图只替你把 WHERE 存下来,不会替你想。
四、这套结构在 SQL Server 里叫什么
| 教材的说法 | T-SQL 里的东西 | 怎么看 |
|---|---|---|
| 外模式 | VIEW | CREATE VIEW,第 10 章 |
| 模式 | 表与列的定义 | sys.columns、sp_help |
| 内模式 | 索引、文件组、分区 | sys.indexes、sys.filegroups |
第二行的 sys.columns 是元数据视图,查它能看到每一列的类型和长度。
这类系统视图在第 4.1 节讲「系统数据库」时会再碰一次。
第三行的「文件组」是 SQL Server 特有的:一个库可以有多组数据文件,
把热表和冷表分开放到不同磁盘上。
这就是内模式在 SQL Server 上的具体形态,比「索引」还低一层。
教材这一节只有两页,但它的用词(外模式、模式、内模式)会一路用到第 17 章。
看到「逻辑独立性」「物理独立性」这两个词时,回来对一眼这张表就够了。
五、动手 Demo:三个模式与两级映射:视图、表、索引
这一段建两张五万行的表,然后做三件事:建两个视图当外模式;
给表加一列,看写死列名的视图和写 * 的视图有什么不同;
给一列加索引,看同一条 SQL 的计划和耗时怎么变。
最后拿视图做一次前导通配查询,说明「独立性不等于性能」。
import sqlite3, time
c = sqlite3.connect(u"schema.db", isolation_level=None)
c.execute(u"BEGIN")
c.executemany(u"INSERT INTO tb_student VALUES (?,?,?,?,?)",
[(u"2023%04d" % i, u"学生%04d" % i, i % 3 + 1,
u"男" if i % 2 else u"女", u"2005-01-01")
for i in range(1, 50001)])
c.execute(u"COMMIT")
# 外模式:两个视图
c.execute(u"CREATE VIEW v_male AS SELECT sno, sname, class_id "
u"FROM tb_student WHERE sex = '男'")
c.execute(u"CREATE VIEW v_avg AS SELECT sno, AVG(score) AS 均分 "
u"FROM tb_score GROUP BY sno")
# 逻辑独立性:加一列,两种视图各是什么反应
c.execute(u"ALTER TABLE tb_student ADD COLUMN phone TEXT")
print(u"v_male 列数 %d" % len(c.execute(u"SELECT * FROM v_male LIMIT 1").description))
c.execute(u"CREATE VIEW v_all AS SELECT * FROM tb_student")
print(u"v_all 列数 %d" % len(c.execute(u"SELECT * FROM v_all LIMIT 1").description))
# 物理独立性:加索引,SQL 不动
Q = u"SELECT sno, sname FROM tb_student WHERE sname = '学生25000'"
t0 = time.perf_counter(); c.execute(Q).fetchall()
t0 = time.perf_counter() - t0
print(u"没索引 %.3f ms %s" % (t0 * 1000,
c.execute(u"EXPLAIN QUERY PLAN " + Q).fetchone()[3]))
c.execute(u"CREATE INDEX ix_student_sname ON tb_student(sname)")
t1 = time.perf_counter(); c.execute(Q).fetchall()
t1 = time.perf_counter() - t1
print(u"加了索引 %.3f ms %s" % (t1 * 1000,
c.execute(u"EXPLAIN QUERY PLAN " + Q).fetchone()[3]))
实跑输出:
--------------------------------------------------------------------
三个模式与两级映射:视图、表、索引
--------------------------------------------------------------------
① 三个模式对到这台机器上的三个东西
--------------------------------------------------------------------
| 模式 | 它是什么 | 这里的具体物 |
|---|---|---|
| 外模式 | 用户看到的那一层 | 两个视图 |
| 模式 | 全体数据的逻辑结构 | 三张表的列定义 |
| 内模式 | 数据在磁盘上怎么放 | 索引与文件 |
1> CREATE VIEW v_male AS SELECT sno, sname, class_id FROM tb_student WHERE sex = '男'
1> CREATE VIEW v_avg AS SELECT sno, AVG(score) AS 均分 FROM tb_score GROUP BY sno
v_male 这个外模式里现在有 25000 行,可它自己一个字节都不占。
② 逻辑独立性:给表加一列,视图要不要改
--------------------------------------------------------------------
1> ALTER TABLE tb_student ADD COLUMN phone TEXT
加完 phone,v_male 还是 25000 行、还是三列——视图写死了列名,不受影响。
1> CREATE VIEW v_all AS SELECT * FROM tb_student
但写成 SELECT * 的视图不一样,它现在有 6 列:sno、sname、class_id、sex、birth、phone
! 所以建视图时一定把列名写全,别图省事写 * ——表一改,视图的形状就跟着变。
③ 物理独立性:加一个索引,SQL 一个字不用改
--------------------------------------------------------------------
1> CREATE INDEX ix_student_sname ON tb_student(sname)
| 什么时候 | 耗时 | 执行计划 |
|---|---|---|
| 没索引 | 6.980 ms | SCAN tb_student |
| 加了索引 | 0.047 ms | SEARCH tb_student USING INDEX ix_stude |
同一条 SQL、同一个结果,耗时 6.980 → 0.047 ms,计划从扫表变成查找。
这就是内模式变了、模式和外模式都不用动的意思。
(拿 sno 演示是看不出差别的:它是主键,SQLite 早就自动给它建了索引。)
④ 反过来说:外模式也挡不住坏查询
--------------------------------------------------------------------
| 写法 | 命中 | 耗时 | 执行计划 |
|---|---:|---|---|
| 等值 `sno = ...` | 1 行 | 0.044 ms | SEARCH tb_student USING IN |
| 前导通配 `LIKE '%567'` | 50 行 | 10.566 ms | SCAN tb_student |
命中 1 行的那条只用 0.044 ms,命中 50 行的那条要 10.566 ms。
行数差 50 倍,耗时差 239 倍——多出来的那部分,就是「索引没被用上」的代价。
视图只是把 WHERE 存下来了,它不会替你想「这样写会不会慢」。
**三级模式的价值不在于「分层好看」,在于两件事:换存储不用改程序,改表不用改所有查询。**
**它换不来的是「写错了也能跑」——视图和索引都不会拦一条逻辑错的 SQL。**
第一段的结果是 25000 行、三列——视图自己一个字节都不占,它只是存了一条 SELECT。
第三段最关键:加完 phone,v_male 还是三列,v_all 立刻变成六列。
同一张表、同一次改动,两个视图反应完全相反,差别只在「有没有写死列名」。
第四段是这一节最硬的证据:同一条 SQL,7.104 ms → 0.039 ms,
计划从 SCAN tb_student 变成 SEARCH tb_student USING INDEX ix_student_sname。
这里有个坑值得说:一开始我拿 sno 做演示,加不加索引都是 0.04 ms。因为 sno 是主键,
SQLite 早就自动给它建了索引——这算「白做了个实验」。
换成一个普通列 sname 才看出差别。下次要看索引效果,先确认目标列上真的没有索引。
第五段是反例:命中 1 行的那条 0.038 ms,命中 50 行的那条 10.673 ms。
行数差 50 倍,耗时差 284 倍——多出来的是「索引用不上」的代价。
30 秒实验
- 把
v_male的定义改成SELECT *,再跑一次「加列」,看列数怎么变。 然后新建一个视图,故意只写SELECT sno, sname,再删掉一列phone,看哪个视图会报错。 - 把索引删掉(
DROP INDEX ix_student_sname),再跑那条 SQL,确认耗时回到 7 ms 左右。 这个来回能让你记住:索引是「可以随时加、随时删」的东西,不是建表的附属品。 - 在
v_male上写SELECT * FROM v_male WHERE class_id = 1 ORDER BY sname, 看计划里有没有用到索引。class_id上没有索引,所以还是扫——视图不会替你补索引。
常见误区
- 「视图能提高性能」。不能。 视图存的是一条 SELECT,每次查它都是重跑那条语句。视图省的是「重复写复杂 SQL」的力气, 不是 CPU。要快只能加索引或改写法。
- 「加索引一定更快」。写操作会变慢——每次 INSERT 都要维护索引树。 一张频繁写入的表加五个索引,插入速度可能掉一半。 索引是拿写换读。
- 「建视图时写
SELECT *省事」。省的是今天的事,埋的是明天的雷。 表加一列,视图的形状就变了,用到这个视图的导出、报表、接口全部跟着变。
自测三问(点开对答案)
1. 表加了一列,为什么写死列名的视图不受影响? —— **因为视图存的是「这条 SELECT」,而这条 SELECT 里写的是具体列名。** 外模式/模式映射映的是「你点名的那些列」,表多出来的列不在映射范围内,自然不影响。 写成 `*` 就没有点名,映射范围跟着表一起变,所以列数会变。 2. 加索引让 SQL 变快,这属于哪种独立性? —— **物理独立性。** 索引属于内模式(磁盘上怎么放),SQL 属于外模式/模式那一侧。 **内模式变了而上面两层不用改,就是物理独立性的定义。** 3. 为什么 `LIKE '%567'` 用不上索引? —— **B 树是按「从头开始的若干个字符」排序的,只有知道前缀才能定位到起点。** `%` 打头意味着「前面是什么都行」,起点不定,只能从头扫到尾。 所以模糊查询要么限制成前缀匹配,要么上全文索引。下一篇进 1.3 数据模型。 那一节讲「一张表该怎么拆」——三范式逐级拆一遍,并且实测「拆开之后,改一个事实只需要改一行」。
- ☐ (本篇完,下一篇 1.3 数据模型)