🛢️ 第5章 存储引擎及数据类型
📄 一句话概括

数据类型只有两条规矩:装得下、不浪费。 装不下会报错或静默截断,浪费会一直摊在磁盘和内存上。 数字走整型,钱走 DECIMAL,字符看长度变不变,时间看要不要跨时区。 VARCHAR(10) 的 10 是字符不是字节;这条规矩救过很多存 emoji 的项目。

📝 本章和前面几章的关系

5.1 讲的是「表用什么引擎存」,这一篇讲「每一列用什么类型装」。 它是第 6 章建表的最后一环:表名、列名、类型、约束,四样凑齐才能建。 这一篇的坑都很贵:钱用 FLOAT 存、时间用字符串存、主键用 VARCHAR 存,改起来都得动全表。

一、数字:整数、定点、浮点三路

MySQL 的数字类型分成三路,走错路的代价不一样。 整数那一路最安全,浮点那一路最容易出「差一分钱」。

路 类型 存什么 准不准
整数 TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT 没有小数点的数 精确
定点 DECIMAL 钱、必须精确的小数 精确
浮点 FLOAT/DOUBLE 测量值、坐标、科学计数 近似

五种整数的范围不用背,记住「n 个字节就是 8n 位」就能推。 有符号要砍掉一位当符号位,所以 TINYINT 是 -128 到 127。 UNSIGNED 不是「加个保险」,它会改变运算规则。 两个无符号数相减得到负数时,MySQL 8.0 直接报错,而不是给你一个负值。 所以「年龄、数量这些肯定非负的字段加 UNSIGNED」这个习惯要谨慎。 还有一个 8.0 的变化:INT(11) 这种括号里的数字,已经不代表长度了。 它只是历史遗留的「显示宽度」,8.0 起除 ZEROFILL 外一律忽略,写不写都一样。 看到老教程说「INT(11) 比 INT(10) 存得多」,那是错的,两者都是 4 字节。

二、小数:DECIMAL 求准,FLOAT 求快

先说清楚为什么会不准:二进制浮点存不了 0.1。 0.1 在二进制里是无限循环小数,只能存一个最近的近似值。 Demo 里真算了一次:0.1 加 0.2 得到 0.30000000000000004441。 换成十进制算就是 0.3,一位不差。

类型 字节 精度 用在哪
FLOAT 4 约 7 位有效数字 传感器读数、评分
DOUBLE 8 约 15 位有效数字 经纬度、科学计算
DECIMAL(M,D) 按 M 定 精确到第 D 位 金额、单价、税率

DECIMAL(8,2) 的两个数字要读对:一共 8 位,其中 2 位在小数点后。 所以整数部分只剩 6 位,最大 999999.99。 这个上限很容易踩:单价字段定成 DECIMAL(8,2),一开始卖几百块没问题, 等有一天要录一台几十万的设备,就报 1264 了。 还有一条顺序要知道:MySQL 是先按小数位四舍五入,再判断整数部分放不放得下。 所以 999999.999 不会安静地存进去,它会先进位成 1000000.00,然后因为越界报错。

三、字符串:CHAR、VARCHAR、TEXT 三家

这三家的分工很清楚:定长、变长、超长。 选错的代价从「浪费一点空间」到「整张表慢下来」都有。

类型 长度怎么写 实际占用 适合
CHAR(n) 0~255,固定 总是 n 个字符 长度几乎不变的:编码、省份、性别
VARCHAR(n) 最长 65535 字节 实际长度+1 或 2 字节 变化大的:姓名、标题、邮箱
TEXT 不用写长度 单独存放,行里留指针 几百字以上、不常整篇查的

