拒绝 30 个字段!用 MySQL 位运算 (Bitwise) 一个整数存下所有状态

你正在设计一张“用户表”,产品经理一口气提了 N 个开关需求:
- 是否验证过邮箱?
- 是否验证过手机?
- 是否绑定了微信?
- 是否是 VIP?
- 是否被封禁?
- 是否开启了推送通知?
- … (此处省略 20 个 flag)
噩梦设计:
你在表中加了 20 多个 TINYINT 字段:email_verified, phone_verified, wx_bound…
每次新增一个状态,你都要 ALTER TABLE,还要改 Java 代码里的 Entity,改前端的接口。表变得越来越宽,维护极其痛苦。
极客设计:
只加 一个 BIGINT 字段,名叫 status_mask。利用二进制位,一行代码搞定所有状态的存储和查询。
1. 核心原理:二进制的魔法
计算机底层只有 0 和 1。我们可以把一个整数看作一串二进制开关。
一个 INT (32位) 可以存 32 种状态,一个 BIGINT (64位) 可以存 64 种状态。
状态定义 (2 的 N 次幂):
我们给每个状态分配一个独一无二的位值:
- 1 (Binary:
...0001): 邮箱已验证 - 2 (Binary:
...0010): 手机已验证 - 4 (Binary:
...0100): 绑定微信 - 8 (Binary:
...1000): 是 VIP - 16 (Binary:
...10000): 账号被封禁
组合状态:
如果一个用户既验证了邮箱 (1),又是 VIP (8),他的 status_mask 值就是 (Binary: ...1001)。
2. SQL 实战:增删改查的位运算技巧
在 MySQL 中,我们直接使用 & (与), | (或), ^ (异或), ~ (取反) 符号。
A. 开启某个状态 (OR 运算 |)
逻辑:不管原来是什么,把对应位置为 1。
-- 用户验证了手机 (值为 2)
-- 原值如果是 9 (1001),变成 9 | 2 = 11 (1011)
UPDATE users
SET status_mask = status_mask | 2
WHERE id = 1001;
B. 关闭某个状态 (AND NOT 运算 & ~)
逻辑:不管原来是什么,把对应位置为 0。
-- 用户 VIP 过期了,取消 VIP (值为 8)
-- status_mask & (~8)
UPDATE users
SET status_mask = status_mask & ~8
WHERE id = 1001;
C. 查询是否具备某个状态 (AND 运算 &)
这是最高频的操作。
-- 查询所有 "是 VIP" 的用户
-- 如果结果 > 0 (或等于 8),说明该位是 1
SELECT * FROM users
WHERE (status_mask & 8) > 0;
-- 查询 "既验证了邮箱(1) 又绑定了微信(4)" 的用户
-- 1 + 4 = 5
SELECT * FROM users
WHERE (status_mask & 5) = 5;
3. 三大实战场景
场景一:用户偏好设置 (User Preferences)
用户在设置页有几十个开关:
- 接收邮件通知 (1)
- 接收短信通知 (2)
- 接收 App 推送 (4)
- 允许被搜索发现 (8)
- …
优势: 产品经理今天要加一个“接收营销电话”,你只需要在代码里定义一个新的常量 CONST_MARKETING = 32,数据库完全不用动!
场景二:权限控制系统 (RBAC)
这是 Linux 文件权限的经典复刻(r=4, w=2, x=1)。
在简单的权限系统中,我们可以给每个原子权限定义一个位:
- CREATE (1)
- READ (2)
- UPDATE (4)
- DELETE (8)
查询: 查所有拥有“删除权限”的管理员。SELECT * FROM admins WHERE (permissions & 8) > 0;
场景三:商品/内容的多维标签
商品可能有各种布尔属性:
- 是否热销 (1)
- 是否新品 (2)
- 是否包邮 (4)
- 是否参与双11 (8)
如果用位存储,前端传参只需要传一个整数,后端判断逻辑非常统一。
4. 避坑指南与局限性
虽然位运算很爽,但它有明显的短板,使用前必须评估:
- 索引失效 (Index Inefficiency):
WHERE (status & 8) > 0这种写法,通常无法利用 B+ 树索引。
- 后果: 如果用户表有几千万行,查 VIP 用户会导致全表扫描。
- 解法:
- MySQL 8.0 可以创建 函数索引 (Functional Index):
CREATE INDEX idx_is_vip ON users ((status_mask & 8)); - 或者只用于数据量小、或必定会带上
user_id主键查询的场景。
- 可读性差:
数据库里存个13,没人知道它是啥意思(1+4+8)。
- 解法: 必须在代码文档或注释中严格维护位值映射表。
- 有限性:
BIGINT最多 64 位。如果你的状态超过 64 个,就得开第二个字段status_mask_2。
5. 总结
位运算 (Bitwise) 是用“计算换空间”和“维护灵活性”的经典案例。
- 什么时候用? 状态多、并发修改少、不需要对某个特定状态进行高频范围查询(除非加函数索引)的场景。
- 拒绝: 每次新增一个 Boolean 状态就去改表结构。
- 拥抱:
status_mask | FLAG的优雅。
更多推荐




所有评论(0)