🪟 第4章 创建与管理数据库
📄 一句话概括

五个系统库:master 管登记、model 当模板、msdb 管作业、tempdb 当草稿纸、resource 只读。 三类文件:.mdf 主数据、.ndf 次数据、.ldf 事务日志; 日志记的是「改之前的样子」,不是副本。 文件组的意义只有一条:让冷数据和热数据落在不同的盘上,备份和 IO 都分开。

📝 本章和前面几章的关系

前三章都在讲「外面」:装在哪、服务怎么起、怎么连。这一章进到里面,看数据库这个对象本身。 教材 4.1 分五小节:基本概念、常用对象、数据库组成、系统数据库、文件与文件组。 五小节里真正天天用得上的是后两节——系统库和文件。 演示里用 SQLite 的文件模型复刻了「数据文件 + 日志文件」和「文件组」两件事, 这两个机制在 SQL Server 里的形状,用 SQLite 的两个文件就能看得清清楚楚。

一、五个系统库,各管一摊

装完 SQL Server,对象资源管理器里会看到五个库,它们不是样本库,是实例自己的账本。

库名 管什么 坏了会怎样
master 登录名、数据库清单、实例配置 整个实例起不来
model 新建库的模板 新库会带上它的设置
msdb 作业、备份历史、维护计划 代理跑不了,记录丢
tempdb 临时表、排序中间结果 每次重启重建,满了全卡
resource 只读,系统对象 只能靠打补丁更新

master 最特殊,它记着「这个实例上有哪些数据库」。 它坏了, 实例连启动都过不去——所以装完 SQL Server 的第一件事是备份 master, 这句话教材在后面的备份章节还会说一次。 model 是模板,不是样本。 你新建一个库,它是照着 model 抄出来的——model 里的表、视图、排序规则, 新库全都有。有些人往 model 里塞一张公共的日志表,新建的库就都自带这张表。 tempdb 是唯一一个「每次重启清空重建」的库。 所以往里放东西永远是临时的, 连备份都不用做——它本来就不存该留的数据。 resource 是隐藏的。 对象资源管理器里看不见,它存的是系统视图、系统存储过程这些东西,只读, 只能通过打补丁更新。所以「系统存储过程被改了」这种事,正常途径做不到。

二、三类文件,职责完全不同

扩展名 叫什么 几个 装什么
.mdf 主数据文件 1 个,必须有 数据页、索引页、系统页
.ndf 次要数据文件 0 到多个 分担数据,可放别的盘
.ldf 事务日志 至少 1 个 改之前的旧值

日志文件不是数据的副本,是「改之前的样子」。 这个区别很关键:数据文件里存的是当前值, 日志里存的是「改成当前值之前它是什么」。回滚靠它、恢复到某个时间点也靠它。 所以日志和数据必须分开放。 放同一块磁盘,磁盘一坏两个一起没, 日志就白记了——它本来是用来在磁盘坏掉之后恢复的。 演示里用 SQLite 的两个文件复刻了这个结构:一个数据文件,一个日志文件。 写操作先落日志,再慢慢合进数据文件——这就是所有关系库的共同套路,SQL Server、MySQL、 PostgreSQL 都一样,区别只在日志留多久、什么时候合并。 SQL Server 的日志不会「提交就删」。 它一直往后追加,直到你做日志备份,或者把恢复模式切成简单。 所以一个只增不减的库,.ldf 会长得比 .mdf 还大——这是新手最常见的「磁盘怎么突然满了」。

三、文件组:为什么要分

文件组是「一组文件的集合」。建表时可以指定 ON 组名,这张表就落在那个组的文件上。 教材只说了一句「文件组便于管理」,实际用起来只有两个理由。 第一个理由:把冷数据放到慢盘上。 三年前的历史订单放在一块大容量机械盘,今年的热数据放在 SSD。 查询热数据不受冷数据拖累,冷数据也不用占 SSD 的空间。 第二个理由:备份可以分开做。 主文件组一周备一次,冷数据那个组一个月备一次。 数据量大的时候,这个差别直接决定备份窗口够不够。 演示里用两个文件复刻了这件事:主文件放 2001 行,次文件放 500 行, 两个文件各自独立增长,互不影响。 SQL Server 里这两个文件就叫两个「文件组」。 别过度设计。 中小型库一个文件组就够了,分多了反而增加管理成本——加文件、调增长、算容量, 每多一个组就多一套。文件组是给「数据量真的很大」准备的。