第二行的 65535 是字节不是字符,而且它还要跟别的列抢同一行的空间。 一行的总长度有上限(约 65535 字节),所以不能「每列都写 VARCHAR(20000)」。 第三行那个「单独存放」是 TEXT 的关键。 长文本不跟行数据存在一起,所以 SELECT id, title FROM t 不会把正文一起读出来。 但如果 SELECT *,那就全读出来了——这也是「别写 SELECT *」的原因之一。 长度单位是字符,不是字节,这一条从 MySQL 4.1 起就成立了。 VARCHAR(10) 能存 10 个汉字,也能存 10 个 emoji,只是后者占 40 个字节。 前提是字符集得是 utf8mb4——三字节的 utf8 存不下 emoji。 还有一对容易混的:TEXT 和 BLOB。 TEXT 存字符,有字符集和排序规则,能按大小写不敏感比较;BLOB 存字节,没有字符集,只按字节比。 存图片、压缩包用 BLOB,存文章用 TEXT。

四、日期和时间:五种,各有各的边界

这一族最容易出错的地方不是格式,是范围和时区。 教材 5.2.3 列的五种,每一个都有一条硬边界。

类型 范围 用在哪
DATE 1000-01-01 ~ 9999-12-31 生日、营业日
TIME -838:59:59 ~ 838:59:59 时长、班次
DATETIME 1000-01-01 ~ 9999-12-31 本地时间、预约时间
TIMESTAMP 1970-01-01 ~ 2038-01-19 创建时间、更新时间
YEAR 1901 ~ 2155 年份,几乎不用

TIME 那个范围值得停一下:它能到 838 小时,一天多。 因为它存的是「时长」不是「时刻」,所以可以表示一个月的工时。 TIMESTAMP 的 2038 不是传说,是一个确定的日期。 它内部是 32 位有符号整数,存「距 1970 年多少秒」,上限 2147483647 秒, 换成时间就是 2038-01-19 03:14:07 UTC。 TIMESTAMP 和 DATETIME 的真正分水岭是时区。 前者存 UTC 秒数,读出来按会话时区换算;后者存什么读什么。 所以同一行数据,北京的人看到 20:00,伦敦的人看到 12:00,那是 TIMESTAMP 的正常表现。 精度也要显式要。DATETIME 默认只到秒,多出来的小数会被丢掉; 要毫秒就写 DATETIME(3),要微秒就写 DATETIME(6)。

五、还有几种不常用的:ENUM、SET、BIT、JSON

这几种用得少,但见到了要认识,不然会以为是字符串。

类型 实际怎么存 坑在哪
ENUM 内部存 1、2、3 这样的序号 加选项要 ALTER,序号会变
SET 内部存位图,最多 64 个成员 一个字段装多个值,查起来要 FIND_IN_SET
BIT(n) 存二进制位 读写要 b'1010' 这种字面量
JSON 8.0 起是真正的二进制格式 能直接取字段,但改整段要重写

ENUM 那一行是最容易吃亏的:看着像字符串,其实是整数。 ORDER BY 一个 ENUM 列,排的是序号不是字母序,很多人第一次见会以为是 bug。 JSON 那一行是 8.0 的新能力:->> 取标量、JSON_EXTRACT 取片段,还能对某个路径建函数索引。 但它不适合替代正规的表设计——查询、约束、统计都会变麻烦。

六、怎么选:五条判断

教材 5.2 三个小节讲完类型,最后落到「怎么挑」上。五条够用了。

场景 选什么 为什么
金额、单价、税率 DECIMAL(M,2) 浮点会差一分钱
数量、年龄、状态码 整型,最小够用即可 精确、索引快、占得少
长度几乎固定的短串 CHAR(n) 定长不用记长度前缀
长度变化大的串 VARCHAR(n) 按实际长度占空间
文章、评论、日志正文 TEXT 不跟行存在一起,查列表不受拖累

「最小够用」这句要补一句:留出增长余量。 用 TINYINT 存年龄没问题,用 TINYINT 存库存就会在 127 那天出事。 还有一条不在这张表里但很重要:索引列要短。 主键、外键、经常用来查的列,尽量用整型或短的 VARCHAR。 VARCHAR(255) 的列建索引,索引本身就很大——这就是「前缀索引」存在的理由。 最后一句判断标准:如果这一列以后要参与计算或比较,就别用字符串存。 日期存成字符串、金额存成字符串、数量存成字符串,都是「今天省事、明天改全表」的写法。

