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

CREATE DATABASE 的三组参数:SIZE 起步给多少、MAXSIZE 最多长到哪、 FILEGROWTH 一次长多少。 长到 2 GB:按 10% 长要 56 次、最后一次长 189 MB;按 64 MB 固定长要 32 次、每次都是 64 MB。 DROP DATABASE 没有回收站,它真删文件;库正在被连的时候还删不掉。

📝 本章和前面几章的关系

4.1 讲了库是由哪些文件组成的,4.2 讲了名字怎么起,这一节动手:把库建出来、改参数、再删掉。 教材 4.3 分三小节:创建、修改、删除,最后带一个「小结」和「实践与练习」。 三小节里最容易吃亏的是「修改」——改文件路径那条,SQL Server 不会替你搬文件。 演示里把教材那段 CREATE DATABASE 原码贴出来(SQL Server 专用,本沙箱不跑), 再用 SQLite 做等价动作:建文件、定页大小、算增长次数、删文件。 增长那一段算出来的数是真算的,不是抄的。

一、建库:三组参数决定它以后怎么长

教材给的建库语句是这个样子:

CREATE DATABASE db_shop
ON PRIMARY
(
    NAME = 'db_shop_data',
    FILENAME = 'D:\SQLData\db_shop.mdf',
    SIZE = 10MB,
    MAXSIZE = 500MB,
    FILEGROWTH = 10%
)
LOG ON
(
    NAME = 'db_shop_log',
    FILENAME = 'D:\SQLData\db_shop_log.ldf',
    SIZE = 5MB,
    FILEGROWTH = 1MB
)

这段语法是 SQL Server 专用的,本沙箱没有实例,跑不了。 但它的三组参数值得逐个看:

参数 管什么 写错的后果
SIZE 一建出来就占多大 太小,运行中频繁增长
MAXSIZE 最多能长到多大 不写就是不限,可能吃满盘
FILEGROWTH 一次长多少 太小,增长次数太多

SIZE 不是上限,是起步。 建库时它就直接占掉这么多——演示里 SQLite 建的库只有 32 KB, 而 SQL Server 会一上来就占 10 MB, 这是两种策略:SQLite「用多少占多少」,SQL Server「先占一块地盘」。 先占地的理由是不想让文件在运行中频繁增长。 每次增长都要暂停写入,一秒钟的停顿用户就能感觉到。 所以宁可一开始多占点。 MAXSIZE 建议写。 不写就是不限,一个写错的循环能把整块磁盘吃满——磁盘满了之后,日志写不进去, 整个实例都会拒绝服务,影响的不止这一个库。

二、增长方式:10% 还是固定值

演示里算了一遍:从 10 MB 长到 2 GB,两种方式各要长几次。

增长方式 要长几次 最后一次长多少
每次长 10% 56 次 189 MB
每次长 64 MB 32 次 64 MB

按百分比长,次数看着不多,但最后几次一次要长一百多 MB。那一下的停顿很明显, 而且越到后面越长——这叫「增长惩罚」。 按固定值长,次数多,但每次停顿都一样短。 生产库一般用固定值,比如 64 MB 或 256 MB。 教材给的是 10% 和 1 MB 两个值,其中 1 MB 那个偏小。 日志文件的 FILEGROWTH 更要小心。 日志是顺序追加的, 一次长 1 MB 意味着写几 MB 数据就要长一次。建议日志一次给足,或者按 256 MB 固定增长。 还有一招比调参数更有效:预先给够。 知道这个库一年会长到 20 GB, 建的时候就把 SIZE 设成 20 GB。磁盘空间是一次性分配的,但增长停顿一次都不会有。

三、改库:能改的比想的多

要改什么 写法 要不要停服务
加数据文件 ADD FILE 不用
改增长方式 MODIFY FILE 不用
改排序规则 COLLATE 要独占,得踢人
改文件路径 MODIFY FILE 要离线
改恢复模式 SET RECOVERY 不用

「改文件路径」这一条最容易踩。 很多人以为改完参数 SQL Server 就把文件搬过去了—— 不会。它只改「以后去哪儿找」,文件还在老地方。 正确顺序是四步: 把库设成离线 → 手工把文件搬过去 → 改路径参数 → 再把库设成在线。少一步就报「找不到文件」, 而且这时候库已经离线了,只能再改回来。 「改排序规则」要独占访问。 也就是所有连接都得断掉,否则报「数据库正在使用」。 生产库上干这件事得挑时间窗口,因为它会重建所有索引——大库可能要几十分钟。 「改恢复模式」不用停。 从完整切到简单,日志会在下一次检查点时被截断, .ldf 会突然变小——这个操作经常被用来「清理日志文件」,比 SHRINKFILE 更合理。