四、常用对象:一张表说清

对象 一句话 什么时候用
表 Table 存数据的地方 所有业务数据
视图 View 存起来的一条查询 常查的复杂连接
索引 Index 为了查得快而建的目录 经常出现在 WHERE 里
存储过程 Proc 一段带名字的 SQL 重复执行的业务逻辑
触发器 Trigger 数据一变就自动跑 审计、级联更新
约束 Constraint 不许写坏数据的规矩 主键、外键、非空

这六样里,前四样天天用,后两样要慎用。 触发器在 SQL Server 里是「语句级」的, 一次更新一千行只触发一次,但里面要处理一千行的集合——写不好就会拖慢整条语句。 约束比触发器优先。 能用主键、外键、CHECK 解决的问题,不要用触发器——约束是内核执行的,快; 触发器是你写的 SQL,慢而且容易漏。

五、动手 Demo:用两个文件复刻数据文件与日志

这一段先列五个系统库和三类文件,再用 SQLite 建一个库, 把「数据 + 日志」和「文件组」两个机制各演一遍——日志文件会在写入时出现、合并后消失, 第二个文件会独立长大。

import os, sqlite3

db = u"shop.mdf"
c = sqlite3.connect(db)
c.execute(u"PRAGMA journal_mode=DELETE")     # 日志提交后就删
c.execute(u"CREATE TABLE tb (id INTEGER PRIMARY KEY, v TEXT)")
c.executemany(u"INSERT INTO tb VALUES (?,?)",
              [(i, u"第 %d 行" % i) for i in range(1, 2001)])
c.commit()
print(u"数据文件 %d 字节,日志文件 %d 字节"
      % (os.path.getsize(db), os.path.getsize(db + u"-journal")))

c.execute(u"PRAGMA journal_mode=WAL")        # 日志攒着不合并
c.execute(u"INSERT INTO tb VALUES (?,?)", (9999, u"再加一行"))
c.commit()
print(u"数据文件 %d,日志文件 %d"
      % (os.path.getsize(db), os.path.getsize(db + u"-wal")))

# 文件组:第二个文件,独立长大
c.execute(u"ATTACH DATABASE ? AS hist", (u"hist.ndf",))
c.execute(u"CREATE TABLE hist.tb_old (id INTEGER PRIMARY KEY, v TEXT)")
c.executemany(u"INSERT INTO hist.tb_old VALUES (?,?)",
              [(i, u"历史 %d" % i) for i in range(1, 501)])
c.commit()
print(u"次文件 %d 字节" % os.path.getsize(u"hist.ndf"))

实跑输出:

--------------------------------------------------------------------
五个系统库与三类文件
--------------------------------------------------------------------


  ① 五个系统库,各管一摊
  --------------------------------------------------------------------
| 库名 | 管什么 | 坏了会怎样 |
|---|---|---|
| master | 登录名、数据库清单、实例配置 | 整个实例起不来 |
| model | 新建库的模板,所有新库都照它建 | 新建库会带上它的设置 |
| msdb | 作业、备份历史、维护计划 | 代理跑不了,备份没记录 |
| tempdb | 临时表、排序的中间结果 | 每次重启重建,满了全卡 |
| resource | 只读,系统对象和系统存储过程 | 只能靠打补丁更新 |

  master 最特殊:它记着「这个实例上有哪些数据库」。它坏了,实例连启动都过不去。
  所以装完 SQL Server 的第一件事,是备份 master——这句话教材在后面的备份章节还会说一次。
  tempdb 是唯一一个「每次重启都清空重建」的库,所以往里放东西永远是临时的。

  ② 三类文件,职责不同
  --------------------------------------------------------------------