七、动手 Demo:数据类型的每一条边界都是实的

这一课把五类类型的边界都算一遍、存一遍:整数越界、小数精度、字符字节、时间范围、类型转换。 能在这个沙箱里跑出来的都跑了,跑不出来的在正文里明说。 最后一段会看到一件有意思的事:同一条比较表达式,MySQL 和 SQLite 给出不同答案。

# 一、整型:范围现算 + 真插一个越界值
for name, nb in INTS:
    bits = nb * 8
    print(name, -(2 ** (bits - 1)), 2 ** (bits - 1) - 1, 2 ** bits - 1)
c.execute("INSERT INTO tb_tiny (n) VALUES (?)", (128,))   # IntegrityError

# 二、小数:浮点 vs 十进制
print(0.1 + 0.2)                                    # 0.30000000000000004441
print(Decimal("0.1") + Decimal("0.2"))               # 0.3

# 三、字符串:字符数 vs UTF-8 字节数
c.execute("SELECT length(?), length(CAST(? AS BLOB))", (s, s))

# 四、日期:2038 与精度截断
datetime.fromtimestamp(2 ** 31 - 1, timezone.utc)

# 五、类型转换:文本与数字谁大
c.execute("SELECT '9' > 10")     # SQLite 给 1,MySQL 给 0

实跑输出:

--------------------------------------------------------------------
第二十四课:数据类型的每一条边界都是实的
--------------------------------------------------------------------

  **数据类型不是「写个大概就行」的东西。**
  **选窄了存不进,选宽了白占空间,选错了一年后数据全是错的。**
  **这一课把五类类型的边界都算一遍、存一遍。**
  **凡是能在这个沙箱里跑出来的,都跑;跑不出来的,明说跑不出来。**

  一、整型:先记字节数,范围都是算出来的
  --------------------------------------------------------------------

  类型       字节  有符号范围                                  无符号上限
  ---------------------------------------------------------------------------------
  TINYINT    1     -128 ~ 127                                  255
  SMALLINT   2     -32768 ~ 32767                              65535
  MEDIUMINT  3     -8388608 ~ 8388607                          16777215
  INT        4     -2147483648 ~ 2147483647                    4294967295
  BIGINT     8     -9223372036854775808 ~ 9223372036854775807  18446744073709551615
  **不用背,记住两件事就能推:n 个字节是 8n 位,有符号砍掉一位当符号。**
  **所以 TINYINT 是 -2 的 7 次方到 2 的 7 次方减一。**
  **无符号那一列不是「多一点」,是「翻倍再减一」。**
  **`INT` 从 21 亿变成 42 亿,很多人以为只多一倍——确实是只多一倍,但对存不下的那一行就是救命的。**
  **边界两头的数能进去,越界就进不去。**
  **下面用 CHECK 约束装成 TINYINT 的样子,真插了三个值:**

  插入的值  结果
  ---------------------------------------------------------------
  -128      成功
  127       成功
  128       被拒:CHECK constraint failed: n BETWEEN -128 AND 127
  **真 MySQL 报的是 1264:`Out of range value for column 'n'`。**
  **注意:如果 SQL 模式不严(老版本默认),它会静默截到 127 而不报错。**
  **这就是「数据莫名其妙变了」的来源。**
  **再看一个存不下的:`BIGINT UNSIGNED` 的上限是 18446744073709551615。**
  **这个数超过 2 的 63 次方,而 SQLite 的整数最多到 2 的 63 次方减一。**
  **我试着把它插进去,结果比预想的还干脆:**

  要存的值              结果
  ---------------------------------------------------------------------------------------
  18446744073709551615  绑定阶段就被拒:Python int too large to convert to SQLite INTEGER
  9223372036854775807   这才存得进去
  **不是存进去变形,是连参数都绑不进去。**
  **因为 SQLite 全库只有一种整数类型,上限就是 8 字节有符号。**
  **真 MySQL 的 `BIGINT UNSIGNED` 能存到 18446744073709551615。**
  **这也是「换引擎要照实标差异」的一个实例:同一条 INSERT,一个存进去了,一个连门都进不了。**
  **实践上的意思是:真要用到千万亿这个量级,得先确认目标库支持不支持无符号 8 字节。**
  **换成 `DECIMAL(20,0)` 存更保险,代价是不能再做位运算。**

  二、小数:DECIMAL 求准,FLOAT 求快
  --------------------------------------------------------------------
  **先说最经典的那一道:0.1 加 0.2 到底等于几。**
  **浮点数存不了 0.1 这个值,它只能存一个最近的近似。**
  **这一次真算出来的结果:0.30000000000000004441**
  **换成十进制算:0.3**
  **所以判断「两个钱数相等」永远不能用 `=`。**
  **要么用 DECIMAL 存,要么两边都减一下看差是否小于一个阈值。**
  **再累加一百次 0.01,这一项更能说明问题:**

  算法        一百次之后
  --------------------------------
  浮点累加    1.000000000000000666
  十进制累加  1.00
  正确答案    1.00
  **`DECIMAL(8,2)` 的意思是「一共 8 位,其中 2 位在小数点后」。**
  **所以整数部分只剩 6 位,最大 999999.99。四个值挨个试:**

  要存的值    先舍入到两位  结果
  ---------------------------------------------
  1234.5678   1234.57       存成这个值
  0.005       0.01          存成这个值
  999999.999  1000000.00    舍入后越界,报 1264
  1000000.00  1000000.00    舍入后越界,报 1264
  **第二行的 0.005 被舍成了 0.01,这是正常的四舍五入。**
  **后两行才是坑:999999.999 舍进来刚好破一千万,1000000.00 本来就在外面。**
  **注意顺序:MySQL 是先按小数位四舍五入,再判断整数部分放不放得下。**
  **所以「差一点就存下」的值不会安静地进去,它会先进位,再因为进位越界而报错。**
  **一句话的选法:钱用 `DECIMAL`,坐标用 `DOUBLE`。**
  **`FLOAT` 只在「省空间且不在乎末位」时才考虑。**

  三、字符串:字符数和字节数不是一回事
  --------------------------------------------------------------------
  **`VARCHAR(10)` 里的 10 是「字符」不是「字节」,这是 4.1 之后才成立的规矩。**
  **四字节的字符在 `utf8mb4` 下占 4 个字节,但仍然只算 1 个字符。**
  **四个样本,逐个数一遍字符和字节:**

  内容   字符数  UTF-8 字节数  三字节的 utf8 装得下吗
  ---------------------------------------------------
  abc    3       3             能
  张三   2       6             能
  😀     1       4             不能,要 utf8mb4
  中a😀  3       8             不能,要 utf8mb4
  **第三行那个笑脸占 4 个字节,三字节的 `utf8` 装不下——这就是「存 emoji 报 1366」的全部原因。**
  **换成 `utf8mb4` 就好,MySQL 8.0 的默认字符集已经是它了。**
  **CHAR 和 VARCHAR 的差别在「定长还是变长」,这一条在沙箱里量不到准确的字节数。**
  **能讲清的是规则:`CHAR(10)` 存 `abc` 会在 InnoDB 里补足到定长。**
  **`VARCHAR(10)` 只存 3 个字符加一个长度前缀。**

  类型         存 abc 实际占                                适合存什么
  ------------------------------------------------------------------------------------------
  CHAR(10)     定长,补空格(读回时 MySQL 会去掉尾部空格)  长度几乎不变的:性别、省份、编码
  VARCHAR(10)  3 个字符 + 1 或 2 字节长度前缀              长度变化大的:姓名、标题
  TEXT         单独存放,行里只留指针                       几百字以上、不常整篇查的
  **TEXT 那一行是关键:长文本不跟行存在一起,所以「查列表」时不会把正文一起拖出来。**
  **四个 TEXT 兄弟的上限是算出来的:**

  类型        上限字节    大概能装
  ------------------------------------
  TINYTEXT    255         短备注
  TEXT        65535       文章正文
  MEDIUMTEXT  16777215    一本书
  LONGTEXT    4294967295  一整个资料库
  **它们不是「性能不同」,是「长度上限不同」。**
  **中文字按三字节算,所以 `TEXT` 的 65535 字节大约只装得下两万字。**

  四、日期时间:五种,各有各的范围和精度
  --------------------------------------------------------------------

  类型       字节  范围                                       精度
  --------------------------------------------------------------------------
  DATE       3     1000-01-01 ~ 9999-12-31                    天
  TIME       3     -838:59:59 ~ 838:59:59                     秒(可带小数)
  DATETIME   5~8   1000-01-01 00:00:00 ~ 9999-12-31 23:59:59  秒起,可到微秒
  TIMESTAMP  4~7   1970-01-01 00:00:01 ~ 2038-01-19 03:14:07  秒起,可到微秒
  YEAR       1     1901 ~ 2155                                年
  **第二行的时间范围是「一天以上」——`TIME` 存的是时间段,不是时刻。**
  **所以它能到 838 小时,够存一个多月的工时。**
  **第四行的 `TIMESTAMP` 只有到 2038 年,这一条每次都要单独讲。**
  **它内部就是一个 32 位有符号整数,存的是「距 1970 年多少秒」。**
  **32 位有符号整数的上限是 2147483647 秒,换成时间是:2038-01-19 03:14:07 UTC。**
  **那一刻之后再写 `TIMESTAMP`,就会溢出。**
  **离今天还剩 4099 天,大约 11 年。**
  **所以「什么时候会出事」这个问题有确切答案:2038 年 1 月 19 日。**
  **`DATETIME` 没有这个问题,它存的是字面的年月日时分秒,上限到 9999 年。**
  **精度这一项也能算清。同一个时刻,存进三种精度的列:**

  列类型                 存进去之后
  -------------------------------------------------
  DATETIME(默认 0 位)  2026-10-30 12:34:56
  DATETIME(3)            2026-10-30 12:34:56.789
  DATETIME(6)            2026-10-30 12:34:56.789123
  **五位小数那部分是被丢掉的,不是四舍五入。**
  **要存毫秒就必须把列声明成 `DATETIME(3)`,只写 `DATETIME` 会静默截断。**
  **最后还有一条 `TIMESTAMP` 和 `DATETIME` 的分水岭:时区。**
  **`TIMESTAMP` 存的是 UTC 秒数,读出来按会话时区换算;`DATETIME` 存什么读什么。**

  存进去(UTC 12:00)  会话时区 +08:00 读回
  -----------------------------------------
  TIMESTAMP            2026-10-30 20:00:00
  DATETIME             2026-10-30 12:00:00
  **同一行数据,两个人用不同时区连进来,`TIMESTAMP` 读出不同的字面值,`DATETIME` 读出一样的。**
  **哪个对?看业务:跨时区的下单时间该用 `TIMESTAMP`,本地门店的营业日期该用 `DATETIME`。**

  五、类型转换:两个引擎在这里给出不同答案
  --------------------------------------------------------------------
  **最后这一节是本课最该记住的一节:同一条 SQL,MySQL 和 SQLite 答案不同。**
  **原因不在谁对谁错,在「什么跟什么比、按什么规则比」。**

  表达式      本次实测  MySQL 的结果  差在哪
  -------------------------------------------------------------------------------
  '10' > 9    1         1             两边都成立,但理由不同
  '9' > 10    1         0             SQLite:文本永远大于数字;MySQL:转成数字比
  '9' > '10'  1         1             都是字符串,按字典序比
  'abc' > 0   1         0             MySQL 把 'abc' 转成 0,0 不大于 0
  **第二行是关键:`'9' > 10` 在 MySQL 里是假,在这里是真。**
  **因为 SQLite 定了一条死规矩——任何文本都排在所有数字前面。**
  **MySQL 的规矩是「把字符串转成数字再比」。**
  **`'abc'` 转数字得 0,所以第四行在 MySQL 里是假。**
  **这两种规矩放一起,后果是:同一句 `WHERE`,换了库结果就变。**
  **所以列类型要对齐:字符串列就跟字符串比,别指望数据库帮你转。**
  **最狠的一种情况是隐式转换让索引失效。**
  **`WHERE phone = 13800138000` 这种写法,如果 `phone` 是 `VARCHAR`,**
  **MySQL 会把每一行都转成数字再比,索引直接废掉。**