四、删库:没有回收站

DROP DATABASE 做三件事:从 master 里摘掉登记、删数据文件、删日志文件。演示里真删了一个文件, 删之前 32 KB,删之后连文件都不在了。 没有回收站。 要么事先有备份,要么就真没了。 教材在这一节末尾提醒「删除前先备份」,只有一句话,但它是这一节代价最高的一句。 库正在被连接时删不掉。 会报「无法删除正在使用的数据库,因为该数据库当前正在使用」。 解决办法有两个:切到别的库再删,或者先 SET SINGLE_USER WITH ROLLBACK IMMEDIATE 把所有人踢掉。 DROP 和 DELETE 差一个字,后果差一个量级。 DELETE 删行,能回滚; DROP 删对象,删完连表结构都没了。在正式库上敲 DROP 之前,先看一眼连接的是哪个库。

五、动手 Demo:建一个库、算一遍增长、再删掉

这一段先贴教材的 CREATE DATABASE 原码(本沙箱不执行),再用 SQLite 做等价动作:建文件、量页数、 算两种增长方式的次数、列出能改的参数,最后真删一次。

import os, sqlite3

db = u"db_shop.sqlite"
c = sqlite3.connect(db)
c.execute(u"PRAGMA page_size=4096")
c.execute(u"CREATE TABLE tb_goods (id INTEGER PRIMARY KEY, name TEXT, price REAL)")
c.executemany(u"INSERT INTO tb_goods (name, price) VALUES (?,?)",
              [(u"商品 %d" % i, i * 1.5) for i in range(1, 1001)])
c.commit()
c.execute(u"PRAGMA wal_checkpoint(FULL)")
pages = c.execute(u"PRAGMA page_count").fetchone()[0]
ps = c.execute(u"PRAGMA page_size").fetchone()[0]
print(u"%d 页 x %d 字节 = %d 字节" % (pages, ps, pages * ps))

# 两种增长方式,各要长几次才到 2 GB
for label, mode in ((u"10%", u"pct"), (u"64MB", u"fix")):
    size, n = 10.0, 0
    while size < 2048 and n < 400:
        grow = size * 0.10 if mode == u"pct" else 64.0
        size = min(size + grow, 2048)
        n += 1
    print(u"%s:%d 次,最后一次长 %.0f MB" % (label, n, grow))

# 删库:真删文件
before = os.path.getsize(db)
c.close()
os.remove(db)
print(u"删前 %d 字节,删后文件还在吗:%s"
      % (before, os.path.exists(db)))

实跑输出:

--------------------------------------------------------------------
建库、改库、删库
--------------------------------------------------------------------


  ① 教材原码:CREATE DATABASE
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE DATABASE db_shop
  | ON PRIMARY
  | (
  |     NAME = 'db_shop_data',
  |     FILENAME = 'D:\SQLData\db_shop.mdf',
  |     SIZE = 10MB,
  |     MAXSIZE = 500MB,
  |     FILEGROWTH = 10%
  | )
  | LOG ON
  | (
  |     NAME = 'db_shop_log',
  |     FILENAME = 'D:\SQLData\db_shop_log.ldf',
  |     SIZE = 5MB,
  |     FILEGROWTH = 1MB
  | );

  这段是 SQL Server 专用语法,本沙箱没有实例,跑不了。
  能跑的是它的等价动作:建一个文件、定初始大小、定增长方式。
  建库:db_shop.sqlite
    页大小 4096 字节,共 8 页,32768 字节
    文件实际 32768 字节
  注意这里只有 32 KB,而教材那个 SIZE = 10MB 是「一建出来就占 10MB」。
  SQLite 是「用多少占多少」,SQL Server 是「先占一块地盘」——两边策略不同。
  先占地的理由是不想让文件在运行中频繁增长:每次增长都要暂停写入。
  所以 SQL Server 那个 SIZE 参数不是「上限」,是「起步就给你这么多」。

  ② 增长参数:10% 还是固定 64MB
  --------------------------------------------------------------------
| 增长方式 | 长到 2 GB 要长几次 | 最终大小 | 最后一次长多少 |
|---|---|---:|---:|
| 每次长 10% | 56 | 2048 MB | 189 MB |
| 每次长 64MB | 32 | 2048 MB | 64 MB |

  按百分比长,次数看着少,但最后几次一次要长一百多 MB,那一下的停顿很明显;
  按固定 64MB 长,次数多,每次停顿都一样短。生产库一般用固定值。
  教材给的是 10% 和 1MB 两个值。1MB 那个偏小,日志会频繁增长——
  日志文件建议一次给足,或者按 256MB 固定增长。

  ③ 改库:能改的比想象的多
  --------------------------------------------------------------------
