MySQL数据库:数据类型

文章目录

  • 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 核心区别对比表)
    • 六、企业级使用规范与选型标准
      • [6.1 数值类型规范](#6.1 数值类型规范)
      • [6.2 字符串类型规范](#6.2 字符串类型规范)
      • [6.3 日期类型规范](#6.3 日期类型规范)
      • [6.4 一张决策表:直接照着选](#6.4 一张决策表:直接照着选)
    • 七、综合实战:一张标准业务表完整建表语句
    • 八、避坑口诀与建表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。

原因很简单:

  1. 两个unsigned数做减法,可能出现意想不到的溢出结果(比如1-2会变成超大数)
  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 超大文本,极少使用

使用注意:

  1. text/blob不能设默认值,不能加普通索引(只能加前缀索引)
  2. 大字段建议拆分到副表,避免影响主表查询性能
  3. 图片、文件不建议存数据库,放对象存储

四、日期时间类型:小心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:

  1. 新增/删除选项必须alter table,大数据量表会锁表,影响线上业务
  2. 数字映射只存在数据库里,代码层看不到,可读性和可维护性差
  3. 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、超大数值、订单号 小状态、小计数(浪费空间)

强制规范

  1. 主键id一律使用bigint,禁止使用int做主键
  2. 状态、标记类字段优先用tinyint,默认值设0
  3. 所有整型字段建议加not null default 0,避免null带来的索引失效
  4. 禁止写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) 备注、说明、普通长文本

强制规范

  1. 按业务实际长度+20%冗余设定,禁止一律写varchar(255)
  2. 超过500字符,直接用text类型
  3. 统一字符集用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 '更新时间',

强制规范

  1. 所有表必备两个字段:gmt_create、gmt_modified
  2. 类型统一用datetime,禁止使用timestamp
  3. 需要毫秒精度用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

选型口诀

  1. 整数选小不选大,主键直接bigint
  2. 括号数字是摆设,显示宽度别当真
  3. 金额必须decimal,浮点精度会翻车
  4. 比特位省空间,业务开发别瞎沾
  5. 定长char变长varchar,大文本出门找text
  6. 日期优先datetime,timestamp小心2038
  7. 枚举集合虽好用,扩展起来很头疼
  8. 字段尽量not null,默认值要安排上

建表合规checklist

建表完对照检查,全中就是标准建表:

  1. ✅ 主键全部用bigint
  2. ✅ 状态、标记全部用tinyint,默认0,not null
  3. ✅ 金额全部用decimal,没有float/double
  4. ✅ 固定长度字符串用char,变长用varchar
  5. ✅ 字符串长度按需设定,不是一律255
  6. ✅ 时间字段全部用datetime,没有timestamp
  7. ✅ 有gmt_create和gmt_modified,自动填充更新
  8. ✅ 没有enum、set、bit等难维护类型
  9. ✅ 字符集统一utf8mb4
  10. ✅ 大文本拆分到副表,主表没有大text字段

写在最后

数据类型是mysql建表的基石,选对了能让你的数据库又快又稳,选错了后期就是无穷无尽的坑。

很多人觉得"不就是个类型吗,差不多就行",等数据量上来了、业务扩展了,才发现当初随便选的类型全是债。

希望这篇文章能帮你绕开新手常踩的坑,建出规范又高效的表。

相关推荐
天空之城--1 小时前
Android一周动态:Android 18首次官宣、Compose Material3 1.4转正(5趋势+5资讯)
android·人工智能·flutter·架构·android jetpack
恋猫de小郭1 小时前
Meta 分享怎么用 AI 迁移 Compose 项目不烧心
android·前端·flutter
梦想画家1 小时前
SQLMesh 模型类型详解(三):MANAGED——把数据刷新外包给引擎
数据库·数据开发·sqlmesh
DingYuan1011 小时前
Django模板继承
数据库·sqlite
Mortalbreeze1 小时前
MySQL 基础篇(四):表的约束
linux·服务器·数据库·mysql
阳光九叶草LXGZXJ1 小时前
达梦数据库-学习-71-存过执行过程中重建存过影响验证
linux·运维·数据库·sql·学习
傲世仙尊1 小时前
MySQL上手-库与表的操作编码校验集与备份还原
数据库·mysql
纪念 2291 小时前
C++ string(一)
android·开发语言·c++
志栋智能3 小时前
安全超自动化如何支持快速安全扩容?
运维·服务器·数据库·架构·自动化