在C++开发中,我们对基本数据类型的边界(如
int、
float、
double的内存占用与精度)以及位运算(Bitwise operations)了如指掌。然而,当我们把视角转向数据库,尤其是 MySQL 时,数据类型的选用和底层表现同样至关重要。
如果选型不当,不仅会浪费珍贵的磁盘和内存空间,更可能导致高并发下的高精度计算失真(如金融场景),或者因超出范围而发生插入异常。今天,我将结合底层设计与越界测试,带大家彻底搞懂 MySQL 的数据类型系统。
MySQL 的数据类型丰富多样,大体上可以分为以下四大核心版块:
| 分类 | 核心类型 | 适用场景 |
|---|---|---|
| 数值类型 | BIT、TINYINT、INT、BIGINT、FLOAT、DECIMAL | 存储数字(年龄、金额、计数等) |
| 字符串类型 | CHAR、VARCHAR、TEXT、BLOB | 存储文本(姓名、地址、大文本、二进制数据) |
| 日期时间类型 | DATE、DATETIME、TIMESTAMP | 存储时间(生日、创建时间、时间戳) |
| 特殊字符串 | ENUM(枚举)、SET(集合) | 固定选项(性别、爱好、状态等) |
| 二进制类型 | BLOB | 存储图片、文件等二进制数据 |

数值类型是最常用的类型,核心关注范围和精度,避免数据溢出或精度丢失。
整数类型按占用字节和范围分为 5 类,支持
UNSIGNED
(无符号)修饰(默认有符号):类型 | 字节 | 最小值 (带符号 / 无符号) | 最大值 (带符号 / 无符号) |
|---|---|---|---|
TINYINT | -128 / 0 | 127 / 255 | |
SMALLINT | -32768 / 0 | 32767 / 65535 | |
MEDIUMINT | -8388608 / 0 | 8388607 / 16777215 | |
INT | -2147483648 / 0 | 2147483647 / 4294967295 | |
BIGINT | -9223372036854775808 / 0 | 9223372036854775807 / 18446744073709551615 |
关键实战要点:
TINYINT UNSIGNED
插入 - 1 直接报错),且与有符号类型计算时容易出现逻辑问题。建议直接用更大的整数类型(如用INT
替代TINYINT UNSIGNED
)。TINYINT 占用 1 字节,有符号范围 - 128~127,无符号范围 0~255:

? C++博主避坑建议: > 尽可能少用
UNSIGNED。对于一个容易溢出的
INT,
INT UNSIGNED的物理极限虽然翻倍,但面对海量业务同样可能面临溢出。不如在设计之初,直接将字段类型升级为
BIGINT,避免后期在线改表的痛苦。

语法:
BIT[(M)]
,M 表示每个值的位数(Bit),范围 1-64。如果忽略 M,默认值为 1。
当我们在终端中查询
BIT
类型时,可能会发现值“神秘消失”或显示怪异:

原因分析:
BIT
字段在终端显示时,是按照其对应的 ASCII 码字符进行渲染的。查询时按 ASCII 码显示
mysql> insert into tt4 values (65, 65); -- 65 对应的 ASCII 字符是 'A'mysql> select * from tt4;+------+------+| id | a |+------+------+| 10 | || 65 | A | -- 此时显示出了字符 'A'+------+------+
-- 1. INT存储手机号(越界测试)CREATE TABLE test_int(phone INT);INSERT INTO test_int VALUES(13800138000); -- 报错:Out of range value for column 'phone' at row 1(INT最大值2147483647 < 13800138000)-- 2. BIGINT存储手机号(成功)CREATE TABLE test_bigint(phone BIGINT);INSERT INTO test_bigint VALUES(13800138000); -- 成功SELECT * FROM test_bigint;+-------------+| phone |+-------------+| 13800138000 |+-------------+
FLOAT[(M, D)] [UNSIGNED]
,M 指定显示长度(含小数点和负号),D 指定小数位数。占用 4 字节。-- 定义 float(4,2),表示范围在 -99.99 到 99.99 之间mysql> create table tt6(id int, salary float(4,2));mysql> insert into tt6 values (100, -99.99); -- 边界插入mysql> insert into tt6 values (101, 99.991); -- 多出的一位小数四舍五入被拿掉mysql> select * from tt6;+------+--------+| id | salary |+------+--------+| 100 | -99.99 || 101 | 99.99 |+------+--------+

思考:如果定义为
FLOAT(6,3)
有符号,它的数值范围是多少? 答案是:-999.999 - 999.999。如果是无符号
FLOAT(4,2) UNSIGNED
,其范围则是 0 - 99.99(对负数报错拦截)。对于高精度的商业计算,我们通常会选用
DECIMAL
。DECIMAL(M, D) [UNSIGNED]
接下来我们看一波
FLOAT
vs
DECIMAL
的真实精度对比:mysql> create table tt8 (id int, salary float(10,8), salary2 decimal (10,8));mysql> insert into tt8 values (100, 23.12345612, 23.12345612);mysql> select * from tt8;+------+-------------+-------------+| id | salary | salary2 |+------+-------------+-------------+| 100 | 23.12345695 | 23.12345612 | +------+-------------+-------------+-- 瞧!float(10,8) 存入的值被失真变成了 23.12345695!而 decimal(10,8) 依然保持绝对精确。

原因:
FLOAT
的单精度浮点数有效精度大约只有 7 位(C++ 同理)。凡是涉及金钱、科学计数、高精度的场景,一律强推使用
DECIMAL
。在字符串处理上,MySQL 分为定长和变长两种核心策略:
mysql> create table tt9(id int, name char(2));mysql> insert into tt9 values (100, 'ab'); -- 成功mysql> insert into tt9 values (101, '中国'); -- 成功(存放了两个汉字字符)-- 如果超过 255mysql> create table tt10(id int, name char(256));ERROR 1074 (42000): Column length too big for column 'name' (max=255); use BLOB or TEXT instead

