4.数据类型【由浅入深-MySQL】

文章目录

  • 第一章:数据类型分类
  • [第二章: 数值类型:整型](#第二章: 数值类型:整型)
    • [2.1 整型列定义的完整语法](#2.1 整型列定义的完整语法)
    • [2.2 五种整型的存储本质与取值范围](#2.2 五种整型的存储本质与取值范围)
    • [2.3 默认有符号与显式无符号(UNSIGNED)的实战差异](#2.3 默认有符号与显式无符号(UNSIGNED)的实战差异)
    • [2.4 MySQL 对非法值的"铁腕拦截":区别于编程语言的内存截断](#2.4 MySQL 对非法值的“铁腕拦截”:区别于编程语言的内存截断)
    • [2.5 约束的本质:让数据库成为规范执行者](#2.5 约束的本质:让数据库成为规范执行者)
    • [2.7 BIT 类型:二进制位存储与客户端显示陷阱](#2.7 BIT 类型:二进制位存储与客户端显示陷阱)
      • [2.7.1 定义语法与结构查看](#2.7.1 定义语法与结构查看)
      • [2.7.2 插入操作:数值范围与隐式转换](#2.7.2 插入操作:数值范围与隐式转换)
      • [2.7.3 查询显示的"空白"之谜:ASCII 渲染机制](#2.7.3 查询显示的“空白”之谜:ASCII 渲染机制)
      • [2.7.4 可靠查看 BIT 数据的最佳实践](#2.7.4 可靠查看 BIT 数据的最佳实践)
        • [补充:`HEX(column)`、`BIN(column)` 和 `CAST(column AS UNSIGNED)` 都是 MySQL 中**对数据进行类型转换或查看不同表示形式的函数**。`HEX(column)` 用于把字段值转换成**十六进制形式**显示,例如数字 `10` 转换为 `A`(十六进制表示),常用于查看二进制数据或字符编码;`BIN(column)` 用于把整数转换成**二进制形式**显示,例如 `10` 转换为 `1010`,方便查看数据的二进制结构;`CAST(column AS UNSIGNED)` 用于把字段转换成**无符号整数类型**,例如字符串 `'123'` 转换成数字 `123`,便于进行数学运算或比较。简单理解:**HEX看十六进制,BIN看二进制,CAST负责把一种数据类型转换成另一种数据类型。**](#补充:HEX(column)BIN(column)CAST(column AS UNSIGNED) 都是 MySQL 中对数据进行类型转换或查看不同表示形式的函数HEX(column) 用于把字段值转换成十六进制形式显示,例如数字 10 转换为 A(十六进制表示),常用于查看二进制数据或字符编码;BIN(column) 用于把整数转换成二进制形式显示,例如 10 转换为 1010,方便查看数据的二进制结构;CAST(column AS UNSIGNED) 用于把字段转换成无符号整数类型,例如字符串 '123' 转换成数字 123,便于进行数学运算或比较。简单理解:HEX看十六进制,BIN看二进制,CAST负责把一种数据类型转换成另一种数据类型。)
  • [第三章 数值类型:小数](#第三章 数值类型:小数)
    • [3.1 小数类型定义语法全解析](#3.1 小数类型定义语法全解析)
    • [3.2 三种类型的本质区别与存储特性](#3.2 三种类型的本质区别与存储特性)
    • [3.3 UNSIGNED 在小数领域的"不翻倍"规则](#3.3 UNSIGNED 在小数领域的“不翻倍”规则)
    • [3.4 四舍五入与越界拦截:先舍入,后判定](#3.4 四舍五入与越界拦截:先舍入,后判定)
    • [3.5 精度对决:为什么大数用 FLOAT 会"不准"?](#3.5 精度对决:为什么大数用 FLOAT 会“不准”?)
    • [3.6 DECIMAL 的四舍五入与越界拒绝(重申与强化)](#3.6 DECIMAL 的四舍五入与越界拒绝(重申与强化))
    • [3.7 综合实战与选型建议](#3.7 综合实战与选型建议)
  • [第四章 字符串类型](#第四章 字符串类型)
    • [4.1 字符(L)与字节的底层分野](#4.1 字符(L)与字节的底层分野)
    • [4.2 CHAR(L):固定长度的"规矩派"](#4.2 CHAR(L):固定长度的“规矩派”)
    • [4.3 VARCHAR(L):可变长度的"实用派"](#4.3 VARCHAR(L):可变长度的“实用派”)
    • [4.4 关于 UTF-8 下字符字节数的关键纠正与深度说明](#4.4 关于 UTF-8 下字符字节数的关键纠正与深度说明)
    • [4.5 VARCHAR 的"隐藏枷锁":行大小对其他列的影响](#4.5 VARCHAR 的“隐藏枷锁”:行大小对其他列的影响)
    • [4.6 BLOB 与 TEXT:大文本与二进制数据的归宿](#4.6 BLOB 与 TEXT:大文本与二进制数据的归宿)
  • [第五章 字符串类型------日期与时间类型](#第五章 字符串类型——日期与时间类型)
    • [5.1 三大核心日期时间类型概览](#5.1 三大核心日期时间类型概览)
    • [5.2 DATE 类型 ------ 纯粹的日历日期](#5.2 DATE 类型 —— 纯粹的日历日期)
    • [5.3 DATETIME 类型 ------ 固定时区的日期时间](#5.3 DATETIME 类型 —— 固定时区的日期时间)
    • [5.4 TIMESTAMP 类型 ------ 自动时区转换的"活"时间](#5.4 TIMESTAMP 类型 —— 自动时区转换的“活”时间)
    • [5.5 实操演示:三者的建表与自动更新机制](#5.5 实操演示:三者的建表与自动更新机制)
      • [5.5.1 插入数据](#5.5.1 插入数据)
      • [5.5.2 更新操作与自动时间戳](#5.5.2 更新操作与自动时间戳)
    • [5.6 三者的选择决策树](#5.6 三者的选择决策树)
  • [第六章 字符串类型------ENUM 与 SET:约束型字符串的利器](#第六章 字符串类型——ENUM 与 SET:约束型字符串的利器)
    • [6.1 ENUM 与 SET 的定位与存储差异](#6.1 ENUM 与 SET 的定位与存储差异)
    • [6.2 ENUM 类型 ------ 严谨的单选约束](#6.2 ENUM 类型 —— 严谨的单选约束)
      • [6.2.1 基本定义与合法取值](#6.2.1 基本定义与合法取值)
      • [6.2.2 插入数据:既可以用字符串,也可以用下标索引](#6.2.2 插入数据:既可以用字符串,也可以用下标索引)
      • [6.2.3 ENUM 的查询陷阱:下标与字符串的微妙差异](#6.2.3 ENUM 的查询陷阱:下标与字符串的微妙差异)
    • [6.3 SET 类型 ------ 灵活的多选位图](#6.3 SET 类型 —— 灵活的多选位图)
      • [6.3.1 定义与成员排序](#6.3.1 定义与成员排序)
      • [6.3.2 插入多值:字符串组合与位图数字](#6.3.2 插入多值:字符串组合与位图数字)
      • [6.3.3 重要区分:NULL 与空字符串('')的区别](#6.3.3 重要区分:NULL 与空字符串('')的区别)
    • [6.4 ENUM 与 SET 的高级查询技巧](#6.4 ENUM 与 SET 的高级查询技巧)
      • [6.4.1 SELECT 不仅是查表,更是计算器与函数容器](#6.4.1 SELECT 不仅是查表,更是计算器与函数容器)
      • [6.4.2 SET 的精确匹配查询](#6.4.2 SET 的精确匹配查询)
      • [6.4.3 查找包含单个元素的记录:FIND_IN_SET](#6.4.3 查找包含单个元素的记录:FIND_IN_SET)
      • [6.4.4 查找包含多个元素的记录:使用 AND 逻辑组合](#6.4.4 查找包含多个元素的记录:使用 AND 逻辑组合)
      • [6.5 ENUM 和 SET 的最佳实践与避坑指南](#6.5 ENUM 和 SET 的最佳实践与避坑指南)
      • [6.6 总结](#6.6 总结)

第一章:数据类型分类

第二章: 数值类型:整型

设计一张表时,最先确定的往往是字段的数值类型。MySQL将整数类型细分为五种,并非为了制造选择困难,而是让开发者能够根据业务数值的实际范围,精准地平衡存储空间与性能。本章我们将从一条完整的列定义语法出发,逐步深入到每种整型的存储边界,并通过大量插入操作来观察MySQL的"拦截"行为------这不仅是语法的学习,更是对数据库数据完整性保障机制的第一次亲密接触。

2.1 整型列定义的完整语法

在MySQL中,定义一个整型列的完整语法结构如下:

sql 复制代码
col_name data_type [UNSIGNED] [ZEROFILL] [NOT NULL] [DEFAULT default_value] [AUTO_INCREMENT]
  • col_name:字段名称。
  • data_type :五种整型之一(TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT),可以附加一个可选的显示宽度 ,如 INT(11)。注意,这个宽度不限制存储范围 ,仅用于某些客户端工具显示时的填充宽度,且必须与 ZEROFILL 配合才有效,属于历史遗留特性,现代开发中不推荐依赖它来做任何逻辑约束。
  • UNSIGNED:将类型切换为无符号,取值下限从负数变为0,上限翻倍。
  • ZEROFILL自动为该列添加 UNSIGNED 属性,并在查询结果中左补零至显示宽度。实际存储的数值不变,只是展示格式变化,业务层不应依赖此特性。
  • NOT NULL :约束该列不可存储 NULL 值,插入时必须显式赋值。
  • DEFAULT:指定默认值,当插入未显式赋值时使用此值。
  • AUTO_INCREMENT :自增属性,只能用于整数类型,且该列必须定义为 NOT NULL(通常也作为主键)。每张表最多一个自增列。

语法实例:创建一个包含多种整型定义的用户信息表。

sql 复制代码
CREATE TABLE user_demo (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄,无符号微整型',
    score SMALLINT NOT NULL COMMENT '分数,有符号短整型,不允许空',
    big_num BIGINT UNSIGNED ZEROFILL DEFAULT NULL COMMENT '大数,展示时补零',
    PRIMARY KEY (id)
);

补充:AUTO_INCREMENT

AUTO_INCREMENT(自增)就是让 MySQL 自动给整数列生成连续编号,不用我们手动填写 。例如用户表里的 id,第一个用户插入时自动得到 1,第二个用户自动得到 2,依次递增。因为编号必须是数字,所以只能用于整数类型(如 INT);为了保证每条数据都有唯一编号,这一列不能没有值,所以必须是 NOT NULL,通常会把它设置成主键。 一张表只能有一个自增列,因为一个表通常只需要一个自动编号规则。

例如:

sql 复制代码
CREATE TABLE user (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(20)
);

插入数据:

sql 复制代码
INSERT INTO user(name) VALUES ('张三');
INSERT INTO user(name) VALUES ('李四');

不用写 id,MySQL 会自动生成:

id name
1 张三
2 李四

你可以把 AUTO_INCREMENT 理解成:"这个字段是系统自动发放的编号,像身份证号码一样,每新增一条数据自动+1。"

2.2 五种整型的存储本质与取值范围

无论定义语法多么花哨,整型在磁盘上最终只占用固定长度的字节。五种类型背后是对字节数的精打细算:

类型 字节数 有符号最小值 → 最大值 无符号最小值 → 最大值
TINYINT 1 字节(8 bit) -128 ~ 127 0 ~ 255
SMALLINT 2 字节(16 bit) -32,768 ~ 32,767 0 ~ 65,535
MEDIUMINT 3 字节(24 bit) -8,388,608 ~ 8,388,607 0 ~ 16,777,215
INT 4 字节(32 bit) -2,147,483,648 ~ 2,147,483,647 0 ~ 4,294,967,295
BIGINT 8 字节(64 bit) -9.22×10¹⁸ ~ 9.22×10¹⁸ 0 ~ 1.84×10¹⁹

重构后的核心认知 :选择哪种类型,首先要问"业务中该字段可能出现的最值是多少",然后选择刚好能容纳 的最小字节类型。例如,TINYINT 完全适合存储性别状态(0/1)、年龄(0-120);SMALLINT 可存全国城市代码;INT 是主键最常用的选择;只有分布式ID或时间戳毫秒数才需要考虑 BIGINT

2.3 默认有符号与显式无符号(UNSIGNED)的实战差异

MySQL 中整数类型默认是有符号的 ,即若省略 UNSIGNED,则自动按有符号取值范围处理。下面我们通过两张表的对比操作来直观感受差异。

场景一:默认有符号的 TINYINT

sql 复制代码
CREATE TABLE test_signed (
    num TINYINT   -- 未加 UNSIGNED,默认为有符号
);

-- 插入边界值:127 成功,128 失败
INSERT INTO test_signed (num) VALUES (127);   -- Query OK
INSERT INTO test_signed (num) VALUES (128);   
-- 报错:Out of range value for column 'num' at row 1

INSERT INTO test_signed (num) VALUES (-128);  -- Query OK
INSERT INTO test_signed (num) VALUES (-129);  
-- 报错:Out of range value for column 'num' at row 1

场景二:显式无符号(UNSIGNED)

sql 复制代码
CREATE TABLE test_unsigned (
    num TINYINT UNSIGNED   -- 显式声明无符号
);

-- 插入 255 成功,256 失败
INSERT INTO test_unsigned (num) VALUES (255);   -- Query OK
INSERT INTO test_unsigned (num) VALUES (256);   
-- 报错:Out of range value for column 'num' at row 1

-- 尝试插入负数,直接拦截
INSERT INTO test_unsigned (num) VALUES (-1);    
-- 报错:Out of range value for column 'num' at row 1

从上述实例可以清晰看到,UNSIGNED 使得列完全拒绝负数,同时允许的正数上限提高一倍。这非常适用于"数量"、"点击量"、"等级"等语义上不可能为负的字段,同时还能扩大正数范围。

2.4 MySQL 对非法值的"铁腕拦截":区别于编程语言的内存截断

如果您有 C/C++ 背景,一定很熟悉这样的场景:将 300 赋值给一个 signed char 变量,编译器不会报错,只会默默地截断高位,最终得到 44(300 - 256)。这种静默截断在系统底层有时可以接受,但在数据库领域却是灾难------它会悄悄篡改业务数据,造成报表错误、逻辑混乱,且很难追溯。

MySQL 完全抛弃了这种"宽容"策略。当插入的数值超出该列类型所定义的合法区间时,MySQL 会直接拒绝本次操作 ,并返回明确的错误信息(如 Out of range value for column '...')。如果当前处于事务中,该语句导致整个事务回滚(视隔离级别和错误处理机制)。

实例:尝试向 SMALLINT 插入超出范围的数值

sql 复制代码
CREATE TABLE test_range (
    score SMALLINT
);

-- 成功:32767 是 SMALLINT 有符号最大值
INSERT INTO test_range (score) VALUES (32767);

-- 失败:32768 超出 1,直接被拦截
INSERT INTO test_range (score) VALUES (32768);
-- ERROR 1264 (22003): Out of range value for column 'score' at row 1

这种机制带来的一个重要推论是:任何成功写入 MySQL 的数据,其类型一定是合法的。当我们从表中查询数据时,无需再编写额外的应用层校验来检查数值是否越界,因为数据库已经帮我们做了第一道(也是最严格的一道)防线。这极大地减轻了程序员的负担,也让数据库中的数据状态变得"可预期"。

2.5 约束的本质:让数据库成为规范执行者

通过上面的所有例子,我们可以总结出整型在 MySQL 中的终极角色------它不仅仅是存储格式,更是一种强约束 (Constraint)。这种约束面向的是数据写入者(即应用程序或DBA),它强制要求:

  1. 插入的数值必须在类型定义的范围内。
  2. 若使用 UNSIGNED,负值彻底失去写入资格。
  3. 若使用 NOT NULL,则不允许省略该列值(除非有 DEFAULT)。
  4. 若使用 AUTO_INCREMENT,则插入时必须保证唯一性或依赖数据库自动生成。

这种设计哲学倒逼程序员在开发阶段就认真考虑字段的合法取值范围,将错误消灭在数据进入数据库之前。反过来,如果您在使用过程中发现频繁出现"Out of range"错误,恰恰说明数据模型设计不合理(例如用 TINYINT 存储订单金额),此时应当调整字段类型,而非试图绕过约束。

2.7 BIT 类型:二进制位存储与客户端显示陷阱

MySQL 的数值类型家族中还有一位成员非常特殊------BIT。它不用于算术运算,而是专为位级存储 设计,非常适合存放布尔标志、权限掩码或紧凑的状态位。然而,正是因为存储的是二进制原始数据,它在命令行客户端的显示行为常常让人困惑:明明插入了数据,SELECT * 却看到一片空白;有时又能冒出字母 a。本节我们将彻底剖析 BIT 的存储本质、定义语法,并逐一解开这些"显示幻象"。

2.7.1 定义语法与结构查看

BIT 类型的完整定义格式为 BIT(M),其中 M 表示位数 (即存储的二进制位长度),取值范围为 1 到 64若省略 M,则默认值为 1。

  • BIT(1):仅存储 1 个位,只能存放数值 01
  • BIT(8):存储 8 个位,可存放 0 到 255 之间的整数(对应一个字节)。
  • BIT(64):存储 64 个位,对应无符号 64 位整数的完整范围。

使用 DESCSHOW COLUMNS 查看表结构时,Type 列会精确显示括号中的长度,这正是定义时指定的位宽。例如:

sql 复制代码
CREATE TABLE t3 (
    id INT,
    online BIT(1)   -- 位长度为 1
);

-- 查看表结构,括号中的 (1) 即代表该列的存储位数
DESC t3;

执行结果中 onlineType 显示为 bit(1),直观地告诉我们该列只占用 1 个二进制位。

2.7.2 插入操作:数值范围与隐式转换

BIT 列插入数据时,MySQL 接受数值字面量或数字字符,并将它们转换为对应的二进制位串存储。

MySQL 也支持隐式类型转换:如果插入一个字符,如 'a',MySQL 会取其 ASCII 码值(97)转换为二进制位串存入。因此,向 BIT(8) 插入 'a' 与插入 97 的效果完全一致。

sql 复制代码
-- 创建位宽为 8 的表
CREATE TABLE t4 (
    id INT,
    flag BIT(8)
);

-- 插入字符 'a',实际存入的是 ASCII 码 97
INSERT INTO t4 (id, flag) VALUES (123, 'a');

-- 插入数值 97,结果与上一行完全相同
INSERT INTO t4 (id, flag) VALUES (124, 97);

2.7.3 查询显示的"空白"之谜:ASCII 渲染机制

很多初学者第一次执行 SELECT * FROM t3 时,会惊讶地发现 online 列似乎空空如也,就像数据根本没有写入一样。但执行 SELECT id, HEX(online) FROM t3 后,却能清晰地看到 01 等真实数值------这说明数据其实完整存储了,只是默认显示方式骗过了我们的眼睛

这个现象的根本原因在于:MySQL 命令行客户端将 BIT 类型视为二进制字符串(Binary String)进行渲染。当客户端输出结果时,它会尝试将每个字节的二进制值映射为 ASCII 字符来显示:

  • 如果该字节值对应可打印的 ASCII 码 (范围 32~126 ),则直接显示该字符(如 0x61 显示为 'a'0x30 显示为 '0')。
  • 如果该字节值对应控制字符或不可打印字符 (范围 0~31 以及 127),客户端通常会渲染为空白、占位符或完全不可见。

BIT(1) 示例中,插入的值为 0(二进制 0x00)和 10x01)。这两个值分别是空字符(NUL)和标题开始(SOH),均属于不可打印的控制字符,因此 SELECT * 输出时表现为空白。而 HEX(online) 则忠实还原了存储的十六进制原始数据,让我们看到了 01

2.7.4 可靠查看 BIT 数据的最佳实践

鉴于 SELECT * 的显示可能产生误导,在生产环境中操作 BIT 类型时,强烈建议养成如下习惯:

  1. 查询时主动使用转换函数 :始终搭配 HEX(column)BIN(column)CAST(column AS UNSIGNED) 来查看真实数值,杜绝依赖客户端的不确定性渲染。
  2. 区分布尔值与位掩码 :仅需 0/1 状态时,BIT(1) 可用,但 TINYINT(1) 在多数客户端的显示更直观(直接显示 0/1)。若需要进行多标志位组合(如权限掩码),则 BIT(8) 或更大的位宽是更节省空间的优选项。
  3. 善用位运算BIT 类型天然支持位与(&)、位或(|)、位异或(^)等操作,在组合状态判断时效率极高。
sql 复制代码
-- 推荐的查看方式(绕过显示陷阱)
SELECT 
    id, 
    online,                      -- 可能显示为空白或字符,不可靠
    HEX(online) AS hex_val,      -- 十六进制原始值,最清晰
    BIN(online) AS bin_val,      -- 二进制原始值
    CAST(online AS UNSIGNED) AS dec_val  -- 转换为无符号整数
FROM t3;

通过这种方式,无论存储的是控制字符、数字还是字母,我们都能一目了然地获知数据库中真正保存的位数据,彻底告别"为什么查不到"的困惑。

补充:HEX(column)BIN(column)CAST(column AS UNSIGNED) 都是 MySQL 中对数据进行类型转换或查看不同表示形式的函数HEX(column) 用于把字段值转换成十六进制形式 显示,例如数字 10 转换为 A(十六进制表示),常用于查看二进制数据或字符编码;BIN(column) 用于把整数转换成二进制形式 显示,例如 10 转换为 1010,方便查看数据的二进制结构;CAST(column AS UNSIGNED) 用于把字段转换成无符号整数类型 ,例如字符串 '123' 转换成数字 123,便于进行数学运算或比较。简单理解:HEX看十六进制,BIN看二进制,CAST负责把一种数据类型转换成另一种数据类型。

第三章 数值类型:小数

整数类型为我们解决了"多少个"的问题,但现实世界中的数据远不止整数------价格、温度、比率、经纬度,无一不涉及小数。MySQL 为此提供了三种核心的小数类型:FLOATDOUBLEDECIMAL。它们分别对应着"追求速度的近似值"与"追求绝对精确的定点数"两种截然不同的设计哲学。

本章我们将从一条完整的定义语法出发,彻底厘清精度(M)与标度(D)的含义,揭示 UNSIGNED 在小数领域与整数领域的本质差异,并通过大量插入实验,亲眼见证 MySQL 在处理四舍五入、边界越界以及大数精度丢失时的完整行为。

3.1 小数类型定义语法全解析

三种小数类型的定义语法极其相似,均支持指定精度(Precision)标度(Scale) ,也均可选追加 UNSIGNED 属性。其完整形式如下:

sql 复制代码
FLOAT[(M, D)] [UNSIGNED]
DOUBLE[(M, D)] [UNSIGNED]
DECIMAL[(M, D)] [UNSIGNED]
  • M(精度) :表示该列总共能存储的数字位数包含整数部分和小数部分的总和)。
  • D(标度) :表示该列小数部分保留的位数

举个例子,DECIMAL(5, 2) 表示总共 5 位数字,其中小数占 2 位,因此整数部分最多为 3 位。其取值范围为 -999.99999.99(有符号),或 0999.99(无符号)。

重要说明

  • 对于 FLOATDOUBLE(M, D)可选的。如果不指定,MySQL 会按默认精度存储,但这可能导致意想不到的舍入误差(后文会详述)。
  • 对于 DECIMAL(M, D) 仍然可选,但默认为 DECIMAL(10, 0),即默认视为 10 位精度的整数(但本质仍是定点数)。
  • 如果定义了 (M, D),则 D 必须小于等于 M ,且 M 的最大值对于 DECIMAL 为 65,D 最大为 30。

3.2 三种类型的本质区别与存储特性

类型 存储空间 本质属性 适用场景
FLOAT 4 字节 近似值(单精度浮点数),基于 IEEE 754 标准 科学计算、对精度不敏感的数值(如温度、经纬度)
DOUBLE 8 字节 近似值(双精度浮点数),精度约为 FLOAT 的两倍 科学计算、金融统计中的中间计算
DECIMAL 变长(每 9 位数 4 字节) 精确值 (定点数),以字符串形式存储原始数字 金融货币、财务数据、任何禁止舍入误差的业务

理解"近似值"与"精确值"的鸿沟,是选择小数类型的底层逻辑。FLOATDOUBLE 在存储时会将十进制数转换为二进制浮点数,这个过程本身就可能产生无限循环小数(如 0.1 在二进制中是无限循环的),因此它们存储的永远是一个"非常接近真实值"的近似结果。而 DECIMAL 直接按十进制数字位存储,天然保证了精确性。

3.3 UNSIGNED 在小数领域的"不翻倍"规则

在学习整数类型时,我们记住了一条铁律:加上 UNSIGNED,上限翻倍(因为符号位被解放)。然而这条规则在浮点数与定点数中完全失效

在小数类型中,UNSIGNED 仅仅表示"拒绝负数",将取值范围的下限从负数提升到 0,但上限保持与有符号时的正数最大值完全相同,并不会翻倍。

实战验证

sql 复制代码
-- 创建有符号的 DECIMAL(3, 1),范围 -99.9 ~ 99.9
CREATE TABLE test_decimal_signed (
    price DECIMAL(3, 1)
);

-- 创建无符号的 DECIMAL(3, 1),范围 0 ~ 99.9(注意上限仍是 99.9,不是 199.9)
CREATE TABLE test_decimal_unsigned (
    price DECIMAL(3, 1) UNSIGNED
);

-- 向无符号表插入 99.9 成功
INSERT INTO test_decimal_unsigned (price) VALUES (99.9);  -- Query OK

-- 向无符号表插入 100.0 失败(因为总位数 4 > 3)
INSERT INTO test_decimal_unsigned (price) VALUES (100.0);  
-- ERROR 1264 (22003): Out of range value for column 'price'

-- 向无符号表插入 -0.1 失败(被 UNSIGNED 拦截)
INSERT INTO test_decimal_unsigned (price) VALUES (-0.1);
-- ERROR 1264 (22003): Out of range value for column 'price'

结论:UNSIGNED 在小数类型中仅具备"非负约束"的语义,完全不具备"扩大正数范围"的功能。因此,在设计小数类型的无符号字段时,务必清醒地意识到:上限并未增加

3.4 四舍五入与越界拦截:先舍入,后判定

MySQL 在处理小数插入时,遵循一套严格且清晰的流程:先根据定义的标度(D)对数据进行四舍五入,再检查舍入后的结果是否在精度(M)允许的总位数范围内。只有两步均通过,插入才能成功。

规则拆解

  1. 精度多余时,四舍五入:如果插入值的小数位数多于 D,MySQL 会按四舍五入截断到 D 位。
  2. 舍入后依然越界,直接拒绝:如果舍入后的整数部分位数超过了(M - D),即便原始数据看似接近边界,也会被无情拦截。
  3. 位数不足时,补零对齐:如果小数位数少于 D,MySQL 会自动在末尾补零,以满足标度要求。

实例深度剖析

sql 复制代码
CREATE TABLE test_round (
    value DECIMAL(5, 2)  -- 总 5 位,整数 3 位,小数 2 位,范围 -999.99 ~ 999.99
);

-- 场景1:小数位多于 D,四舍五入成功
INSERT INTO test_round (value) VALUES (12.345);   -- 四舍五入为 12.35,总位数 4 (12.35) < 5,成功
INSERT INTO test_round (value) VALUES (12.344);   -- 四舍五入为 12.34,成功
SELECT * FROM test_round;  -- 显示 12.35, 12.34

-- 场景2:小数位多于 D,四舍五入后整数位数膨胀(关键陷阱!)
INSERT INTO test_round (value) VALUES (999.995);  
-- 先四舍五入到 2 位小数 -> 1000.00
-- 此时总位数变成 6 (1000.00),超过了 M=5
-- MySQL 直接报错,而非截断整数部分!
-- ERROR 1264 (22003): Out of range value for column 'value'

-- 场景3:位数不足,自动补零
INSERT INTO test_round (value) VALUES (123.4);    -- 实际存入 123.40
INSERT INTO test_round (value) VALUES (123);      -- 实际存入 123.00

这个机制确保了数据库中存储的小数始终严格符合列定义的(M, D)格式 ,不存在"小数位数超长"或"格式不统一"的脏数据。同时它也提醒我们,在设计 (M, D) 时必须为四舍五入预留足够的整数位缓冲,否则业务上看似合法的 999.995 会因为舍入进位而被拦在门外。

3.5 精度对决:为什么大数用 FLOAT 会"不准"?

这是浮点数类型最经典的致命伤,也是区分它与 DECIMAL 的核心分水岭。由于 FLOATDOUBLE 采用二进制存储,当数值的绝对值变得非常大时,其精度分辨率会急剧下降,甚至出现"整数部分都无法精确表示"的荒谬现象。

实战对比 :创建一张对比表,分别用 FLOAT(默认无精度参数)和 DECIMAL 存储相同的超大数值。

sql 复制代码
CREATE TABLE precision_test (
    id INT,
    float_col FLOAT,                -- 默认单精度
    decimal_col DECIMAL(20, 3)      -- 20位总精度,3位小数,足够大
);

-- 插入一个包含 10 位整数和 3 位小数的数值
INSERT INTO precision_test (id, float_col, decimal_col) 
VALUES (1, 1234567890.123, 1234567890.123);

SELECT 
    id,
    float_col,
    decimal_col
FROM precision_test;

预期输出

  • decimal_col:精确显示为 1234567890.123
  • float_col:大概率显示为 1234567940.0 或类似数值(具体结果取决于 MySQL 版本和硬件)。你会发现整数部分已经发生了偏差,且小数部分几乎完全丢失。

为什么?因为 FLOAT 只有 23 个有效二进制位用于尾数,换算成十进制大约只有 7 位有效数字。当数值达到 10 位数时,低位数字直接被"挤"出了存储空间。DOUBLE 稍好(约 15-16 位有效数字),但面对天文级数字或极高精度要求的累加运算,同样会崩盘。

反观 DECIMAL :它按十进制位逐位存储,对于 DECIMAL(20, 3),它能精确表示整数部分最多 17 位的任何数字,分毫不差。这就是 DECIMAL 被称为"定点数"且"不会发生精度损失"的根本原因。

3.6 DECIMAL 的四舍五入与越界拒绝(重申与强化)

再次强调,DECIMAL 同样遵循"先四舍五入,后校验范围"的铁律。即使类型是精确值,四舍五入操作本身依然会改变数值大小,从而可能引发越界。

示范

sql 复制代码
CREATE TABLE test_decimal_boundary (
    amount DECIMAL(4, 2)  -- 总 4 位,整数 2 位,范围 -99.99 ~ 99.99
);

-- 插入 99.994:四舍五入后为 99.99,总位数 4,成功
INSERT INTO test_decimal_boundary (amount) VALUES (99.994);  -- 成功,存为 99.99

-- 插入 99.995:四舍五入后为 100.00,总位数变成 5,超限,拒绝
INSERT INTO test_decimal_boundary (amount) VALUES (99.995);  
-- ERROR 1264 (22003): Out of range value for column 'amount'

这个例子残酷地说明:99.995 在实际业务中可能被视为"近似 100",但在 DECIMAL(4,2) 的定义下,它触及了整数部分扩容的边界,直接被数据库踢出。因此,设计 (M, D) 时,务必为四舍五入预留至少 1 个整数位的安全余量

3.7 综合实战与选型建议

选型决策树

  1. 金融、货币、账务相关 :无脑选 DECIMAL。精度即正义,任何微小的浮点误差都可能造成资金对不齐。
  2. 科学计算、大数据量统计分析 :使用 DOUBLE。其精度足以满足大部分工程需求,且计算速度远超 DECIMAL(CPU 原生支持浮点运算)。
  3. 极低存储成本的标志性小数(如温度 36.5) :如果仅需 1 位小数且范围不大,FLOAT 即可,但务必指定 (M, D) 以固定行为。
  4. 绝对禁止 :不要试图用 FLOATDOUBLE 存储"精确到分"的金额,也不要让它们参与循环累加(误差会滚雪球)。

本章实战模板

sql 复制代码
-- 创建一个完整的产品定价表,融合三种小数类型
CREATE TABLE product_pricing (
    id INT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(50),
    exact_price DECIMAL(10, 2) UNSIGNED NOT NULL,  -- 精确价格,总 10 位,小数 2 位
    tax_rate FLOAT(4, 3) UNSIGNED NOT NULL,        -- 税率,总 4 位,小数 3 位,如 0.175
    scientific_metric DOUBLE(12, 6)                -- 科学指标,总 12 位,小数 6 位
);

-- 插入测试数据
INSERT INTO product_pricing (product_name, exact_price, tax_rate, scientific_metric)
VALUES ('精密仪器', 9999.99, 0.135, 123456.789012);

-- 验证 DECIMAL 的精确性
SELECT 
    exact_price * 100 AS price_in_cents,   -- 精确计算
    tax_rate,
    scientific_metric
FROM product_pricing;

通过本章的学习,我们应该建立起一个清醒的认知:在小数领域,"快"与"准"不可兼得FLOAT/DOUBLE 如同赛车,风驰电掣但偶尔偏离路线;DECIMAL 如同精密机械表,步履沉重但分秒不差。根据业务场景审慎抉择,才是专业开发者的必经之路。

第四章 字符串类型

踏入字符串领域,我们首先要告别整数和小数中"数值大小"的直观概念。在 MySQL 中,字符串类型面临两个核心维度:字符长度存储字节 ------它们之间并非恒等关系,而是由字符集 (如 utf8utf8mb4gbk)决定的映射。本章我们将围绕 CHARVARCHARBLOBTEXT 这四种最基础的字符串类型,彻底厘清其定义语法、存储机制、长度限制以及字符集带来的底层影响。

4.1 字符(L)与字节的底层分野

在开始学习具体类型之前,必须建立一条绝对基准线:所有字符串类型定义中的 (L)(size),一律指的是字符的个数(Character Count),而非字节数(Byte Count)。 这是一个极易踩中的陷阱。

  • 一个英文字母 'a'utf8 字符集下占用 1 个字节 ,在 utf8mb4 下同样占用 1 个字节。
  • 一个汉字 '中'utf8 字符集下占用 3 个字节 ,在 utf8mb4 下也是 3 个字节,在某些生僻字或表情符号(如 😊)下占用 4 个字节。

因此,当我们定义 CHAR(2) 时,表示该列最多存放 2 个字符 ,至于这 2 个字符是 'ab'(占 2 字节)还是 '中国'(占 6 字节),完全取决于字符集编码。MySQL 只负责数"字符个数",而存储层按"字节"分配空间。

4.2 CHAR(L):固定长度的"规矩派"

CHAR 类型是典型的定长字符串,其完整语法为 CHAR(L),其中 L 表示字符个数。

  • 最大长度限制L 的取值范围为 0 到 255若省略 L,默认为 1。
  • 存储机制 :无论实际存入的字符串有多长(只要不超过 L 个字符),MySQL 都会在硬盘上分配 固定 L × 字符集最大字节数 的空间。存入时,如果字符不足 L,会在右侧用空格(0x20)填充至固定长度;取出时,默认会移除这些填充空格(除非启用 PAD_CHAR_TO_FULL_LENGTH SQL 模式)。
  • 性能特点因为长度固定,读写时的偏移量计算极快,更新操作也不会引起页分裂 ,因此在存储"状态码"、"固定编号"、"性别"等长度绝对一致的数据时,CHAR 效率极高。
  • 语法实例与拦截
sql 复制代码
-- 创建一张 CHAR(2) 测试表
CREATE TABLE t8 (
    id INT,
    name CHAR(2)   -- 最大容纳 2 个字符
);

-- 插入 1 个字符成功(不足 2 个,底层补空格填充)
INSERT INTO t8 (id, name) VALUES (1, 'a');   -- Query OK
INSERT INTO t8 (id, name) VALUES (1, 'b');   -- Query OK

-- 插入 2 个字符成功
INSERT INTO t8 (id, name) VALUES (1, 'ab');  -- Query OK

-- 插入 3 个字符失败,直接被拦截
INSERT INTO t8 (id, name) VALUES (1, 'abc');
-- ERROR 1406 (22001): Data too long for column 'name'

-- 汉字同样按字符个数计数:插入 1 个汉字 '中' 成功
INSERT INTO t8 (id, name) VALUES (1, '中');   -- Query OK

-- 插入 2 个汉字 '中国' 成功(占 6 字节,但字符个数为 2)
INSERT INTO t8 (id, name) VALUES (2, '中国'); -- Query OK

-- 插入 3 个汉字 '中国人' 失败(字符个数 3 > 2)
INSERT INTO t8 (id, name) VALUES (1, '中国人');
-- ERROR 1406 (22001): Data too long for column 'name'

观察结论 :该示例清晰地印证了 L 计数的"字符"本质------无论是 ASCII 字母还是多字节汉字,MySQL 都严格按字符个数进行边界检查。

4.3 VARCHAR(L):可变长度的"实用派"

VARCHAR 是针对不定长字符串设计的类型,其语法为 VARCHAR(L)L 同样表示最大字符个数。

  • 最大长度限制理论上限为 65,535 个字符,但实际受限于整行最大 65,535 字节的限制(详见 4.6 节)。
  • 存储机制它不会预分配固定空间,而是 用多少空间,就分配多少空间。但为了在读取时知道数据的实际边界,MySQL 必须额外在数据前面使用 1 到 2 个字节(长度前缀)来记录实际存储的字节数:
    • 如果列定义的最大字节长度(L × 字符集最大字节数) ≤ 255,则使用 1 个字节 记录长度。
    • 如果列定义的最大字节长度 > 255,则使用 2 个字节 记录长度。
  • 性能权衡:由于长度可变,频繁更新可能导致数据页内碎片和行迁移(如果新数据比旧数据长),但它在存储"用户名"、"标题"、"地址"等长短差异巨大的字段时,能节省大量磁盘空间。
sql 复制代码
-- 创建一张 VARCHAR(6) 测试表(最大 6 个字符)
CREATE TABLE t9 (
    id INT,
    nickname VARCHAR(6)
);

-- 插入短字符串,实际只占用 "Hello" 的 5 字节 + 1 字节长度前缀
INSERT INTO t9 (id, nickname) VALUES (1, 'Hello');

-- 插入 6 个字符成功
INSERT INTO t9 (id, nickname) VALUES (2, '你好世界啊'); -- 6 个汉字,占用 18 字节 + 长度前缀

-- 插入 7 个字符失败
INSERT INTO t9 (id, nickname) VALUES (3, 'HelloWorld'); -- 10 个字符 > 6
-- ERROR 1406 (22001): Data too long for column 'nickname'

4.4 关于 UTF-8 下字符字节数的关键纠正与深度说明

  • 实际情况 :在标准的 utf8(即 utf8mb3)字符集下:

    • 'a'(ASCII 基础拉丁字母)占 1 个字节
    • '中'(CJK 统一表意文字)占 3 个字节
  • 如果是在 utf8mb4 字符集下

    • 'a' 仍占 1 字节。
    • '中' 仍占 3 字节。
    • '😊'(Emoji)占 4 个字节

结论CHARVARCHAR 中的 L 只负责计数"字符个数",而底层占用的实际字节数由 字符集最大字节数 × L 决定。这也是为什么 VARCHAR(255)utf8mb4 下会使用 2 字节长度前缀(因为 255×4=1020 > 255),而在 latin1 下可能仅用 1 字节前缀。

4.5 VARCHAR 的"隐藏枷锁":行大小对其他列的影响

这是 MySQL 中一个极易被忽视的硬性限制:单行数据的总字节数(包括所有列)不得超过 65,535 字节(即 64KB) 。这个限制会直接压制 VARCHAR 理论上可达的 65,535 字符上限。

  • 如果表中仅有一列 VARCHAR(65535),且字符集为 latin1(1 字节/字符),那么该列定义已逼近行上限,勉强可行(还需扣除长度前缀等开销)。
  • 一旦表中还有其他列 (如额外的 INTCHAR 或另一个 VARCHAR),那么分配给该 VARCHAR 的实际可用字节数就会被压缩。

计算实例

sql 复制代码
-- 假设表定义如下(字符集 latin1,1 字节/字符)
CREATE TABLE t10 (
    id INT,                -- 占用 4 字节
    name VARCHAR(20000),   -- 希望定义 20000 字符
    description VARCHAR(20000)  -- 同样定义 20000 字符
);
-- 这两列加起来 40000 字节,加上 id 的 4 字节,再算上长度前缀 (每列 2 字节,共 4 字节)
-- 总计 40008 字节 < 65535,理论上可行。

-- 但如果定义成:
CREATE TABLE t11 (
    id INT,
    col1 VARCHAR(30000),
    col2 VARCHAR(30000)
);
-- 60000 字节 + 4 字节 + 长度前缀 4 字节 = 60008 字节 < 65535,勉强可行。

-- 但如果三列各 VARCHAR(25000):
-- 75000 字节 > 65535,MySQL 会直接拒绝创建表或报错。

因此,在实际生产设计中,VARCHAR 的最大可用 L 是一个动态值 ,取决于同一行中其他所有列(特别是 CHAR 固定长度列)的字节消耗。设计表结构时,务必使用 SHOW TABLE STATUS 或直接尝试 CREATE TABLE 来验证,而非单纯记忆 65,535 这个数字。

4.6 BLOB 与 TEXT:大文本与二进制数据的归宿

当应用程序需要存储的字符串长度远超 VARCHAR 的上限(MySQL 中 VARCHAR 最大约 65,535 字节,且受行大小限制),或者数据本身是二进制形式(如图片、音频、视频、压缩文件、序列化对象、加密数据等),标准字符类型便不再适用。此时,TEXTBLOB 家族登场------它们专为大容量数据而生,但各自的设计哲学与行为差异,决定了它们适用于截然不同的场景。

一、语法与容量分级

定义形式TEXTBLOB 的列定义不包含括号长度 (不像 VARCHAR(255)),而是通过类型前缀来区分最大存储容量。这一设计暗示了它们的存储引擎内部处理方式与变长字符串有本质区别。

类型前缀 最大字节数 等效大小 常见用途
TINYTEXT / TINYBLOB 255 255 B 极短备注、状态码序列化
TEXT / BLOB 65,535 64 KB 普通文章片段、小型JSON/XML
MEDIUMTEXT / MEDIUMBLOB 16,777,215 16 MB 书籍全文、中等尺寸图片/音频
LONGTEXT / LONGBLOB 4,294,967,295 4 GB 超长日志、视频文件、大型备份

注意 :虽然上限很高,但实际可用容量还受 max_allowed_packet 和存储引擎限制(如 InnoDB 的 innodb_log_file_size 等)。

二、核心区别:BLOB vs TEXT

这是开发者最容易混淆的地方,也是选型的关键:

维度 BLOB TEXT
数据性质 二进制字节流(不关心字符集) 字符文本(必须关联字符集)
排序与比较 二进制字节值 (即数值大小)进行比较,区分大小写'A''a' 字符集和排序规则(Collation) 控制,默认大多数 utf8mb4 排序规则不区分大小写('A' = 'a'
空格处理 比较时会考虑所有字节,填充空格不会被忽略 比较时尾部空格通常会被忽略(取决于 Collation),但存储时保留
字符集转换 不进行任何字符集转换,原样存储和读取 写入时按列的字符集编码,读取时按客户端 character_set_client/character_set_connection 进行转码,可能产生额外 CPU 开销
索引限制 只能对前缀建索引(如 BLOB(100)),且索引长度受 innodb_large_prefix 限制 同样只能对前缀建索引,但前缀长度以字符为单位(非字节)
数据校验 无字符有效性校验,任何字节序列都合法 会校验字节序列是否合法符合当前字符集(如 utf8mb4 中不合法字节会报错或替换为 ?

实战建议

  • 存储图片、音频、序列化对象、加密密文 → 选 BLOB,因为它们是纯粹的字节序列,不应受字符集干扰,且可能包含 '\0' 等特殊字节。
  • 存储HTML、JSON、XML、日志、文章、用户输入 → 选 TEXT,因为这些是自然语言,需要支持大小写不敏感的检索、排序以及字符集转换。

三、存储行为与性能影响

1. 行内存储 vs 溢出页

InnoDB 的聚簇索引行默认大小为 innodb_page_size(通常 16 KB)。当 TEXTBLOB 列的实际数据长度超过行内剩余空间 时,InnoDB 会将超过 768 字节的部分移出到独立的溢出页(Overflow Page),行内只保留一个 20 字节的指针(指向外部页)。这种"行外存储"机制带来两面性:

  • 优点:避免单行数据撑爆行大小限制(行大小上限约等于页大小的一半,即约 8KB)。
  • 缺点:读取该列时,需要额外 I/O 访问溢出页,尤其是访问大字段的中间部分时,可能产生随机 I/O;如果频繁查询不包含这些大列,建议将它们单独放在附属表中,以减小主表扫描代价。

2. 临时表与内存表

MySQL 在执行 ORDER BYGROUP BY 或使用临时表时,若涉及 TEXT/BLOB 列,会强制使用磁盘临时表 (而非内存临时表),显著降低排序和分组性能。因此,尽量避免在大字段上排序,或仅对前缀进行排序(如 ORDER BY SUBSTRING(long_text, 1, 20))。

3. 更新代价

如果更新一个大字段,将旧值替换为新值,且新值长度超过原存储空间,InnoDB 可能需要分配新页并移动数据,导致行碎片化。建议使用 OPTIMIZE TABLE 定期重建表以回收空间。

四、索引与查询限制

  • 无法全列索引 :由于字段可能巨长,MySQL 不允许对 TEXTBLOB 列直接创建完整索引,必须指定前缀长度,例如:

    sql 复制代码
    CREATE INDEX idx_text_prefix ON t12 (long_content(100));  -- 只索引前100个字符

    前缀索引显著降低索引大小,但无法用于 LIKE '%xxx%' 等无法利用前缀的场景。

  • 等值比较 :即使是等值查询(WHERE long_content = 'abc'),MySQL 也会隐式使用前缀比较?不,等值比较会读取整个列进行比较,若列很大,效率极低。因此,常见优化手段是额外存储 MD5/SHA 散列值,并对散列列建索引,用散列值做精确匹配,再用大字段值做二次校验。

  • 全文索引(FULLTEXT)TEXT 列支持全文索引,可进行自然语言搜索;BLOB 列不支持(因为二进制无意义)。若需对 BLOB 做全文搜索,可将其转换为 TEXT 并指定字符集(不推荐)。

五、使用示例与陷阱

sql 复制代码
-- 创建包含 TEXT 与 BLOB 的表
CREATE TABLE t12 (
    id INT PRIMARY KEY AUTO_INCREMENT,
    long_text TEXT,                -- 默认字符集 utf8mb4
    binary_data BLOB,
    medium_text MEDIUMTEXT
) ENGINE=InnoDB;

-- 插入超长文本(此处故意超出普通 TEXT 上限 65,535 字节)
-- 注意:REPEAT('A', 70000) 会产生 70,000 个字符,若字符集 utf8mb4,每个字符占 1~4 字节,
-- 实际字节可能远超 65,535,但 REPEAT 生成的是单字节 'A',所以字节数 = 70,000 > 65,535,报错。
INSERT INTO t12 (long_text) VALUES (REPEAT('A', 70000));
-- ERROR 1406 (22001): Data too long for column 'long_text'
-- 修正:使用 MEDIUMTEXT 列
INSERT INTO t12 (medium_text) VALUES (REPEAT('A', 70000));  -- 成功

-- 存储二进制(例如一张小图片的字节流)
INSERT INTO t12 (binary_data) VALUES (X'89504E470D0A1A0A...');  -- PNG 头

-- 注意排序差异
INSERT INTO t12 (long_text) VALUES ('a'), ('A'), ('b'), ('B');
SELECT long_text FROM t12 ORDER BY long_text;
-- 在 utf8mb4_general_ci 下,顺序是 'a', 'A', 'b', 'B'(不区分大小写,a/A 视为相等,但实际排序可能按二进制二次比较,结果不稳定)
-- 若需要区分大小写排序,可使用 COLLATE utf8mb4_bin 或使用 BLOB 类型。

六、替代方案与最佳实践

  1. 能不存就不存 :对于图片、音频等文件,更推荐将文件存储在对象存储(OSS/S3) 或文件系统中,数据库中仅存储文件路径或元数据。这能减轻数据库负担,备份恢复更快。
  2. 拆分大表 :如果表中包含多个大字段,考虑将它们垂直拆分到独立的扩展表,主表仅存高频查询的小字段,通过 JOIN 关联,避免扫描大字段影响缓存命中率。
  3. 压缩存储 :对于文本数据(如 JSON、日志),可在应用层使用 gzip 压缩后再存入 BLOB,能节省存储空间和网络传输,但会消耗 CPU 压缩/解压缩。
  4. 分区与归档 :对于超大的 TEXT/BLOB 表,结合分区按时间归档,或使用 pt-archiver 工具定期清理老数据。
  5. 监控与调优 :定期检查 information_schema.tables 中的 AVG_ROW_LENGTHDATA_FREE,判断是否存在大量碎片;调整 innodb_file_per_tableinnodb_compression_level 等参数。

七、总结对比表

特性 VARCHAR TEXT BLOB
最大长度 65,535 字节(受行大小影响) 按前缀分 255B~4GB 同左
默认值 允许 不允许(MySQL 5.7+ 可设但需注意) 不允许
存储位置 行内(若长度合适) 行内部分 + 溢出页(大时) 同左
字符集相关
区分大小写 取决于 Collation 取决于 Collation 总是区分
索引 可完整索引(限长) 仅前缀索引 仅前缀索引
适用场景 短字符串(标题、用户名) 长文本、HTML、JSON 图片、音频、序列化对象

掌握 TEXTBLOB 的差异,合理选择类型,并配合恰当的设计模式,才能在大数据场景下既保证功能正确,又兼顾性能与运维成本。永远记住:数据库不适合做大容量文件的存储仓库,文件系统或对象存储才是更好的归宿,数据库中的大字段应仅用于真正需要事务一致性和原生 SQL 查询能力的场景。

第五章 字符串类型------日期与时间类型

在数据库设计中,日期和时间信息几乎无处不在------订单创建时间、用户生日、文章发布时间、最后修改记录......MySQL 为此提供了一组专门的时间数据类型。本章我们将深入剖析其中最核心的三个类型:DATEDATETIMETIMESTAMP,并通过实操案例揭示它们各自的行为特性与适用场景。

5.1 三大核心日期时间类型概览

类型 占用字节 格式示例 支持范围 零值表示
DATE 3 'YYYY-MM-DD' '1000-01-01''9999-12-31' '0000-00-00'
DATETIME 8(5.6+) 'YYYY-MM-DD hh:mm:ss' '1000-01-01 00:00:00''9999-12-31 23:59:59' '0000-00-00 00:00:00'
TIMESTAMP 4 'YYYY-MM-DD hh:mm:ss' '1970-01-01 00:00:01' UTC 至 '2038-01-19 03:14:07' UTC '0000-00-00 00:00:00'(或 NULL

从表中可以清楚看出,DATE 只存储日期部分,不包含时间;DATETIMETIMESTAMP 则同时保存日期和时间。三者最本质的区别在于时区处理存储范围,这也是我们选择时的首要考量。

5.2 DATE 类型 ------ 纯粹的日历日期

DATE 类型用于存储"年-月-日"信息,不涉及时分秒。它的输入格式非常灵活,但推荐始终使用标准字符串 'YYYY-MM-DD',这样可以避免因区域设置导致的歧义。

典型应用场景

  • 员工出生日期
  • 合同生效日
  • 财务账期日期
  • 排班日历

示例操作

sql 复制代码
CREATE TABLE events (
    event_id INT PRIMARY KEY,
    event_date DATE
);

INSERT INTO events VALUES (1, '2026-08-24');
INSERT INTO events VALUES (2, '2026-08-24 14:30:00');  -- 自动截断时间,仅保留日期

第二条插入语句虽然提供了时间部分,但 MySQL 会发出警告并自动截断,最终存储为 '2026-08-24'。这也是 DATE 类型与其他时间类型混用时容易忽略的细节。

5.3 DATETIME 类型 ------ 固定时区的日期时间

DATETIME 存储完整的日期和时间,并且不依赖于时区设置也就是说,你存入什么值,读出来就是什么值,MySQL 不会做任何转换。 这使得 DATETIME 非常适合记录那些与时区无关的绝对时刻,或者业务上已经明确指定时区(如"北京时间")的时间戳。

语法DATETIME[(fsp)],其中 fsp 表示小数秒精度(0~6),默认为 0。

典型应用场景

  • 订单生成时间(业务本地时间)
  • 预约时间段
  • 历史日志记录(不跨时区比较时)

5.4 TIMESTAMP 类型 ------ 自动时区转换的"活"时间

TIMESTAMP 是 MySQL 中最特殊的一个日期时间类型。它存储的是自 '1970-01-01 00:00:00' UTC 以来的秒数(即 Unix 时间戳),但在显示时会根据当前会话的 time_zone 变量自动转换成对应时区的日期时间字符串。换言之,存进去的是 UTC 值,取出来的是会话时区的本地时间 。这一特性让 TIMESTAMP 成为跨时区应用的理想选择,例如全球用户统一记录事件发生时刻。

然而,它也有明显的局限性:

  • 范围上限为 '2038-01-19 03:14:07'(受 32 位时间戳限制),对于需要存储远期日期的系统(如保险、遗产规划)并不适合。
  • 范围下限为 '1970-01-01 00:00:01',无法表示更早的日期。

5.5 实操演示:三者的建表与自动更新机制

为了直观理解这三种类型的行为差异,我们执行以下建表语句:

sql 复制代码
CREATE TABLE IF NOT EXISTS t11 (
    t1 DATE,
    t2 DATETIME,
    t3 TIMESTAMP
);

使用 DESC t11; 查看表结构,会发现一个极具 MySQL 特色的现象:

Field Type Null Key Default Extra
t1 date YES NULL
t2 datetime YES NULL
t3 timestamp NO CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP

注意 t3 列被自动赋予了两个属性:

  • DEFAULT CURRENT_TIMESTAMP插入时若未显式赋值,则自动填入当前时间。
  • ON UPDATE CURRENT_TIMESTAMP当该行任何其他字段被更新时,t3 会自动更新为当前的系统时间。

这正是 TIMESTAMP 类型独有的"自动追踪"特性,尤其适合用作 created_at / updated_at 这类审计字段。

5.5.1 插入数据

我们尝试向 t11 插入一条仅指定 t1t2 的记录:

sql 复制代码
INSERT INTO t11 (t1, t2) VALUES ('2000-10-01', '1949-10-01 08:00:00');

此处特别注意 :如果错误地写成 INSERT INTO t11 (date, datetime) ...,会触发 ERROR 1054 (42S22): Unknown column 'date' in 'field list',因为列名是 t1t2,而不是类型名称。这是新手常见的笔误。

执行成功后查询结果:

sql 复制代码
SELECT * FROM t11;

输出:

t1 t2 t3
2000-10-01 1949-10-01 08:00:00 2023-04-12 12:57:39

可以看到:

  • t1 仅保留日期,无时间部分。
  • t2 完整保留了插入的日期时间值,未作任何转换。
  • t3 由于我们未提供值,自动填入了当前时刻(这里是 2023-04-12 12:57:39)。

5.5.2 更新操作与自动时间戳

接下来我们执行一条更新语句,只修改 t1 列:

sql 复制代码
UPDATE t11 SET t1 = '1999-01-01';

再次查询:

t1 t2 t3
1999-01-01 1949-10-01 08:00:00 2023-04-12 12:58:22

关键变化在于 t3 字段的值从 12:57:39 变成了 12:58:22------它自动更新为更新操作发生的时刻。这完全符合 ON UPDATE CURRENT_TIMESTAMP 的定义。

这个特性在实际开发中非常实用:我们无需在每次 UPDATE 语句中手动修改"最后修改时间"字段,MySQL 会自动帮我们完成,既准确又省力。

5.6 三者的选择决策树

需求场景 推荐类型
仅存储日期,不关心时间,且日期跨度大(如公元前后) DATE
需要精确到秒,且所有客户端统一使用同一时区(如业务在东八区) DATETIME
需要自动记录创建和更新时间,或系统面向全球多时区用户 TIMESTAMP
需要存储早于 1970 年或晚于 2038 年的时间戳 DATETIME(避开 TIMESTAMP 的范围陷阱)

额外提醒:如果使用 TIMESTAMP 且希望插入时主动指定值(而非自动填充),只需在 INSERT 语句中显式提供合法的时间字符串即可。自动机制仅在字段被省略或显式赋值为 NULL(若允许 NULL)时触发。

第六章 字符串类型------ENUM 与 SET:约束型字符串的利器

在前几章中,我们探讨了 CHARVARCHAR 以及 TEXT 系列等自由文本类型。这类类型虽然灵活,却无法对字段的合法取值进行约束。在实际业务中,我们常常遇到"性别只能取男/女"、"兴趣爱好从固定列表中多选"等场景,此时就需要 MySQL 提供的两种特殊的字符串类型------ENUM(枚举)和 SET(集合)。

本章我们将深入剖析这两种类型的底层存储结构、插入查询技巧以及它们背后独特的索引逻辑,配合实战演示,让你彻底掌握何时用、怎么用。

6.1 ENUM 与 SET 的定位与存储差异

特性 ENUM(枚举) SET(集合)
含义 从预定义列表中单选一个值 从预定义列表中多选多个值
定义格式 ENUM('值1','值2',...,'值n') SET('值1','值2',...,'值n')
存储占用 1 或 2 字节(最多 65,535 个成员) 1、2、3、4 或 8 字节(最多 64 个成员)
内部存储 存储值为整数索引(1, 2, 3...) 存储值为位图(位掩码),每一位代表一个选项
排序规则 按定义的索引顺序排序,而非字典序 按定义的顺序存储和显示

ENUM 的本质不是字符串,而是映射到数字索引的"代号"。SET 的本质则是一个二进制位掩码,每一位(bit)独立控制一个选项的开关状态。

6.2 ENUM 类型 ------ 严谨的单选约束

6.2.1 基本定义与合法取值

假设我们有一个用户投票表 votes,其中包含用户名字段和性别字段。性别在大多数业务中是固定的二元或三元选项,使用 ENUM 再合适不过:

sql 复制代码
CREATE TABLE votes (
    username VARCHAR(20),
    gender ENUM('男', '女')   -- 仅允许插入'男'或'女'
);

ENUM 的成员列表一旦定义,插入非法值(如 人妖)时,MySQL 会抛出错误或插入空字符串(取决于 SQL 模式)。

6.2.2 插入数据:既可以用字符串,也可以用下标索引

ENUM 有一个非常巧妙的特性:除了直接写入定义的字符串外,还可以写入成员对应的下标数字 。成员在定义中的顺序从 1 开始编号(注意不是从 0 开始)。

  • 1 对应第一个成员 '男'
  • 2 对应第二个成员 '女'

因此,下面两条插入语句在效果上是等价的:

sql 复制代码
-- 方式一:直接写字符串
INSERT INTO votes (username, gender) VALUES ('张飞', '男');

-- 方式二:写下标索引(1 代表 '男')
INSERT INTO votes (username, gender) VALUES ('刘表', 1);

如果尝试使用 0 或超出范围的数字(如 3),MySQL 会将其视为无效值并做特殊处理(通常插入空字符串 '')。

6.2.3 ENUM 的查询陷阱:下标与字符串的微妙差异

既然 ENUM 底层存储的是数字索引,那么在查询条件中,直接使用数字和字符串的结果可能截然不同。

我们来看一组典型的查询演示:

sql 复制代码
-- 查询性别为 '女' 的记录(按字符串匹配)
SELECT * FROM votes WHERE gender = '女';
-- 结果:返回刘备、孙权(gender 显示为 '女')

-- 查询条件使用数字 1(即索引值为 1 的成员,即 '男')
SELECT * FROM votes WHERE gender = 1;
-- 结果:返回所有性别为 '男' 的记录(张飞、孙权、曹操、刘表等)

核心结论WHERE gender = '女' 比较的是字符串值,而 WHERE gender = 1 比较的是底层索引值。这一点经常让开发者困惑,尤其是在动态拼接 SQL 时,务必区分传入的是字面字符串还是代表枚举顺序的整数。

6.3 SET 类型 ------ 灵活的多选位图

SET 类型用于表示一组选项中的任意组合,非常适合存储用户的兴趣爱好、商品的多重标签、权限位等。

6.3.1 定义与成员排序

我们为 votes 表增加一个兴趣字段,预设五个常见项目:

sql 复制代码
ALTER TABLE votes ADD hobby SET('代码', '羽毛球', '乒乓球', '足球', '游泳');

SET 中的每个元素被分配一个固定的位值(bit value):

  • '代码' → 二进制 00001(位值 1)
  • '羽毛球' → 二进制 00010(位值 2)
  • '乒乓球' → 二进制 00100(位值 4)
  • '足球' → 二进制 01000(位值 8)
  • '游泳' → 二进制 10000(位值 16)

6.3.2 插入多值:字符串组合与位图数字

SET 列插入数据时,有两种主流的写法。

写法一:使用逗号分隔的字符串列表

多个值用英文逗号(,)隔开,且不允许有空格(虽然宽松模式下 MySQL 会容忍,但强烈建议不加空格):

sql 复制代码
-- 插入羽毛球和乒乓球
INSERT INTO votes (username, hobby) VALUES ('曹操', '羽毛球,乒乓球');

写法二:使用位图数字(比特位 / 位掩码)

因为每个选项对应一个 bit 位,我们可以直接传入该组合的十进制数值。例如:

  • 只选 '代码'(位值 1)→ 插入 1
  • 只选 '羽毛球'(位值 2)→ 插入 2
  • 同时选 '代码''羽毛球'(1+2=3)→ 插入 3
  • 一个都不选 → 插入 0(代表空集)
sql 复制代码
--  演示:插入 '刘表',gender 为 1(男),hobby 为 0(空集)
INSERT INTO votes VALUES ('刘表', 1, 0);

--  演示:插入 '刘表',gender 为 1(男),hobby 为 1(仅包含 '代码')
INSERT INTO votes VALUES ('刘表', 1, 1);

在实际查询中,hobby 字段存储 0 时,显示为空字符串(''),这明确代表了 "空集""未勾选任何选项"

6.3.3 重要区分:NULL 与空字符串('')的区别

在 MySQL 的字符串语境中,务必厘清三个易混淆的概念:

  • NULL :表示该字段的值"未知"或"不存在",参与任何运算结果仍为 NULL
  • ''(空字符串) :表示一个长度为 0 的字符串,它是有值的,只是内容为空。
  • 对于 SET 类型 :插入 0 或者插入 ''(在某些模式下),存储结果为内部空位图,表现为空字符串;而直接插入 NULL 则表示该值缺失。

此外,关于字符串的引号问题:MySQL 中字符串字面量推荐使用单引号(' ,这是 SQL 标准。虽然 MySQL 默认也允许双引号("),但为了兼容性和避免与标识符混淆,请始终使用单引号包裹字符串值

6.4 ENUM 与 SET 的高级查询技巧

6.4.1 SELECT 不仅是查表,更是计算器与函数容器

在深入 SET 查询之前,我们先回顾 MySQL 的一个基础但重要的特性:SELECT 语句本身就是一个表达式计算引擎,并不强制依赖 FROM 子句:

sql 复制代码
-- 直接进行算术运算
SELECT 1 + 1;  -- 返回 2

-- 直接调用内置函数
SELECT NOW();  -- 返回当前时间
SELECT UPPER('hello');  -- 返回 'HELLO'

这意味着在编写查询时,我们可以灵活地在 SELECTWHERE 等子句中嵌套各种函数来处理 SETENUM 字段。

6.4.2 SET 的精确匹配查询

如果我们要查询 hobby 完全等于 某个特定组合的记录,可以直接使用等号(=),此时必须保证顺序和内容完全一致:

sql 复制代码
-- 查询爱好精确为 '代码' 的用户(不包含任何其他爱好)
SELECT * FROM votes WHERE hobby = '代码';

这种严格匹配在业务中往往不够灵活,因为我们更常需要"包含某个兴趣"的记录。

6.4.3 查找包含单个元素的记录:FIND_IN_SET

FIND_IN_SET(str, strlist) 是 MySQL 专门为逗号分隔列表设计的函数。它返回第一个参数在第二个参数(逗号分隔列表)中的位置 (从 1 开始),如果找不到则返回 0

在 SQL 条件中,0 代表假(FALSE),非 0 代表真(TRUE)。因此,查找所有喜欢"羽毛球"的用户可以这样写:

sql 复制代码
-- 只要 hobby 中包含 '羽毛球',FIND_IN_SET 返回非 0 值,即满足条件
SELECT * FROM votes WHERE FIND_IN_SET('羽毛球', hobby);

返回结果中会包含"曹操(羽毛球)"、"刘表(代码,羽毛球)"等所有记录。

关键限制FIND_IN_SET 只能用于查找单个字符串是否存在于集合中 。如果要同时查找同时包含"羽毛球"和"代码"的用户,不能写成 FIND_IN_SET('羽毛球,代码', hobby),因为该函数只把它当作一个完整的字符串去匹配,不会拆分。

6.4.4 查找包含多个元素的记录:使用 AND 逻辑组合

既然一个 FIND_IN_SET 只能查一个元素,那么查找同时包含多个元素的需求,就要用多个条件通过 AND 连接:

sql 复制代码
-- 查找既喜欢 '羽毛球' 又喜欢 '代码' 的用户
SELECT * FROM votes 
WHERE FIND_IN_SET('羽毛球', hobby) 
  AND FIND_IN_SET('代码', hobby);

该查询会准确返回所有同时具备这两项爱好的记录,无论它们是否还包含其他选项。这种 AND 链式组合可以轻松扩展至三个、四个任意条件,极大地增强了 SET 在复杂标签筛选场景下的实用性。

6.5 ENUM 和 SET 的最佳实践与避坑指南

  1. 慎用 ENUM 的索引值查询 :如上文所述,WHERE gender = 1 容易产生歧义,代码可读性极差。除非有极特殊的性能优化需求,否则始终建议使用字符串值进行比较(如 WHERE gender = '男'),让同事和维护者一目了然。

  2. SET 插入时的空格问题 :插入字符串列表如 '代码,羽毛球' 时,逗号后坚决不要加空格。虽然某些版本会自动截断空格,但跨版本行为不一致,且 FIND_IN_SET 对空格敏感('羽毛球'' 羽毛球' 不匹配)。

  3. 长度上限ENUM 最多支持 65,535 个成员,SET 最多支持 64 个成员。若选项超过 64 个,请改用关联表(一对多)设计,切勿强行使用 SET

  4. 排序诡异ORDER BY genderENUM 而言,排序依据是定义时的索引顺序(男=1,女=2),而不是拼音或笔画。如果期望字典序,需要额外使用 CAST(gender AS CHAR) 转换。

  5. 修改定义的代价ENUMSET 的成员列表一旦变更(尤其是中间插入新值),MySQL 可能会重建整个表,在数据量巨大时带来极高的 IO 开销。在设计之初请尽量预留充足的选项空间。

6.6 总结

ENUMSET 是 MySQL 区别于其他关系型数据库的特色类型。ENUM 以紧凑的数字索引存储,提供高效的单选约束;SET 凭借位图存储,以极少的空间代价实现了多选组合的灵活存储。

在实际开发中:

  • 当业务字段取值固定且单选 (如性别、状态、级别)时,优先考虑 ENUM
  • 当业务字段取值固定但复选 (如标签、权限、偏好)时,优先考虑 SET
  • 对于复杂的多对多关系,或者选项数量超过 64 个,请回归标准的关系表设计。
相关推荐
denggun123451 小时前
yield
前端·数据库·python
ltl1 小时前
SQLite Rollback Journal:写前拷贝与提交点
数据库
ltl1 小时前
TiFlash Learner:Raft 日志如何进列存
数据库
ltl1 小时前
BM25 与 Lucene Similarity:公式如何落到 postings
数据库
ltl1 小时前
RocksDB Block Cache 与 Bloom Filter:读放大怎么裁
数据库
deli0070073 小时前
JSON 树形查看器:粘贴即解析,折叠展开一目了然
前端·数据库·json·ai编程
国科安芯3 小时前
轨道供电韧性:ASP4644四通道降压架构在低轨卫星平台电源管理中的适用性分析
运维·网络·数据库·嵌入式硬件·架构·状态模式·低轨卫星星座
好好学习的星熊3 小时前
SQL 面试题(数据分析师)
sql·mysql
破土士V4 小时前
MySQL基础知识集合
数据库·mysql