| 要改什么 | 写法 | 要不要停服务 |
|---|---|---|
| 加数据文件 | ADD FILE | 不用 |
| 改增长方式 | MODIFY FILE | 不用 |
| 改排序规则 | COLLATE | 要独占,得踢人 |
| 改文件路径 | MODIFY FILE | 要离线 |
| 改恢复模式 | SET RECOVERY | 不用 |

  「改文件路径」这一条最容易踩:不是改完就搬,得先把库设成离线、手工搬文件、再改路径、再上线。
  SQL Server 不会替你搬——它只改「以后去哪儿找」,文件还在老地方,上线就报找不到。

  ④ 删库:DROP 之后就是删了
  --------------------------------------------------------------------
  DROP 前:db_shop.sqlite  32768 字节
  DROP 后:db_shop.sqlite  0 字节(文件都不在了)
  SQL Server 的 DROP DATABASE 做三件事:摘掉登记、删数据文件、删日志文件。
  没有回收站。要么事先有备份,要么就真没了。
  教材在这一节末尾提醒「删除前先备份」,一句话,但代价最高。
  还有一条:库正在被连接时删不掉,会报「无法删除正在使用的数据库」。
  要先切到别的库、或者用 SINGLE_USER 把人踢掉,才能删。

第一段量出来的是 32 KB,而教材那段写的是 SIZE = 10MB。 这不是矛盾,是策略不同:SQLite 用多少占多少,SQL Server 先占一块地盘。 先占地的理由是不想让文件在运行中频繁增长。 第二段的两个数要放在一起看:56 次对 32 次,189 MB 对 64 MB。按百分比长看着省事, 但最后几次的停顿会越来越长——这叫「增长惩罚」。 第三段是真删。 删之前 32 KB,删之后 os.path.exists 返回 False——不是「移到了回收站」, 是文件没了。SQL Server 的 DROP DATABASE 一样的道理。 这段没有一个字是 SQL Server 的 T-SQL。 它演的是机制:文件怎么来、增长怎么算、删除意味着什么。 真在 SQL Server 上跑,用的是 4.3 开头贴的那段原码。

30 秒实验

  1. 在 SSMS 里建一个测试库,SIZE 写 5 MB、FILEGROWTH 写 1 MB,然后写个循环插几万行, 盯着文件属性的「大小」和「可用空间」两个数。你会看到「大小」一次次跳,这就是增长。
  2. 把 FILEGROWTH 改成 64 MB 再跑一遍,数一下任务管理器里磁盘写入的尖峰少了多少次。
  3. 想一想:如果一个库的 .ldf 有 40 GB,但数据只有 2 GB,最合理的处理是什么? 先切简单恢复模式让日志截断,再 SHRINKFILE 收回来——直接删文件会让库打不开。

常见误区

  1. 「改文件路径就是搬文件」。SQL Server 只改「以后去哪儿找」,不搬。 正确顺序是:离线 → 手工搬 → 改路径 → 在线。顺序错了库就起不来,而且它已经离线了, 只能再改回去。
  2. 「日志文件太大,删掉就行」。删 .ldf 会让库直接打不开——SQL Server 认为数据不完整。 正确做法是切简单恢复模式,让日志自己截断,再收空间。
  3. 「MAXSIZE 不用写,反正磁盘够大」。写错的循环能把盘吃满。 盘满了日志写不进去,整个实例拒绝服务,影响所有库——不只是那一个。 给 MAXSIZE 一个合理的上限,是给自己留条后路。
自测三问(点开对答案) 1. `SIZE = 10MB` 是上限还是起步? —— **是起步。** 建库时就直接占掉 10 MB,不是「最多 10 MB」。上限是 `MAXSIZE` 那个参数。 这两个经常被搞混,结果就是「为什么我刚建的库占了 10 MB」。 2. 从 10 MB 长到 2 GB,10% 和 64 MB 各要长几次? —— **10% 要 56 次,最后一次长 189 MB;64 MB 要 32 次,每次都是 64 MB。**次数看着差得不多, 但百分比的最后几次停顿很长——所以生产库用固定值。 3. `.ldf` 涨到 40 GB,能不能直接删? —— **不能,删了库就打不开。** 正确做法是切简单恢复模式让日志截断(或者先做一次日志备份), 再 `SHRINKFILE` 把空间收回来。**直接删文件是最常见的一种「把库弄坏」的方式。**

第 4 章到此结束。 下一篇进第 5 章 5.1,讲数据表: 字段类型怎么选、NULL 和空字符串差在哪、IDENTITY 自增列为什么不能自己插值。

相关笔记