第一段的表是现算的,所以每一格都是准的。 紧跟着的三行插入里,-128 和 127 成功,128 被 CHECK 拦下来。 真 MySQL 拦它的是 1264,不是 CHECK,但意思一样:越界就是不进。 第二段的 BIGINT UNSIGNED 那一行更有意思:它不是存进去变形,是连参数都绑不进去。 报的是 Python int too large to convert to SQLite INTEGER。 原因很简单:SQLite 只有一种整数类型,上限就是 8 字节有符号。 第三段那两张小数表,浮点累加一百次 0.01 得到 1.000000000000000666。 十进制累加是 1.00。这就是为什么钱的字段不能用 FLOAT——误差会累积。 第四段字符串那张表,看第三行那个笑脸:1 个字符、4 个字节。 三字节的 utf8 装不下它,这就是「存 emoji 报 1366」的全部原因。 第五段是这一课最值钱的地方:'9' > 10,这里给 1,MySQL 给 0。 因为 SQLite 定了一条死规矩——文本永远大于数字;MySQL 是把字符串转成数字再比。 同一句 WHERE,换了库结果就变,这不是 bug,是两套语义。

30 秒实验

  1. 把 DECIMAL(8,2) 改成 DECIMAL(10,2),再算一遍整数部分能装多大。 8 位变 10 位,小数还是 2 位,整数部分从 6 位变成 8 位,上限到 99999999.99。
  2. 把 DATETIME(3) 换成 TIMESTAMP(3),想一想北京和伦敦的人读同一行会看到什么。 看到的时间不一样,因为 TIMESTAMP 存 UTC、按会话时区换算。
  3. 试一下 SELECT CAST('abc' AS INTEGER),再想想为什么「手机号存成整型」是个坏主意。 得到 0,因为转不动就归零。手机号存整型会丢前导零,也存不了带 +86 的格式。