VARCHAR 的实际有效存储字节上限为 65532 字节(因为有 1~3 个字节用于记录实际数据的大小)。-- 验证 utf8 下 L=21845 越界:mysql> create table tt11(name varchar(21845)) charset=utf8;ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBS, is 65535.-- 验证 L=21844 成功创建:mysql> create table tt11(name varchar(21844)) charset=utf8;Query OK, 0 rows affected (0.01 sec)
主流编码下的字节数
| 编码 | 一个汉字的字节数 | 说明 |
|---|---|---|
| UTF-8 | 3 字节 | 最通用的编码,网页、大多数数据库默认 |
| GBK / GB2312 | 2 字节 | 简体中文环境常用,Windows 系统常见 |
| UTF-16 (LE/BE) | 2~4 字节 | 多数常用汉字占 2 字节,生僻字可能占 4 字节 |
在底层存储中,我们假设在
utf8
编码下:
实际存储内容 | CHAR(4) 占用空间 | VARCHAR(4) 占用空间 |
|---|---|---|
| 4 * 3 = 12 bytes | 4 * 3 + 1 (长度信息) = 13 bytes |
| 4 * 3 = 12 bytes (浪费空间) | 1 * 3 + 1 (长度信息) = 4 bytes (按需开辟) |
| 数据超长,拦截报错 | 数据超长,拦截报错 |
MySQL 日常开发中常用的日期时间类型主要有三种:
类型 | 格式 | 占用字节 | 特点 |
|---|---|---|---|
DATE |
| 3 字节 | 只存储日期 |
DATETIME |
| 8 字节 | 表示范围从 1000 到 9999 年 |
TIMESTAMP |
| 4 字节 | 时间戳。插入/更新数据时会自动刷新为当前时间 |
-- 1. 创建表mysql> create table birthday (t1 date, t2 datetime, t3 timestamp);-- 2. 仅插入 t1 和 t2mysql> insert into birthday (t1, t2) values('1997-7-1', '2008-8-8 12:1:1');mysql> select * from birthday;+------------+---------------------+---------------------+| t1 | t2 | t3 |+------------+---------------------+---------------------+| 1997-07-01 | 2008-08-08 12:01:01 | 2017-11-12 18:28:55 | -- t3(时间戳) 自动补上当前时间+------------+---------------------+---------------------+-- 3. 更新 t1 的数值mysql> update birthday set t1='2000-1-1';mysql> select * from birthday;+------------+---------------------+---------------------+| t1 | t2 | t3 |+------------+---------------------+---------------------+| 2000-01-01 | 2008-08-08 12:01:01 | 2017-11-12 18:32:09 | -- 更新操作会同步刷新时间戳!+------------+---------------------+---------------------+这两个属于限制型、带有约束属性的字符串类型。
, 隔开。内部同样以数字存储,但和 Linux 文件权限一样,采用比特位(1, 2, 4, 8, 16...)映射的方式来判定集合内的爱好组合。最多支持 64 个选项。⚠️ 避坑指南: 在实际业务中,不推荐在
INSERT时使用数字标号代替文本,因为可读性较差,维护成本高。
我们创建一个问卷投票表:
mysql> create table votes ( username varchar(30), hobby set('登山','游泳','篮球','武术'), gender enum('男','女'));-- 使用文本插入mysql> insert into votes values('雷锋','登山,武术','男');-- 混用数字代号插入('女' 对应的 enum 下标为 2)mysql> insert into votes values('Juse','登山,武术', 2);mysql> select * from votes where gender=2;+----------+---------------+--------+| username | hobby | gender |+----------+---------------+--------+| Juse | 登山,武术 | 女 |+----------+---------------+--------+假设我们现有数据如下:
+------------+---------------+--------+| username | hobby | gender |+------------+---------------+--------+| 雷锋 | 登山,武术 | 男 || Juse | 登山,武术 | 女 || LiLei | 登山 | 男 || HanMeiMei | 游泳 | 女 |+------------+---------------+--------+
如果你尝试用常规的
=
去匹配查询喜欢“登山”的人:
mysql> select * from votes where hobby='登山';+----------+-------+--------+| username | hobby | gender |+----------+-------+--------+| LiLei | 登山 | 男 |+----------+-------+--------+
问题: 结果只查出了李雷。因为等号 = 做的是完整字符串精确匹配!像雷锋、Juse 这种除了登山还喜欢武术的人,直接被过滤掉了。
在 MySQL 中,查找集合中是否包含某项,必须依靠内置函数
find_in_set(sub, str_list)
:sub
存在于以逗号隔开的字符串
str_list
中,返回对应的元素下标(从1开始);否则返回 0。
mysql> select find_in_set('a', 'a,b,c'); -- 返回 1mysql> select find_in_set('d', 'a,b,c'); -- 返回 0-- 完美查询出所有爱好包含 "登山" 的人:mysql> select * from votes where find_in_set('登山', hobby);+----------+---------------+--------+| username | hobby | gender |+----------+---------------+--------+| 雷锋 | 登山,武术 | 男 || Juse | 登山,武术 | 女 || LiLei | 登山 | 男 |+----------+---------------+--------+从底层的位运算(
BIT
、SET
)到高精度的 DECIMAL
处理,我们可以看出,MySQL 的数据类型设计和 C/C++ 的底层内存对齐、结构体映射有着极高的相通之处。合理地规划字段类型,不仅能为你的系统省下海量存储,更能免除后续的各种越界、精度异常 Bug。