文章目录
- mysql数据类型完全指南:从入门到规范,一篇打通
-
- 前言:建表选类型,就像给数据选房子
- 一、数据类型全家桶总览
- 二、数值类型:从牙签到航母的整数与小数
-
- [2.1 整型五兄弟:容量越大,占的字节越多](#2.1 整型五兄弟:容量越大,占的字节越多)
-
- [2.1.1 tinyint 完整语法案例](#2.1.1 tinyint 完整语法案例)
- [2.1.2 smallint 完整语法案例](#2.1.2 smallint 完整语法案例)
- [2.1.3 int 完整语法案例](#2.1.3 int 完整语法案例)
- [2.1.4 bigint 完整语法案例(推荐主键用)](#2.1.4 bigint 完整语法案例(推荐主键用))
- [2.1.5 经典误区:括号里的数字不是存储长度](#2.1.5 经典误区:括号里的数字不是存储长度)
- [2.1.6 unsigned 真的香吗?](#2.1.6 unsigned 真的香吗?)
- [2.2 bit类型:插入比特位完整语法](#2.2 bit类型:插入比特位完整语法)
-
- [2.2.1 两种插入比特位的方式](#2.2.1 两种插入比特位的方式)
- [2.2.2 查询的正确姿势(必转,否则显示乱码)](#2.2.2 查询的正确姿势(必转,否则显示乱码))
- [2.2.3 越界测试](#2.2.3 越界测试)
- [2.2.4 位运算实战案例(状态标记)](#2.2.4 位运算实战案例(状态标记))
- [2.3 小数类型:浮点精度是大坑](#2.3 小数类型:浮点精度是大坑)
-
- [2.3.1 float 完整语法案例](#2.3.1 float 完整语法案例)
- [2.3.2 double 完整语法案例](#2.3.2 double 完整语法案例)
- [2.3.3 decimal 完整语法案例(金额必用)](#2.3.3 decimal 完整语法案例(金额必用))
- 三、字符串类型:定长变长别乱用
-
- [3.1 char 完整语法案例](#3.1 char 完整语法案例)
- [3.2 varchar 完整语法案例](#3.2 varchar 完整语法案例)
- [3.3 char vs varchar 存储对比](#3.3 char vs varchar 存储对比)
- [3.4 text / blob 完整语法案例](#3.4 text / blob 完整语法案例)
- 四、日期时间类型:小心2038年炸弹
-
- [4.1 三类日期完整对比案例](#4.1 三类日期完整对比案例)
- [4.2 三种类型参数对比](#4.2 三种类型参数对比)
- [4.3 巨坑:2038年问题](#4.3 巨坑:2038年问题)
- [4.4 推荐写法:datetime 自动填充](#4.4 推荐写法:datetime 自动填充)
- [五、enum 与 set:单选多选底层原理与查询大全](#五、enum 与 set:单选多选底层原理与查询大全)
-
- [5.1 enum:单选枚举](#5.1 enum:单选枚举)
-
- [5.1.1 基础语法](#5.1.1 基础语法)
- [5.1.2 底层原理:顺序编号存储](#5.1.2 底层原理:顺序编号存储)
- [5.1.3 查询用法:直接等值匹配即可](#5.1.3 查询用法:直接等值匹配即可)
- [5.1.4 最大的坑:加选项要改表结构](#5.1.4 最大的坑:加选项要改表结构)
- [5.2 set:多选集合](#5.2 set:多选集合)
-
- [5.2.1 底层原理:比特位编码](#5.2.1 底层原理:比特位编码)
- [5.2.2 查询用法大全(7种)](#5.2.2 查询用法大全(7种))
-
- [一、精确匹配:`=` 等值查询](#一、精确匹配:
=等值查询) - [二、单值包含查询:`find_in_set` 标准写法](#二、单值包含查询:
find_in_set标准写法) - [三、单值包含查询:位运算 `&` 高性能写法](#三、单值包含查询:位运算
&高性能写法) - [四、多选项同时包含(AND 关系)](#四、多选项同时包含(AND 关系))
- [五、多选项任选其一(OR 关系)](#五、多选项任选其一(OR 关系))
- [六、空集合 / 非空集合判断](#六、空集合 / 非空集合判断)
- [七、不推荐:like 模糊查询](#七、不推荐:like 模糊查询)
- [一、精确匹配:`=` 等值查询](#一、精确匹配:
- [5.2.3 用法汇总对比表](#5.2.3 用法汇总对比表)
- [5.2.4 业务开发建议](#5.2.4 业务开发建议)
- [5.3 enum vs set 核心区别对比表](#5.3 enum vs set 核心区别对比表)
- 六、企业级使用规范与选型标准
- 七、综合实战:一张标准业务表完整建表语句
- 八、避坑口诀与建表checklist
- 写在最后
mysql数据类型完全指南:从入门到规范,一篇打通
前言:建表选类型,就像给数据选房子
很多新手学mysql,第一次建表都会灵魂拷问:
存手机号用啥类型?int?varchar?
存金额用float行不行?不都是小数吗?
性别存enum是不是显得很专业?
其实选数据类型就像给数据租房子:
- 买太大:空着大半间,白白浪费磁盘空间,查询还慢
- 买太小:数据直接"住不下",溢出报错给你看
- 户型错了:明明是个布尔值,你给整个int,纯属浪费
本文整合了mysql数据类型全部基础语法、边界实测、踩坑案例和企业级开发规范,每个类型都附完整可运行语法案例,从入门概念到落地标准一篇讲透。
一、数据类型全家桶总览
mysql的数据类型可以分成四大类,先上总览表混个脸熟:
| 分类 | 数据类型 | 说明 |
|---|---|---|
| 数值类型 | bit(m) | 位类型,m指定位数,默认1,范围1-64 |
| tinyint unsigned | 带符号-128127,无符号0255,默认有符号 | |
| bool | 用0和1表示真和假,底层就是tinyint | |
| smallint unsigned | 带符号-2^15 ~ 215-1,无符号0~216-1 | |
| int unsigned | 带符号-2^31 ~ 231-1,无符号0~232-1 | |
| bigint unsigned | 带符号-2^63 ~ 263-1,无符号0~264-1 | |
| float(m,d) unsigned | m指定显示长度,d指定小数位数,占4字节 | |
| double(m,d) unsigned | 精度比float更高,占8字节 | |
| decimal(m,d) unsigned | 定点数,m总长度,d小数位数 | |
| 字符串类型 | char(size) | 固定长度字符串,最大255字符 |
| varchar(size) | 可变长度字符串,最大65535字节 | |
| blob | 二进制大对象 | |
| text | 大文本,不支持全文索引,不支持默认值 | |
| 日期时间类型 | date / datetime / timestamp | 日期、日期时间、时间戳 |
| 枚举集合类型 | enum | 枚举,单选,值只能从预设列表选一个 |
| set | 集合,多选,值可以是预设列表的任意组合 |
二、数值类型:从牙签到航母的整数与小数
2.1 整型五兄弟:容量越大,占的字节越多
整型家族一共5个成员,区别就是占用字节数不同,能存的数字范围也不同,就像从单间到别墅的区别。
| 类型 | 占用字节 | 有符号最小值 | 有符号最大值 | 无符号最小值 | 无符号最大值 |
|---|---|---|---|---|---|
| tinyint | 1 | -128 | 127 | 0 | 255 |
| smallint | 2 | -32768 | 32767 | 0 | 65535 |
| mediumint | 3 | -8388608 | 8388607 | 0 | 16777215 |
| int | 4 | -2147483648 | 2147483647 | 0 | 4294967295 |
| bigint | 8 | -9e18左右 | 9e18左右 | 0 | 1.8e19左右 |
2.1.1 tinyint 完整语法案例
基础建表与边界测试
sql
-- 1. 创建有符号tinyint表
create table tt1 (
num tinyint
) engine=innodb default charset=utf8;
-- 2. 插入正常值
insert into tt1 values(1);
insert into tt1 values(-128); -- 最小值
insert into tt1 values(127); -- 最大值
-- 3. 插入越界值,直接报错
insert into tt1 values(128);
-- error 1264 (22003): out of range value for column 'num' at row 1
insert into tt1 values(-129);
-- error 1264 (22003): out of range value for column 'num' at row 1
-- 4. 查询结果
select * from tt1;
查询输出:
+------+
| num |
+------+
| 1 |
| -128 |
| 127 |
+------+
unsigned 无符号完整案例
sql
-- 1. 创建无符号tinyint表
create table tt2 (
num tinyint unsigned
);
-- 2. 插入正常值
insert into tt2 values(0);
insert into tt2 values(255); -- 无符号最大值
-- 3. 插入负数,直接报错
insert into tt2 values(-1);
-- error 1264 (22003): out of range value for column 'num' at row 1
-- 4. 查询结果
select * from tt2;
2.1.2 smallint 完整语法案例
sql
-- 1. 创建表
create table test_smallint (
num smallint
);
-- 2. 边界测试
insert into test_smallint values(-32768); -- 最小值,成功
insert into test_smallint values(32767); -- 最大值,成功
insert into test_smallint values(32768); -- 越界,报错
-- error 1264 (22003): out of range value for column 'num' at row 1
-- 3. 无符号版本
create table test_smallint_unsigned (
num smallint unsigned
);
insert into test_smallint_unsigned values(65535); -- 无符号最大值
2.1.3 int 完整语法案例
sql
-- 1. 创建普通int表
create table test_int (
id int,
count int
);
-- 2. 边界测试
insert into test_int values(1, 2147483647); -- 最大值,成功
insert into test_int values(2, -2147483648); -- 最小值,成功
insert into test_int values(3, 2147483648); -- 越界,报错
-- error 1264 (22003): out of range value for column 'count' at row 1
-- 3. 常用自增主键写法(不推荐int做主键,仅演示语法)
create table test_int_auto (
id int not null auto_increment primary key,
name varchar(32)
);
insert into test_int_auto(name) values('张三');
select * from test_int_auto;
2.1.4 bigint 完整语法案例(推荐主键用)
sql
-- 1. 标准自增主键表写法
create table test_bigint (
id bigint not null auto_increment comment '主键id',
username varchar(32) not null default '' comment '用户名',
primary key (id)
) engine=innodb default charset=utf8mb4 comment '测试bigint表';
-- 2. 插入数据
insert into test_bigint(username) values('user1');
insert into test_bigint(username) values('user2');
-- 3. 查询
select * from test_bigint;
✅ 最佳实践:业务表主键一律使用bigint,避免数据量增长后int溢出。
2.1.5 经典误区:括号里的数字不是存储长度
新手第一眼看到 tinyint(4),90%的人都会以为"最多存4位数字",这是mysql入门第一大经典误区!
sql
create table `t1` (
`num` tinyint(4) default null
) engine=innodb default charset=utf8;
insert into t1 values (-128);
insert into t1 values (127);
insert into t1 values (0);
insert into t1 values (-1);
insert into t1 values (1);
select * from t1;
查询结果全部成功插入,范围依然是-128~127,和括号里的4没有任何关系。
核心结论:括号里的数字叫「显示宽度」,和存储范围半毛钱关系都没有
- 只有搭配
zerofill(零填充)时才生效,比如tinyint(4) zerofill,存数字5会显示成0005- 不带zerofill的话,这个括号里的数字纯纯就是个摆设
- mysql 8.0已经开始废弃整数类型的显示宽度语法,未来版本会移除
2.1.6 unsigned 真的香吗?
很多人觉得"我存的数肯定是正的,加个unsigned能多存一倍,血赚!"
但社区里无数踩坑经验告诉你:尽量别随便用unsigned。
原因很简单:
- 两个unsigned数做减法,可能出现意想不到的溢出结果(比如1-2会变成超大数)
- 如果int存不下了,int unsigned大概率也迟早存不下,与其后期改类型锁表,不如一开始直接上bigint
2.2 bit类型:插入比特位完整语法
bit(m) 是mysql里唯一按「比特(bit)」为单位存储的类型,m表示分配的比特位数,范围1~64,默认是1。
2.2.1 两种插入比特位的方式
sql
-- 1. 创建表,a字段8个比特位
create table tt4 (
id int,
a bit(8)
);
-- 方式一:插入十进制数字,mysql自动转二进制
insert into tt4 values(10, 10); -- 十进制10 → 二进制00001010
-- 方式二:直接插入比特字面量 b'二进制串'
insert into tt4 values(11, b'00001010'); -- 和上面效果完全等价
2.2.2 查询的正确姿势(必转,否则显示乱码)
sql
-- 错误写法:直接查询,显示ascii字符,看不到数字
select * from tt4;
-- 正确写法:用函数转换
select
id,
a + 0 as 十进制数值,
bin(a) as 二进制字符串,
hex(a) as 十六进制字符串
from tt4;
查询输出:
+------+--------------+----------------+----------------+
| id | 十进制数值 | 二进制字符串 | 十六进制字符串 |
+------+--------------+----------------+----------------+
| 10 | 10 | 1010 | a |
| 11 | 10 | 1010 | a |
+------+--------------+----------------+----------------+
2.2.3 越界测试
sql
create table tt5 (
gender bit(1)
);
insert into tt5 values(0); -- 成功
insert into tt5 values(1); -- 成功
insert into tt5 values(2); -- 超过1位,报错
-- error 1406 (22001): data too long for column 'gender' at row 1
2.2.4 位运算实战案例(状态标记)
sql
-- 1. 创建用户状态表,4个比特位存4个开关
create table user_flag (
uid int,
status bit(4) comment 'bit0邮箱验证 bit1手机验证 bit2会员 bit3拉黑'
);
-- 2. 插入:邮箱已验证、手机未验证、非会员、已拉黑 → b'1001'
insert into user_flag values(1001, b'1001');
insert into user_flag values(1002, b'0011'); -- 邮箱+手机都验证,普通用户
-- 3. 查询:所有邮箱验证过的用户(判断第0位是否为1)
select uid, status+0 from user_flag where (status & b'0001') != 0;
-- 4. 修改:给用户1001开通会员(把第2位置为1)
update user_flag set status = status | b'0100' where uid = 1001;
-- 5. 修改:取消拉黑(把第3位置为0)
update user_flag set status = status & ~b'1000' where uid = 1001;
✅ 最佳实践:普通业务开发直接用tinyint存状态,别为了省几个字节给自己挖坑。只有亿级以上海量数据、对接硬件二进制数据的场景,才考虑bit。
2.3 小数类型:浮点精度是大坑
小数类型有三个:float、double、decimal,前两个是浮点数,最后一个是定点数。
2.3.1 float 完整语法案例
语法:float[(m, d)] [unsigned]
- m:总显示长度,d:小数位数,占用4字节
- 保存时自动四舍五入
sql
-- 1. 创建表 float(4,2):总长度4位,小数2位 → 范围 -99.99 ~ 99.99
create table tt6 (
id int,
salary float(4,2)
);
-- 2. 插入测试
insert into tt6 values(100, -99.99); -- 边界值,成功
insert into tt6 values(101, -99.991); -- 四舍五入后还是-99.99
insert into tt6 values(102, 99.99); -- 最大值
insert into tt6 values(103, 100); -- 越界,报错
-- error 1264 (22003): out of range value for column 'salary' at row 1
-- 3. 查询
select * from tt6;
查询结果:
+------+--------+
| id | salary |
+------+--------+
| 100 | -99.99 |
| 101 | -99.99 |
| 102 | 99.99 |
+------+--------+
unsigned 无符号版本
sql
create table tt7 (
id int,
salary float(4,2) unsigned
);
insert into tt7 values(100, 0);
insert into tt7 values(101, 99.99);
insert into tt7 values(102, -0.1); -- 负数,触发警告,被截断为0
-- query ok, 1 row affected, 1 warning
show warnings;
-- warning 1264 out of range value for column 'salary' at row 1
2.3.2 double 完整语法案例
sql
-- 1. 创建表,double精度比float高
create table test_double (
id int,
num double(10,5)
);
insert into test_double values(1, 12345.12345);
-- 2. float和double精度对比
create table compare_float_double (
f float(20,10),
d double(20,10)
);
insert into compare_float_double values(1234567.123456789, 1234567.123456789);
select * from compare_float_double;
-- float精度丢失,double更精确,但依然不是绝对精确
2.3.3 decimal 完整语法案例(金额必用)
语法:decimal(m, d) [unsigned]
- m:总位数(整数+小数),最大65,默认10
- d:小数位数,最大30,默认0
sql
-- 1. 创建表 decimal(5,2):总5位,小数2位 → 范围 -999.99 ~ 999.99
create table test_decimal (
id int,
price decimal(5,2)
);
insert into test_decimal values(1, 999.99); -- 最大值
insert into test_decimal values(2, -999.99); -- 最小值
insert into test_decimal values(3, 1000); -- 越界报错
-- error 1264 (22003): out of range value for column 'price' at row 1
精度对比终极实验:float vs decimal
sql
create table tt8 (
id int,
salary float(10,8),
salary2 decimal(10,8)
);
insert into tt8 values(100, 23.12345612, 23.12345612);
select * from tt8;
查询结果:
+------+-------------+-------------+
| id | salary | salary2 |
+------+-------------+-------------+
| 100 | 23.12345695 | 23.12345612 |
+------+-------------+-------------+
结论:float精度大约7位有效数字,会丢失精度;decimal完全精确,金额必须用decimal。
参数省略测试
sql
create table test_decimal_default (
num decimal -- 省略m和d,默认m=10,d=0
);
insert into test_decimal_default values(1234567890); -- 10位整数
insert into test_decimal_default values(123.45); -- 小数部分被截断
select * from test_decimal_default;
-- 输出 1234567890 和 123
✅ 行业强制规范:所有金额、价格等高精度小数,必须使用decimal类型,禁止使用float和double。
三、字符串类型:定长变长别乱用
字符串是用得最多的类型,char和varchar的区别也是面试高频题,其实很好理解:
- char = 固定大小的快递盒,不管装多少,都占满空间
- varchar = 可伸缩的帆布袋,装多少占多少,多花1-2字节记大小
3.1 char 完整语法案例
- 语法:
char(l),l是字符数,最大255 - 特点:固定长度,不足补空格
sql
-- 1. 创建表 char(2):最多存2个字符
create table tt9 (
id int,
name char(2)
);
-- 2. 插入测试
insert into tt9 values(100, 'ab'); -- 2个英文字符,成功
insert into tt9 values(101, '中国'); -- 2个汉字,成功(按字符数算,不是字节)
insert into tt9 values(102, 'abc'); -- 3个字符,超长报错
-- error 1406 (22001): data too long for column 'name' at row 1
-- 3. 查询
select * from tt9;
最大长度限制测试
sql
create table tt10 (
id int,
name char(256) -- 超过255,直接报错
);
-- error 1074 (42000): column length too big for column 'name' (max = 255); use blob or text instead
适用场景:长度固定的内容,如手机号char(11)、身份证号char(18)、md5值char(32)。
3.2 varchar 完整语法案例
- 语法:
varchar(l),l是字符数 - 特点:可变长度,实际占用 = 数据长度 + 1~2字节(记录长度)
sql
-- 1. 创建表 varchar(6):最多6个字符
create table tt10 (
id int,
name varchar(6)
);
-- 2. 插入测试
insert into tt10 values(100, 'hello'); -- 5个英文字符
insert into tt10 values(101, '我爱你中国'); -- 5个汉字
insert into tt10 values(102, 'hello world'); -- 超长报错
select * from tt10;
最大长度与编码关系验证(utf8)
sql
-- utf8编码1个字符占3字节,一行最大65535字节,varchar有效字节65532
-- 最大字符数 = 65532 / 3 = 21844
-- 写21845,报错
create table tt11(name varchar(21845)) charset=utf8;
-- error 1118 (42000): row size too large
-- 写21844,成功
create table tt11(name varchar(21844)) charset=utf8;
-- query ok
注意:utf8mb4编码下,1字符占4字节,最大只能到 65532/4 = 16383 字符。
3.3 char vs varchar 存储对比
以utf8编码、char(4)和varchar(4)为例:
| 实际存储内容 | char(4)存储 | char占用字节 | varchar(4)存储 | varchar占用字节 |
|---|---|---|---|---|
| abcd | abcd | 4×3 = 12 | abcd | 4×3 + 1 = 13 |
| a | a(补空格) | 4×3 = 12 | a | 1×3 + 1 = 4 |
| abcde | 报错超长 | - | 报错超长 | - |
✅ 选型原则:
长度固定用char,长度变化用varchar
定长效率高,变长省空间
别没事就写varchar(255),根据业务长度来,留20%余量就行
3.4 text / blob 完整语法案例
sql
-- 1. 创建text表
create table article (
id int,
title varchar(128),
content text -- 大文本,最多64kb
);
-- 2. 插入数据
insert into article values(1, '文章标题', '这是文章正文内容...');
-- 3. 尝试给text设置默认值,报错
create table test_text_default (
content text default ''
);
-- error 1101 (42000): blob/text column 'content' can't have a default value
text分类及大小
| 类型 | 最大容量 | 适用场景 |
|---|---|---|
| tinytext | 255字节 | 极短文本,基本不用 |
| text | 64kb | 普通长文章、大备注 |
| mediumtext | 16mb | 长文章、富文本内容 |
| longtext | 4gb | 超大文本,极少使用 |
使用注意:
- text/blob不能设默认值,不能加普通索引(只能加前缀索引)
- 大字段建议拆分到副表,避免影响主表查询性能
- 图片、文件不建议存数据库,放对象存储
四、日期时间类型:小心2038年炸弹
4.1 三类日期完整对比案例
sql
-- 1. 创建表,同时包含三种日期类型
create table birthday (
t1 date,
t2 datetime,
t3 timestamp
);
-- 2. 插入数据
insert into birthday(t1, t2) values('1997-7-1', '2008-8-8 12:1:1');
-- t3没有赋值,会自动填充当前时间
-- 3. 查询
select * from birthday;
查询结果(t3自动填上了插入时的时间):
+------------+---------------------+---------------------+
| t1 | t2 | t3 |
+------------+---------------------+---------------------+
| 1997-07-01 | 2008-08-08 12:01:01 | 2026-10-03 15:30:00 |
+------------+---------------------+---------------------+
更新测试:timestamp自动更新
sql
update birthday set t1 = '2000-1-1';
select * from birthday;
查询结果(t3自动变成了更新时的时间):
+------------+---------------------+---------------------+
| t1 | t2 | t3 |
+------------+---------------------+---------------------+
| 2000-01-01 | 2008-08-08 12:01:01 | 2026-10-03 15:35:00 |
+------------+---------------------+---------------------+
4.2 三种类型参数对比
| 类型 | 占用字节 | 格式 | 范围 | 特点 |
|---|---|---|---|---|
| date | 3 | yyyy-mm-dd | 1000-01-01 ~ 9999-12-31 | 只存日期,不带时分秒 |
| datetime | 8 | yyyy-mm-dd hh:mm:ss | 1000-01-01 ~ 9999-12-31 | 日期+时间,与时区无关 |
| timestamp | 4 | yyyy-mm-dd hh:mm:ss | 1970-01-01 ~ 2038-01-19 | 时间戳,自动更新,有时区 |
4.3 巨坑:2038年问题
timestamp用4字节存unix时间戳,最大只能到2038年1月19日,到时候所有用timestamp的系统都会时间溢出,就像当年的千年虫一样。
而且timestamp还带时区属性:存的时候转成utc,查的时候转成当前时区,跨时区业务很容易踩坑。
4.4 推荐写法:datetime 自动填充
sql
-- 标准业务表时间字段写法
create table user_info (
id bigint primary key auto_increment,
username varchar(32),
birthday date comment '生日',
gmt_create datetime not null default current_timestamp comment '创建时间',
gmt_modified datetime not null default current_timestamp on update current_timestamp comment '更新时间'
) engine=innodb default charset=utf8mb4;
-- 插入数据,gmt_create自动填当前时间
insert into user_info(username, birthday) values('张三', '1998-01-01');
-- 更新数据,gmt_modified自动更新
update user_info set username = '张三三' where id = 1;
select * from user_info;
✅ 最佳实践:
- 优先用datetime,范围大,没有2038问题,还支持毫秒(datetime(6))
- 所有表必备gmt_create和gmt_modified字段
五、enum 与 set:单选多选底层原理与查询大全
很多人误以为「enum有数字、set没有数字」,这是典型误区。两者底层全部存数字,不存原始字符串,只是编码规则完全不同;而在查询用法上,二者的差异更大。
5.1 enum:单选枚举
enum就是数据库里的单选框,一个单元格只能选一个预设值。
5.1.1 基础语法
sql
create table votes (
username varchar(30),
gender enum('男','女') comment '性别(单选)',
hobby set('代码','羽毛球','乒乓球','足球','游泳') comment '爱好(多选)'
);
5.1.2 底层原理:顺序编号存储
enum底层按选项出现的顺序编号 存储,从1开始依次递增:
'男'→ 数字 1'女'→ 数字 2
所以既可以插字符串,也可以直接插数字:
sql
-- 插入字符串
insert into votes values('张飞','男','代码');
insert into votes values('刘备','女','代码');
-- 插入数字,等价对应选项
insert into votes values('孙权', 1, '代码,羽毛球'); -- 1 对应 '男'
5.1.3 查询用法:直接等值匹配即可
正因为enum是单选,每个单元格只有一个值,所以直接用 = 等值查询完全没问题:
sql
select * from votes where gender = '男';
select * from votes where gender = 1; -- 用数字查也可以,结果完全一致
运行结果:
+----------+--------+------------------------------------+
| username | gender | hobby |
+----------+--------+------------------------------------+
| 张飞 | 男 | 代码 |
| 孙权 | 男 | 代码 |
| 曹操 | 男 | 羽毛球 |
| 曹操 | 男 | 乒乓球,足球,游泳 |
| 刘表 | 男 | 代码,羽毛球 |
| 刘表 | 男 | 代码,羽毛球,乒乓球,足球,游泳 |
+----------+--------+------------------------------------+
17 rows in set (0.00 sec)
所有性别为男的记录全部命中,查询行为和普通字符串完全一致。
5.1.4 最大的坑:加选项要改表结构
业务一变要加选项,就得alter table,数据量大的时候会锁表,业务直接停摆。
5.2 set:多选集合
set是多选版的enum,一个单元格可以同时存多个预设值,逗号分隔。这也是查询特殊用法最多、坑最多的数据类型。
5.2.1 底层原理:比特位编码
set底层按二进制比特位编码,每个选项对应2的幂次方,和linux权限位原理一模一样:
| 选项 | 位序号 | 十进制值 | 二进制 |
|---|---|---|---|
| 代码 | 第0位 | 1 | 00001 |
| 羽毛球 | 第1位 | 2 | 00010 |
| 乒乓球 | 第2位 | 4 | 00100 |
| 足球 | 第3位 | 8 | 01000 |
| 游泳 | 第4位 | 16 | 10000 |
多选时,把对应位的数字相加 ,就是最终存储的值。比如同时选「代码+羽毛球」,底层存的就是 KaTeX parse error: Can't use function '\(' in math mode at position 1: \̲(̲1+2=3\)。
sql
-- 插入多个爱好,逗号分隔
insert into votes values('刘表','男','代码,羽毛球');
insert into votes values('曹操','男','乒乓球,足球,游泳'); -- 4+8+16=28
-- 也可以直接插数字
insert into votes values('全选测试','男', 31); -- 1+2+4+8+16=31,全部选中
查看底层存储的数字:
sql
select hobby, hobby + 0 as 底层数字 from votes;
输出:
+------------------------------------+--------------+
| hobby | 底层数字 |
+------------------------------------+--------------+
| 代码 | 1 |
| 羽毛球 | 2 |
| 代码,羽毛球 | 3 |
| 乒乓球,足球,游泳 | 28 |
| 代码,羽毛球,乒乓球,足球,游泳 | 31 |
+------------------------------------+--------------+
5.2.2 查询用法大全(7种)
按场景划分,set集合查询一共有7种常用用法,下面逐个拆解。
一、精确匹配:= 等值查询
语法
sql
where 字段名 = '选项1,选项2,...'
示例
sql
-- 查询爱好恰好只有「代码」的人(不能有其他爱好)
select * from votes where hobby = '代码';
说明
- 必须选项完全一致,不多不少才会命中
- set 底层按位存储,字符串顺序不影响结果:
'代码,羽毛球'和'羽毛球,代码'完全等价 - 也可以直接写底层数字:
where hobby = 3等价于where hobby = '代码,羽毛球'
适用场景 :查询"只选了这几个、没有其他选项"的精确场景
坑点:新手用来查"包含",会漏掉同时有其他爱好的记录,是最常见的用法错误
二、单值包含查询:find_in_set 标准写法
语法
sql
where find_in_set('待查选项', 字段名)
示例
sql
-- 查询所有爱好包含「代码」的人(不管还有没有其他爱好)
select * from votes where find_in_set('代码', hobby);
说明
- set 包含查询的标准写法,返回所有包含指定选项的行
- 找到返回选项位置(从1开始),没找到返回0
优点 :写法直观,符合业务直觉
缺点 :走不了普通索引,数据量大了全表扫描
适用场景:90% 的常规多选包含查询
三、单值包含查询:位运算 & 高性能写法
语法
sql
where 字段名 & 对应位值 != 0
示例
sql
-- 代码对应第0位,位值=1;按位与不为0就是包含该选项
select * from votes where hobby & 1 != 0;
说明
- 直接对底层比特位做「按位与」运算,有交集就命中
- 每个选项对应位值:第1个选项=1、第2个=2、第3个=4、第4个=8......依次2的幂
优点 :直接操作数字,性能比 find_in_set 更好
缺点 :需要记住选项顺序对应的位值,可读性差,业务代码不直观
适用场景:大数据量、性能要求高的底层场景
四、多选项同时包含(AND 关系)
需要所有指定选项同时都包含才命中。
写法1:多个 find_in_set 叠加
sql
select * from votes
where find_in_set('代码', hobby)
and find_in_set('羽毛球', hobby);
写法2:位运算掩码匹配
sql
-- 代码(1) + 羽毛球(2) = 3;按位与后等于3,说明两个都有
select * from votes where hobby & 3 = 3;
适用场景:标签交集查询,比如"既会java又会python"
五、多选项任选其一(OR 关系)
只要满足任意一个指定选项就命中。
写法1:多个 find_in_set 或逻辑
sql
select * from votes
where find_in_set('足球', hobby)
or find_in_set('游泳', hobby);
写法2:位运算或掩码
sql
-- 足球(8) + 游泳(16) = 24;按位与不为0,说明至少有一个
select * from votes where hobby & 24 != 0;
适用场景:标签并集查询,比如"会足球或者游泳其中一项"
六、空集合 / 非空集合判断
查询一个爱好都没选的(空set)
sql
select * from votes where hobby = '';
-- 等价底层数字判断
select * from votes where hobby + 0 = 0;
查询有爱好的(非空set)
sql
select * from votes where hobby != '';
说明:set 允许存 0 个选项,对应空字符串,底层数值为 0
七、不推荐:like 模糊查询
写法
sql
select * from votes where hobby like '%代码%';
为什么不推荐
- 存在误匹配风险:如果有选项叫「写代码」,也会被
%代码%命中 - 无法准确匹配选项边界,结果不可靠
- 同样走不了索引,性能也差
❌ 生产环境严禁用 like 查询 set 字段
5.2.3 用法汇总对比表
| 查询场景 | 实现方式 | 性能 | 可读性 | 推荐指数 | 备注 |
|---|---|---|---|---|---|
| 精确完全匹配 | = 等值 |
好 | 高 | ⭐⭐⭐ | 只能匹配完全一致的组合 |
| 包含单个选项 | find_in_set |
一般 | 高 | ⭐⭐⭐⭐⭐ | 最常用的标准写法 |
| 包含单个选项 | 位运算 & |
好 | 低 | ⭐⭐⭐⭐ | 性能更好,需要记位值 |
| 同时包含多个(AND) | 多个find_in_set | 一般 | 高 | ⭐⭐⭐⭐ | 直观易维护 |
| 同时包含多个(AND) | 位运算掩码 | 好 | 低 | ⭐⭐⭐ | 性能优先场景用 |
| 包含任意一个(OR) | 多个find_in_set | 一般 | 高 | ⭐⭐⭐⭐ | 直观易维护 |
| 包含任意一个(OR) | 位运算掩码 | 好 | 低 | ⭐⭐⭐ | 性能优先场景用 |
| 空/非空集合判断 | 空字符串比较 | 好 | 高 | ⭐⭐⭐⭐ | 直接比较即可 |
| 模糊包含 | like |
差 | 高 | ❌ | 有误匹配风险,禁止使用 |
5.2.4 业务开发建议
为什么业务开发不推荐用set:
- 新增/删除选项必须
alter table,大数据量表会锁表,影响线上业务 - 数字映射只存在数据库里,代码层看不到,可读性和可维护性差
- set的包含查询走不了普通索引,数据量大了性能下降明显
✅ 业务替代方案:
- 单选状态:用
tinyint存编码,代码里用枚举类对应,加选项只需改代码- 多选场景:用中间关联表实现多对多,扩展性好,查询能走索引
5.3 enum vs set 核心区别对比表
| 对比项 | enum(枚举,单选) | set(集合,多选) |
|---|---|---|
| 核心用途 | 只能选一个选项 | 可以选0个、1个、多个选项,逗号分隔 |
| 底层编码规则 | 顺序编号:1, 2, 3, 4, 5... | 比特位幂值:1, 2, 4, 8, 16... |
| 能否叠加组合 | ❌ 不能叠加,只能取其中一个编号 | ✅ 可以相加叠加,实现任意多选组合 |
| 最大成员数量 | 最多65535个选项 | 最多64个选项(受64bit限制) |
等值查询 = |
✅ 正常使用,精确匹配一个值 | ❌ 只能匹配完全相同的组合,不能用于包含查询 |
| 包含查询 | 不需要(本身就一个值) | ✅ 用 find_in_set 或 位运算 & |
| 索引支持 | 普通索引有效 | 普通索引对包含查询无效,位运算也走不了索引 |
| 底层存储大小 | 1~2字节 | 1~8字节 |
六、企业级使用规范与选型标准
6.1 数值类型规范
整型
推荐写法
sql
`status` tinyint not null default 0 comment '状态 0正常 1禁用',
`user_id` bigint not null auto_increment comment '主键id',
选型标准
| 类型 | 推荐使用场景 | 不推荐场景 |
|---|---|---|
| tinyint | 状态码、标记位、年龄、数量级≤100的计数 | 主键、自增id、大数量计数 |
| int | 普通业务计数、浏览量、非主键的中等数值 | 主键id(数据量大会溢出) |
| bigint | 表主键id、分布式id、超大数值、订单号 | 小状态、小计数(浪费空间) |
强制规范
- 主键id一律使用
bigint,禁止使用int做主键 - 状态、标记类字段优先用
tinyint,默认值设0 - 所有整型字段建议加
not null default 0,避免null带来的索引失效 - 禁止写
int(11)、tinyint(4)这种带显示宽度的写法
小数
推荐写法
sql
`price` decimal(10,2) not null default 0.00 comment '单价',
`amount` decimal(19,4) not null default 0.0000 comment '交易金额',
选型标准
| 类型 | 推荐使用场景 | 禁止场景 |
|---|---|---|
| decimal | 金额、价格、财务数据、高精度计算 | 无,高精度场景首选 |
| float | 科学计算、低精度近似值 | ❌ 绝对禁止存金额、价格 |
| double | 双精度科学计算 | ❌ 绝对禁止存金额、价格 |
6.2 字符串类型规范
char
- 只在长度完全固定的场景使用:手机号(11)、身份证(18)、md5(32)
- 长度严格按实际定,不要写
char(255)乱用
varchar
行业通用长度参考
| 长度建议 | 适用场景 |
|---|---|
| varchar(16) | 短编码、短名称、手机号备用 |
| varchar(32) | 用户名、昵称、短标题 |
| varchar(64) | 长名称、路径、分类名 |
| varchar(128) | 地址、链接、描述 |
| varchar(255) | 备注、说明、普通长文本 |
强制规范
- 按业务实际长度+20%冗余设定,禁止一律写varchar(255)
- 超过500字符,直接用
text类型 - 统一字符集用
utf8mb4,支持emoji和所有中文
6.3 日期类型规范
推荐写法
sql
`birthday` date default null comment '生日',
`gmt_create` datetime not null default current_timestamp comment '创建时间',
`gmt_modified` datetime not null default current_timestamp on update current_timestamp comment '更新时间',
强制规范
- 所有表必备两个字段:
gmt_create、gmt_modified - 类型统一用
datetime,禁止使用timestamp - 需要毫秒精度用
datetime(6)
6.4 一张决策表:直接照着选
| 数据内容 | 首选类型 | 长度示例 | 备注 |
|---|---|---|---|
| 表主键id | bigint | - | 自增主键,统一bigint |
| 状态/标记/性别 | tinyint | - | 0、1、2编码,代码里做枚举映射 |
| 年龄/小计数 | tinyint | - | 不超过100的数值 |
| 普通计数/浏览量 | int | - | 百万级以内 |
| 金额/价格 | decimal | decimal(10,2) | 财务高精度用decimal(19,4) |
| 手机号 | char | char(11) | 长度固定 |
| 身份证号 | char | char(18) | 长度固定 |
| 用户名/昵称 | varchar | varchar(32/64) | 按实际长度定 |
| 地址/备注 | varchar | varchar(128/255) | 不太长的文本 |
| 文章正文/长描述 | mediumtext | - | 拆分到副表 |
| 生日/日期 | date | - | 只有年月日 |
| 创建/更新时间 | datetime | - | 自动填充、自动更新 |
七、综合实战:一张标准业务表完整建表语句
sql
create table `sys_user` (
`id` bigint not null auto_increment comment '主键id',
`username` varchar(32) not null default '' comment '用户名',
`password` char(32) not null default '' comment 'md5密码',
`mobile` char(11) not null default '' comment '手机号',
`gender` tinyint not null default 0 comment '性别 0未知 1男 2女',
`age` tinyint not null default 0 comment '年龄',
`balance` decimal(10,2) not null default 0.00 comment '账户余额',
`status` tinyint not null default 1 comment '状态 0禁用 1正常',
`birthday` date default null comment '生日',
`remark` varchar(255) not null default '' comment '备注',
`gmt_create` datetime not null default current_timestamp comment '创建时间',
`gmt_modified` datetime not null default current_timestamp on update current_timestamp comment '更新时间',
primary key (`id`),
unique key `uk_username` (`username`),
key `idx_mobile` (`mobile`),
key `idx_status` (`status`)
) engine=innodb default charset=utf8mb4 comment '系统用户表';
八、避坑口诀与建表checklist
选型口诀
- 整数选小不选大,主键直接bigint
- 括号数字是摆设,显示宽度别当真
- 金额必须decimal,浮点精度会翻车
- 比特位省空间,业务开发别瞎沾
- 定长char变长varchar,大文本出门找text
- 日期优先datetime,timestamp小心2038
- 枚举集合虽好用,扩展起来很头疼
- 字段尽量not null,默认值要安排上
建表合规checklist
建表完对照检查,全中就是标准建表:
- ✅ 主键全部用
bigint - ✅ 状态、标记全部用
tinyint,默认0,not null - ✅ 金额全部用
decimal,没有float/double - ✅ 固定长度字符串用
char,变长用varchar - ✅ 字符串长度按需设定,不是一律255
- ✅ 时间字段全部用
datetime,没有timestamp - ✅ 有gmt_create和gmt_modified,自动填充更新
- ✅ 没有enum、set、bit等难维护类型
- ✅ 字符集统一utf8mb4
- ✅ 大文本拆分到副表,主表没有大text字段
写在最后
数据类型是mysql建表的基石,选对了能让你的数据库又快又稳,选错了后期就是无穷无尽的坑。
很多人觉得"不就是个类型吗,差不多就行",等数据量上来了、业务扩展了,才发现当初随便选的类型全是债。
希望这篇文章能帮你绕开新手常踩的坑,建出规范又高效的表。