常见误区

  1. 「金额用 FLOAT 存,反正只显示两位小数。」 显示的时候可以舍,累加的时候不能舍。 一百笔 0.01 加完是 1.000000000000000666,对账时就是差一分。
  2. 「INT(11) 比 INT(10) 存得大。」 括号里是显示宽度,8.0 起已经废弃,两者都是 4 字节。 要更大的范围得换类型或用 UNSIGNED。
  3. 「时间存成字符串最省事,什么格式都能写。」 省的是写入时的校验,赔的是范围查询、日期计算和索引。 字符串比日期,比的是字面不是时间。
  4. 「VARCHAR(255) 是万能长度,全写它就行。」 行有总长上限,而且 VARCHAR 越长,索引越大、临时表越占内存。 长度按业务定,别一律 255。
自测三问(点开对答案) 1. `DECIMAL(8,2)` 最大能存多少? —— **999999.99。** **8 位里 2 位给了小数,整数部分只剩 6 位。** 2. 为什么 `TIMESTAMP` 只能到 2038 年,`DATETIME` 能到 9999 年? —— **`TIMESTAMP` 内部是 32 位有符号整数,存的是距 1970 年的秒数,** **2147483647 秒就是 2038-01-19。** **`DATETIME` 存的是字面的年月日时分秒,不靠一个整数表示。** 3. `'9' > 10` 在 MySQL 里是真还是假? —— **假。MySQL 把字符串转成数字再比,9 不大于 10。** **SQLite 给的是真,因为它规定文本永远大于数字。**

第 5 章到这儿结束。下一篇进第 6 章,讲数据表的创建、修改、复制和删除—— 引擎和类型都定了,接下来就该真把表建出来了。

相关笔记