SET 类型
导学
ENUM 只能选一个,但现实中很多场景需要"多选"——比如用户的兴趣标签、商品的属性特征、权限的叠加组合。SET 类型让你在一个字段中存储多个预定义选项的组合,内部用位运算存储,查询时支持精确匹配和包含判断。
定义
SET:集合类型,用于存储从一个预定义字符串列表中选择的零个或多个值的组合。内部以位图(bitmask)存储,每个成员对应一个比特位。
核心语法
CREATE TABLE 表名 (
列名 SET('值1', '值2', '值3', ...) [NOT NULL] [DEFAULT '']
);
| 特性 | 说明 |
|---|---|
| 最大成员数 | 64 个 |
| 存储空间 | 1~8 字节(成员数决定,按位存储) |
| 成员对应位 | 第 1 个成员 = 第 0 位(值为 1),第 2 个成员 = 第 1 位(值为 2),以此类推 |
| 空值 | '' 表示空集合(所有位为 0) |
| 重复值 | 自动去重('A,A,B' 存储为 'A,B') |
演示数据准备
DROP TABLE IF EXISTS employees;
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept VARCHAR(20),
score DECIMAL(5,2),
skills SET('Java', 'Python', 'SQL', 'Linux') DEFAULT ''
);
INSERT INTO employees (emp_name, dept, score, skills) VALUES
('大翔', '技术部', 100, 'Java,Python,SQL,Linux'),
('白歌', '技术部', NULL, 'Python,SQL');
当前 employees 表中的完整数据如下:
| emp_id | emp_name | dept | score | skills |
|---|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 | Java,Python,SQL,Linux |
| 2 | 白歌 | 技术部 | NULL | Python,SQL |
SQL 示例
场景一:插入 SET 值(多选组合)
当前数据状态:见上文 employees 表完整数据。
执行语句:
INSERT INTO employees (emp_name, dept, score, skills) VALUES ('孔蓝', '产品部', 88, 'SQL,Linux');
操作后结果:
SELECT * FROM employees;
| emp_id | emp_name | dept | score | skills |
|---|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 | Java,Python,SQL,Linux |
| 2 | 白歌 | 技术部 | NULL | Python,SQL |
| 3 | 孔蓝 | 产品部 | 88 | SQL,Linux |
结果解读:'SQL,Linux' 是 SET 定义中的合法组合,插入成功。SET 值用逗号分隔多个成员,顺序不影响存储结果(内部按定义顺序排序显示)。
场景二:插入非法成员的陷阱
当前数据状态:见上文 employees 表完整数据。
执行语句:
-- 严格模式下报错
INSERT INTO employees (emp_name, dept, score, skills) VALUES ('赵鸣', '运营部', 76, 'Java,Go');
操作后结果:
在 STRICT_TRANS_TABLES 模式下:
ERROR 1265 (01000): Data truncated for column 'skills'
在非严格模式下:
SELECT emp_name, skills FROM employees WHERE emp_name = '赵鸣';
| emp_name | skills |
|---|---|
| 赵鸣 | Java |
结果解读:'Go' 不在 SET('Java', 'Python', 'SQL', 'Linux') 列表中。严格模式下报错;非严格模式下会静默截断非法成员,只保留合法成员 'Java'。这是 SET 的常见陷阱——数据被部分截断而不报错。
场景三:SET 的精确匹配查询(=)
当前数据状态:见上文 employees 表完整数据。
执行语句:
SELECT emp_name, skills FROM employees WHERE skills = 'Python,SQL';
操作后结果:
| emp_name | skills |
|---|---|
| 白歌 | Python,SQL |
结果解读:skills = 'Python,SQL' 是精确匹配,要求 skills 的值恰好等于 'Python,SQL'。大翔的 skills = 'Java,Python,SQL,Linux' 虽然包含 'Python,SQL',但不等于它,因此不匹配。
注意:
SET的精确匹配要求成员顺序与定义顺序一致(内部存储时会自动按定义顺序排序),但查询时写'SQL,Python'和'Python,SQL'是等价的,因为内部存储顺序固定。
场景四:SET 的包含查询(FIND_IN_SET)
当前数据状态:见上文 employees 表完整数据。
执行语句:
-- 查询包含 Python 技能的人
SELECT emp_name, skills FROM employees WHERE FIND_IN_SET('Python', skills) > 0;
操作后结果:
| emp_name | skills |
|---|---|
| 大翔 | Java,Python,SQL,Linux |
| 白歌 | Python,SQL |
结果解读:FIND_IN_SET('Python', skills) 返回 'Python' 在 skills 集合中的位置(从 1 开始),如果不包含则返回 0。因此 > 0 表示"包含"。这是 SET 类型最常用的查询方式——判断某个成员是否在集合中。
再看一个用位运算的等价写法:
-- 使用位运算判断(更底层,性能更好)
SELECT emp_name, skills FROM employees WHERE skills & (1 << 1) > 0;
'Python' 是 SET 定义中的第 2 个成员,对应第 1 位(1 << 1 = 2)。skills & 2 > 0 表示该位被置 1,即包含 'Python'。
场景五:SET 的位运算查询(多条件组合)
当前数据状态:见上文 employees 表完整数据。
执行语句:
-- 查询同时包含 Java 和 Linux 的人
SELECT emp_name, skills FROM employees
WHERE skills = 'Java,Linux' -- 精确匹配,只包含这两个
OR (FIND_IN_SET('Java', skills) > 0 AND FIND_IN_SET('Linux', skills) > 0);
操作后结果:
| emp_name | skills |
|---|---|
| 大翔 | Java,Python,SQL,Linux |
结果解读:
skills = 'Java,Linux'精确匹配只包含这两个技能的人,结果为空(大翔还包含 Python 和 SQL)FIND_IN_SET('Java', skills) > 0 AND FIND_IN_SET('Linux', skills) > 0判断同时包含 Java 和 Linux,匹配大翔
这是 SET 查询的核心认知:精确匹配用 =,包含判断用 FIND_IN_SET(),多条件组合用 AND。
场景六:SET 的添加和删除成员
当前数据状态:见上文 employees 表完整数据。
执行语句:
-- 给白歌添加 Linux 技能
UPDATE employees SET skills = skills | (1 << 3) WHERE emp_id = 2;
-- 等价于:UPDATE employees SET skills = skills + 'Linux' WHERE emp_id = 2;
-- 从大翔移除 Java 技能
UPDATE employees SET skills = skills & ~(1 << 0) WHERE emp_id = 1;
-- 等价于:UPDATE employees SET skills = skills - 'Java' WHERE emp_id = 1;
操作后结果:
SELECT emp_name, skills FROM employees;
| emp_name | skills |
|---|---|
| 大翔 | Python,SQL,Linux |
| 白歌 | Python,SQL,Linux |
结果解读:
skills | (1 << 3)用位或运算添加成员('Linux'是第 4 个成员,对应第 3 位)skills & ~(1 << 0)用位与运算清除成员('Java'是第 1 个成员,对应第 0 位)- MySQL 也支持更直观的字符串运算:
skills + 'Linux'添加成员,skills - 'Java'移除成员
常见误区
| 误区 | 正解 |
|---|---|
"SET 和 ENUM 一样只能选一个" | 不是。SET 可以多选,ENUM 只能单选。 |
"SET 查询用 = 可以匹配包含关系" | 不是。= 是精确匹配,包含关系必须用 FIND_IN_SET()。 |
"SET 存储的是逗号分隔的字符串" | 不是。内部是位图(bitmask),1~8 字节,比 VARCHAR 节省空间。 |
"SET 成员顺序影响存储" | 不影响。内部按定义顺序自动排序显示,插入顺序无关。 |
"SET 可以存储 100 个成员" | 不能。最多 64 个成员。 |
"SET 比用关联表存储多对多关系更好" | 不一定。SET 适合选项少且固定的场景;选项多或动态变化时,用关联表更灵活。 |
面试考点
Q:SET 和 ENUM 的区别?
ENUM单选,内部 1~2 字节;SET多选,内部 1~8 字节按位存储。ENUM像单选按钮,SET像复选框。两者都限制在预定义列表中,但SET支持零个、一个或多个值的组合。
Q:SET 如何查询"包含某个成员"?
用
FIND_IN_SET('成员', 列名) > 0。不能用=,因为=是精确匹配。也可以用位运算列名 & (1 << n) > 0,其中 n 是成员在定义中的位置减 1。
Q:SET 最多多少个成员?存储空间怎么算?
最多 64 个成员。存储空间按成员数向上取整到字节:1~8 成员 = 1 字节,9~16 成员 = 2 字节,...,57~64 成员 = 8 字节。每个成员对应一个比特位。
Q:为什么生产环境很少用 SET?
- 查询语法特殊(
FIND_IN_SET),不如关联表直观;2. 成员列表修改困难(ALTER TABLE);3. 无法存储额外属性(如每个技能的熟练度);4. 超过 64 个成员不支持。现代设计更倾向于用关联表(用户-技能中间表)存储多对多关系。
Q:SET 的位运算怎么理解?
SET('A','B','C','D')中,A=第0位(1),B=第1位(2),C=第2位(4),D=第3位(8)。skills & 2 > 0判断是否包含 B;skills | 4添加 C;skills & ~1移除 A。
小结
SET用于存储预定义列表中的多个值的组合,内部以位图存储,节省空间- 查询包含关系用
FIND_IN_SET(),不能用=(=是精确匹配) - 支持位运算添加/删除成员,也支持字符串运算
+/- - 最多 64 个成员,修改成员列表需要
ALTER TABLE - 适合选项少且固定的多选场景(如兴趣标签、权限组合),复杂场景建议用关联表
下一章引子:ENUM 和 SET 都是基于字符串列表的类型,但如果需要存储的是纯二进制标志位——比如 8 个开关状态的组合——BIT 类型是更底层的选择。