🛢️ 第4章 数据库操作
📄 一句话概括

建库只有一条语句,但写法有四种,差别全在「库已存在」那一刻。 脚本里必须写 IF NOT EXISTS,否则跑第二遍就停在那儿。 建库几乎不花时间:它只在磁盘上建一个目录,不预分配空间。

📝 本章和前面几章的关系

上一篇认清了库是什么,这一篇开始建。 它是本章第一段真语法,后面查看、选择、修改、删除都是围着它转。 建库时定的字符集,会一路继承到后面建的表,所以这一步别糊弄。 和第 5 章的关系:库建错了字符集,第 5 章建表时就只能一个个显式指定,很麻烦。

一、四种写法,一个比一个稳

教材给了四种,其实是一条语句的四种变体。

写法 库已存在时 什么时候用
CREATE DATABASE 库名 报错 1007 一次性手工建
CREATE SCHEMA 库名 报错 1007 同上,等价写法
CREATE DATABASE IF NOT EXISTS 库名 静默跳过 初始化脚本必须这么写
CREATE DATABASE 库名 CHARACTER SET ... 报错 1007 显式指定字符集

第三种的「静默」是关键:它不报错,也不提示,只是什么都不做。 所以脚本跑完你看不出它到底建了还是跳过——要确认得再查一次。

二、为什么要幂等

「幂等」是这么一件事:同一份脚本跑一遍和跑十遍,结果一样。 初始化脚本必须幂等,因为它一定会被重复执行。

场景 不幂等会怎样
新机器初始化 第一次成功,第二次停在 1007
容器重启自动执行 每次重启都失败,日志全是红的
同事拿到脚本重跑 以为脚本坏了

这条规矩不只适用于建库,建表、插初始数据都一样。 CREATE TABLE IF NOT EXISTS、INSERT ... ON DUPLICATE KEY UPDATE,都是同一个思路。

三、建库时定字符集,别等以后改

CREATE DATABASE 库名 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; 这一句里两个参数:字符集管「能存什么」,校对规则管「怎么比」。 8.0 起默认就是 utf8mb4,所以不写也对。 但生产上建议显式写——默认值会随版本变,写下来才不会被升级改掉。 为什么不写会出事?举个具体的:5.7 时代默认是 utf8(三字节)。 一个 5.7 建的库升到 8.0,字符集不会跟着变,数据文件里就是三字节的东西。 之后用户存一个 emoji,报 Incorrect string value。

四、建库这一步在磁盘上做了什么

几乎什么都没做。 它只在数据目录下建一个同名子目录,写一条元数据记录。

动作 花多少时间 占多少空间
建库 毫秒级 几十字节的元数据
建表 毫秒级 一个空的表空间文件
插数据 看行数 真正开始吃磁盘
建索引 看行数 可能和数据一样大

所以「库太多会不会拖慢」这个问题,答案是「不会」。 库只是目录,真正影响性能的是表、索引和数据量。 反过来说,「库多」会让人找不到东西。 一个实例上几百个库,谁也说不清哪个是哪个——这是管理问题,不是性能问题。

五、动手 Demo:建库的四种写法,和它们各自的下场

这一课把四种写法都跑一遍,看报错和不报错的分界。 沙箱没有 MySQL,所以造了一个最小模型:一个库一个目录,元数据存在 meta.json 里。

import json, os, shutil

class Catalog(object):
    def create(self, name, charset="utf8mb4",
               collate="utf8mb4_0900_ai_ci", if_not_exists=False):
        if os.path.isdir(self._d(name)):
            if if_not_exists:
                return "0 行受影响(库已存在,跳过)"
            raise ValueError("ERROR 1007 (HY000): Can't create database "
                             "'%s'; database exists" % name)
        os.makedirs(self._d(name))
        self._save(name, {"charset": charset, "collate": collate})
        return "Query OK, 1 row affected"

cat = Catalog(DATA)
print(cat.create("db_database28"))          # 第一次,成功
try:
    print(cat.create("db_database28"))      # 第二次,1007
except ValueError as e:
    print(e)
print(cat.create("db_database28", if_not_exists=True))   # 静默跳过

实跑输出:

--------------------------------------------------------------------
第十八课:建库的四种写法,和它们各自的下场
--------------------------------------------------------------------

  **教材 4.2 节给了四种建库写法,差别全在「库已存在」那一刻。**
  **这一课把这四种都跑一遍,看报错和不报错的分界。**

  一、教材原码:四种写法
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE DATABASE db_database28;
  | CREATE SCHEMA db_database28;
  | CREATE DATABASE IF NOT EXISTS db_database28;
  | CREATE DATABASE db_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
    (建库、等价写法、带判断、指定字符集与校对规则)


  二、真跑:第二次建同一个库会怎样
  --------------------------------------------------------------------

  写法                      情形      结果
  ---------------------------------------------------------------------------------------
  CREATE DATABASE           第一次    Query OK, 1 row affected
  CREATE DATABASE           第二次    ERROR 1007 (HY000): Can't create database 'db_data…
  IF NOT EXISTS             库已存在  0 行受影响(库已存在,跳过)
  CREATE ... CHARACTER SET  新库      Query OK, 1 row affected
  **第二行那个 1007 就是「库已存在」。**
  **脚本里不写 `IF NOT EXISTS`,跑第二遍就停在这儿。**
  **初始化脚本必须写成第三种。**
  **幂等——同样一份脚本跑一遍和跑十遍,结果必须一样。**

  三、建库到底在磁盘上做了什么
  --------------------------------------------------------------------

  库名           落地成什么  占多少字节
  -------------------------------------
  db_database28  一个目录    55
  db_shop        一个目录    55
  **MySQL 的库就是一个目录(8.0 在 `datadir` 下,名字就是库名)。**
  **所以「建库」这一步几乎不花时间,也不预分配空间。**
  **这一点决定了后面一件事:库本身不占空间,占空间的是表和索引。**

注意那个 if_not_exists 参数:它把「报错」和「跳过」做成了一个开关。 MySQL 内部也是这么实现的——同一段代码,多一个判断。 第三行那个 1007 是 MySQL 的错误码,真机上措辞一模一样: Can't create database 'xxx'; database exists。 记住这个码,比记住整句话有用。 第四行「0 行受影响」看着像失败,其实成功了——它只是什么都没做。 所以脚本跑完想确认结果,得再查一次库列表,不能只看这条语句的返回。 最后那张表里,两个库都是 55 字节,就是 meta.json 的大小。 这个数说明「建库几乎不占空间」,也说明「库大多不是问题」。

30 秒实验

  1. 把 Demo 里第一次那句也加上 if_not_exists=True,再跑一遍。 两次都返回「0 行受影响」和「Query OK」的混合结果, 这就是幂等脚本的样子:不管从什么状态开始,最后都是同一个状态。
  2. 把 CREATE DATABASE 换成 CREATE SCHEMA,跑一遍。结果完全一样。 SQL 标准里叫 schema,MySQL 两个词都认。
  3. 想一想:为什么不在 CREATE DATABASE 时就把大小定下来? 因为库的大小取决于里面的表和索引,事先定不下来。 能定的是「这个库最多用多少空间」,那是配额,是另一套机制。

常见误区

  1. 「脚本里有 CREATE DATABASE 就够了,不用加 IF NOT EXISTS。」 跑第二遍就停在 1007。 初始化脚本一定会被重复执行,必须幂等。
  2. 「建库不写字符集,到时候再改。」 改库只改「以后新建的表」,老表一张都不动。 建库时那句 CHARACTER SET utf8mb4 是最便宜的一次保险。
  3. 「库建多了会拖慢 MySQL。」 不会。库就是目录,几乎不占空间也不耗性能。 真正拖慢的是表、索引和数据量;库多只是让人找不到东西。
自测三问(点开对答案) 1. 第二次执行 `CREATE DATABASE db_database28;` 会怎样? —— **报 1007:`database exists`。** **加上 `IF NOT EXISTS` 就变成静默跳过。** 2. 为什么初始化脚本必须幂等? —— **因为它一定会被重复执行**:新机器跑一次、容器重启跑一次、同事拿到再跑一次。 **不幂等的脚本,第二次就一定失败。** 3. 建库时最该显式写出来的两个参数是什么? —— **字符集(CHARACTER SET)和校对规则(COLLATE)。** **不写会跟着版本默认值走,升级时可能悄悄变。**

下一篇讲怎么看库:看列表、看定义、看占地,三种视角对应三条完全不同的语句。

相关笔记