| 扩展名 | 叫什么 | 几个 | 装什么 |
|---|---|---|---|
| .mdf | 主数据文件 | 1 个,必须有 | 数据页、索引页、系统页 |
| .ndf | 次要数据文件 | 0 到多个 | 分担数据,可放不同磁盘 |
| .ldf | 事务日志 | 至少 1 个 | 改之前的旧值,回滚和恢复靠它 |

  日志文件不是「数据的副本」,是「改之前的样子」。
  回滚靠它、恢复到某个时间点也靠它。所以日志和数据必须分开放——
  放同一块磁盘,磁盘一坏两个一起没,日志就白记了。

  ③ 用 SQLite 复刻「数据文件 + 日志文件」
  --------------------------------------------------------------------
  刚建完,两个文件:
    shop.mdf         49152 字节  (数据)
    shop.mdf-journal     0 字节  (日志,提交后就删掉了)

  切成 WAL 模式再写一次:
    shop.mdf         49152 字节  (数据)
    shop.mdf-wal      4152 字节  (日志,攒着没合并)
    shop.mdf-shm     32768 字节  (共享内存索引)
  这就是「数据文件 + 日志文件」的最小模型:写操作先落日志,再慢慢合并进数据文件。
  SQL Server 的 .mdf 和 .ldf 是同一种分工,只是它的日志不会「提交就删」,
  而是一直往后追加,直到你做日志备份或者切到简单恢复模式。

  ④ 文件组:把冷数据放到另一块磁盘
  --------------------------------------------------------------------
  主文件 shop.mdf      49152 字节  tb 表 2001 行
  次文件 hist.ndf      20480 字节  tb_old 表 500 行
  SQL Server 里这叫「文件组」:建表时指定 ON 哪个组,这张表就落在那个组的文件上。
  好处是备份可以只备主组——冷数据那个组一周备一次就够。
  教材 4.1 提了一句「文件组便于管理」,指的就是这个用法。

第一段最能说明「日志是什么」:提交之后 -journal 变成 0 字节——因为那次事务已经安全落盘, 日志的活儿干完了,就该删。 第二段切成 WAL 模式,日志就攒着不删了,一直有内容。 这两种模式对应 SQL Server 的两种恢复模式:简单恢复(日志用完就截断)和完整恢复(日志一直留着, 直到你备份)。 第三段那个 hist.ndf 是第二个文件,它自己独立长大,跟主文件互不影响。这就是文件组的作用: 把不同的数据放在不同的文件(不同的盘)上。 注意这里的「文件组」是复刻出来的语义,不是 SQL Server 的原生机制。 SQLite 用 ATTACH 挂另一个文件,效果上等价于「这张表落在那个文件里」,但它没有文件组的备份、还原、 在线迁移这些能力。

30 秒实验

  1. 打开 SSMS,展开「数据库 → 系统数据库」,右键 tempdb 看属性。 它的「文件」页里,数据文件和日志文件都在,但重启实例之后就全空了。
  2. 建一个测试库,什么都不做,直接去看它的 .mdf 有多大。 你会发现不是 0——初始化时就占了一块。这个块就是 SIZE 参数。
  3. 想一想:为什么日志文件必须和数据文件分盘? 因为日志的用途是「磁盘坏了之后恢复」,跟数据放一起,坏了就一起没了。

常见误区

  1. 「日志文件是数据的备份」。不是。 日志记的是「改之前的样子」,不是「改之后的样子」。要恢复成最新状态,得先还原备份, 再把日志重放一遍——两步,不是一步。
  2. 「日志没用,直接删了省空间」。删了就没法回滚、没法恢复到某个时间点。 正确做法是备份日志或者切简单恢复模式, 让 SQL Server 自己截断——它只会截断「已经用过」的部分。
  3. 「文件组越多越专业」。中小型库一个组就够。 分组的成本在管理上:加文件、调增长、算容量,每组一套。没有明确的冷热分层需求,就不要分。
自测三问(点开对答案) 1. 五个系统库里,哪一个坏了会导致整个实例起不来? —— **master。** 它记着实例上有哪些数据库、有哪些登录名、配置是什么。 所以装完的第一件事是备份 master——它不大,但没有它什么都干不了。 2. `.mdf`、`.ndf`、`.ldf` 各是什么? —— **`.mdf` 主数据文件,每个库一个,必须有;`.ndf` 次要数据文件,可以没有,也可以有多个;** **`.ldf` 事务日志,至少一个。** 数据文件存当前值,日志文件存改之前的旧值。 3. 为什么说「日志文件不是数据文件的备份」? —— **因为方向相反。** 数据文件是「改成什么样」,日志是「改成这样之前是什么样」。 恢复的时候是「先还原数据文件的旧备份,再用日志把中间的过程重放一遍」,两步合起来才恢复。

下一篇 4.2 讲命名规则。 看着是琐碎的一节,但它是「为什么我的建表语句报语法错误」的标准答案——名字里带横线、带空格、 是保留字,各有各的解法。

相关笔记