--- tags: [MySQL, 数据类型, INT, VARCHAR, JSON] create time: 2026-05-16 00:30 --- # MySQL 数据类型全景 ## 概述 MySQL 的数据类型可按存储的内容分为六大类。**选型的黄金法则**:用满足业务需求的最小类型,节省空间、提升索引效率与查询性能。 | 大类 | 包含类型 | 核心考量 | |------|---------|---------| | **整数** | TINYINT / SMALLINT / MEDIUMINT / INT / BIGINT | 够用就行,别上来就 BIGINT | | **浮点/定点** | FLOAT / DOUBLE / DECIMAL | **金额永远用 DECIMAL** | | **字符串** | CHAR / VARCHAR / TEXT 系列 | 定长选 CHAR,不定长选 VARCHAR | | **日期时间** | DATE / TIME / DATETIME / TIMESTAMP / YEAR | 跨时区用 TIMESTAMP,否则 DATETIME | | **JSON** | JSON | 灵活扩展字段,搭配 Generated Column 做索引 | | **枚举/集合** | ENUM / SET | 值域固定且极少变才考虑 | | **二进制** | BINARY / VARBINARY / BLOB 系列 | 存文件用对象存储,数据库里只存引用 | > [!QUESTION] 为什么总强调"用小的就行"? > 想象一张百万行的订单表,如果主键用了 `BIGINT(8 bytes)` 而非 `INT(4 bytes)`,仅此一列就多花 4MB。这还没算上二级索引——InnoDB 的二级索引叶子节点会完整存储主键值,每个二级索引同样多花 4MB。表越大,连锁放大效应越惊人。 ### 整数类型 ```mermaid graph TD INT_TYPES["整数类型家族"] INT_TYPES --> TINY["TINYINT
1 byte | ±128"] INT_TYPES --> SMALL["SMALLINT
2 bytes | ±32K"] INT_TYPES --> MED["MEDIUMINT
3 bytes | ±8M"] INT_TYPES --> INTN["INT
4 bytes | ±21亿"] INT_TYPES --> BIG["BIGINT
8 bytes | ±9.2×10^18"] style INT_TYPES fill:#5F2799,color:#fff style TINY fill:#C44569,color:#fff style SMALL fill:#C44569,color:#fff style MED fill:#C44569,color:#fff style INTN fill:#C44569,color:#fff style BIG fill:#FF6B6B,color:#000 ``` | 类型 | 有符号范围 | 无符号范围 | 存储 | 典型用途 | |------|-----------|-----------|------|---------| | **TINYINT** | -128 ~ 127 | 0 ~ 255 | 1 byte | 状态标识、布尔标志、年龄 | | **SMALLINT** | -32K ~ 32K | 0 ~ 65K | 2 bytes | 短编码 ID | | **MEDIUMINT** | -8M ~ 8M | 0 ~ 16M | 3 bytes | 中等范围计数 | | **INT** | ±21 亿 | 0 ~ 42 亿 | 4 bytes | 常规自增主键、外键 | | **BIGINT** | ±9.2×10¹⁸ | — | 8 bytes | Snowflake ID、金额计算 | > [!TIP] 能用小的就不用大的 > `TINYINT UNSIGNED` 能存 255,够用就别用 `INT`。每列差 3 字节,百万行就是 3MB 额外开销,还会导致索引变厚、缓存命中率下降。 ### 浮点与定点 | 类型 | 精度 | 存储 | 适用场景 | |------|------|------|---------| | **FLOAT** | 单精度 7 位 | 4 bytes | 科学计算、不需要精确的场景 | | **DOUBLE** | 双精度 15 位 | 8 bytes | 高精度科学计算 | | **DECIMAL(M,D)** | 精确定点 M-D 位整数 + D 位小数 | 可变(约每 9 位数字 4 字节) | **金额计算!绝对不要用浮点数存钱** | ```sql -- ❌ 错误示范:浮点数误差累积 SELECT 0.1 + 0.2; -- 结果可能是 0.30000000000000004 -- ✅ 正确做法:DECIMAL CREATE TABLE orders ( amount DECIMAL(10, 2) NOT NULL -- 最大 99999999.99 ); INSERT INTO orders VALUES (19.99 + 29.99); -- 精确等于 49.98 ``` > [!QUESTION] 为什么不能用 FLOAT/DOUBLE 存金额? > IEEE 754 浮点数无法精确表示 0.1 这样的十进制小数。在财务场景中,微小的舍入误差经过多次加减后会累积成显著差异。DECIMAL 以字符串形式存储每一位数字,保证运算精确。 ## 字符串类型 | 类型 | 存储规则 | 最大长度 | 特点 | |------|---------|---------|------| | **CHAR(n)** | 固定长度,不足空格填充 | 0~255 | 适合长度固定的数据(MD5 hash、状态码) | | **VARCHAR(n)** | 变长,前缀记录实际长度 | 0~65535(受行大小限制) | 最常用的字符串类型 | | **TINYTEXT** | 1 字节长度前缀 | 255 | 极短文本 | | **TEXT** | 2 字节前缀 | 65535 | 文章摘要、评论 | | **MEDIUMTEXT** | 3 字节前缀 | 16MB | 长文章、富文本 | | **LONGTEXT** | 4 字节前缀 | 4GB | 超大文本(日志、JSON 文档) | ### CHAR vs VARCHAR 的选择 ```sql -- ✅ 适合 CHAR:定长数据 CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, status TINYINT DEFAULT 1, country_code CHAR(3) NOT NULL, -- ISO 3166-1 alpha-3 phone_prefix CHAR(4), -- +86, +1, +44... email VARCHAR(255) UNIQUE -- 不定长,用 VARCHAR ); -- 为什么 gender 有时用 CHAR(1) 而非 TINYINT? -- CHAR(1) 语义更明确,但本质上两者存储相同。 -- 关键是保持一致性——团队规范比个人偏好更重要。 ``` > [!NOTE] VARCHAR 的长度陷阱 > MySQL 行大小上限 65535 字节,但这不只是所有 VARCHAR 加起来的大小。还要考虑: > - 每列的 1~2 字节长度前缀 > - NULL 位图(允许 NULL 的列) > - 实际存储使用 **utf8mb4** 的话,每个字符最多占 4 字节 > > 所以 `VARCHAR(255)` 在 utf8mb4 下最大占用 255 × 4 + 2 ≈ 1022 字节。 ## 日期和时间类型 | 类型 | 格式 | 存储 | 时区感知 | |------|------|------|---------| | **DATE** | `YYYY-MM-DD` | 3 bytes | 无 | | **TIME** | `HH:MM:SS` | 3 bytes | 无(时间段) | | **DATETIME** | `YYYY-MM-DD HH:MM:SS` | 8 bytes | 无(存储原始值) | | **TIMESTAMP** | `YYYY-MM-DD HH:MM:SS` | 4 bytes | **有**(UTC 存储,显示时转换) | | **YEAR** | `YYYY` | 1 byte | — | ```sql -- TIMESTAMP 的时区自动转换特性 CREATE TABLE events ( id BIGINT PRIMARY KEY, name VARCHAR(100), event_time DATETIME, -- 存入什么就读出什么 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 自动填当前 UTC 时间 ); INSERT INTO events (name, event_time) VALUES ('Meeting', '2026-05-16 14:00:00'); -- 在上海时区 (UTC+8) 显示:14:00:00 -- 在纽约时区 (UTC-4) 显示:14:00:00(DATETIME 不变) -- 但如果用 TIMESTAMP,它会转换成纽约本地时间 02:00:00 ``` > [!WARNING] TIMESTAMP 有保质期 > `TIMESTAMP` 的范围是 `1970-01-01 00:00:01` 到 `2038-01-19 03:14:07`(32-bit 上限)。如果你的系统需要支持 2038 年之后的数据,请用 `DATETIME`。 ## JSON 类型 MySQL 5.7+ 引入原生 JSON 类型,支持部分查询和索引能力。 ```sql CREATE TABLE employees ( id BIGINT PRIMARY KEY, name VARCHAR(100), attributes JSON -- 灵活扩展字段 ); INSERT INTO employees VALUES (1, 'Alice', '{"dept": "Engineering", "skills": ["Go", "Python"], "level": 5}'), (2, 'Bob', '{"dept": "Marketing", "skills": ["SEO", "Content"], "level": 3}'); -- JSON 路径查询 SELECT name, attributes->>'$.dept' AS department FROM employees WHERE attributes->>'$.level' >= 4; -- JSON 数组包含判断 SELECT name FROM employees WHERE JSON_CONTAINS(attributes->'$[*]', '"Go"'); -- 生成虚拟列 + 索引(最佳实践) ALTER TABLE employees ADD COLUMN dept VARCHAR(50) GENERATED ALWAYS AS (attributes->>'$.dept') VIRTUAL, ADD INDEX idx_dept (dept); ``` ```mermaid flowchart LR A["JSON Column
原始存储"] --> B["JSON Document"] B --> C["Scalar Values"] B --> D["Arrays"] B --> E["Nested Objects"] C --> F["Generated Column"] D --> F E --> F F --> G["Index
Virtual / Stored"] style A fill:#00B6BC,color:#fff style F fill:#FF9F43,color:#000 style G fill:#C44569,color:#fff ``` > [!TIP] JSON vs 规范化表设计 > - **适合 JSON**:配置项、标签集合、表单动态字段、低频更新的属性 > - **不适合 JSON**:需要 JOIN 关联、频繁条件过滤、强一致性约束的字段 > - 关键技巧:用 **Generated Column + Index** 让 JSON 字段的筛选走索引 ## 枚举与集合 ```sql -- ENUM: 限定可选值列表 CREATE TABLE tasks ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200), status ENUM('todo', 'in_progress', 'done', 'cancelled') DEFAULT 'todo', priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium' ); -- SET: 多选值(逗号分隔存储) CREATE TABLE tags ( id BIGINT PRIMARY KEY, name VARCHAR(100), categories SET('tech', 'business', 'lifestyle', 'health') ); INSERT INTO tags VALUES (1, 'Go Tips', 'tech,business'); ``` ### ENUM vs SET 对比 | 特性 | ENUM | SET | |------|------|-----| | **语义** | 单选:从列表中取一个值 | 多选:从列表中取零个或多个值 | | **内部存储** | 整数索引(1 byte 或 2 bytes) | 位图(最多 64 个成员占 8 bytes) | | **排序规则** | 按索引顺序,非字母序 | 按位数值排序 | | **典型场景** | 工单状态、优先级 | 标签分类、权限角色 | ### ENUM 还是 TINYINT?这是永恒争论 ```sql -- 方案 A:ENUM — 数据库层校验 + 省空间 status ENUM('open', 'closed') NOT NULL -- 1 byte -- 方案 B:TINYINT — 代码层校验 + 灵活 status TINYINT UNSIGNED NOT NULL -- 1 byte -- 应用层保证只写入 0/1/2/3 ``` | 维度 | 选 ENUM | 选 TINYINT | |------|---------|-----------| | **校验** | 数据库自动拒绝非法值 | 需应用层保证 | | **调试** | 查出是数字 2,还要查 schema 才知道含义 | 直接读出原始数字,一目了然 | | **修改成本** | 增删值需 `ALTER TABLE`(大表很贵) | 随时在应用层加常量枚举 | | **代码可追溯性** | IDE 找不到所有使用处 | `grep` 就能定位 | > [!WARNING] ENUM 的反模式警告 > - **不要用 ENUM 存用户可见的文案**——后台查出来是 `2`,前端还得映射回去 > - **不要把 ENUM 当文档用**——队友看不懂 `status = 3` 是什么意思 > - **推荐方案**:中小项目用 TINYINT + Go `const` / Python `IntEnum` 在代码里维护枚举定义;只有在值域极稳定且不需要跨语言共享时再用原生 ENUM > [!NOTE] ENUM 的本质 > ENUM 在内部存储为整数索引(1, 2, 3...),而不是字符串。这意味着: > - ENUM 的排序是按索引而非字母顺序 > - 插入不在列表中的值会导致错误(或空字符串,取决于 sql_mode) > - 修改 ENUM 列表顺序会影响已有数据的解释——**谨慎维护** ## 二进制类型 | 类型 | 说明 | 典型用途 | |------|------|---------| | **BINARY(n)** | 定长二进制 | 哈希值(SHA256 = 32 bytes)| | **VARBINARY(n)** | 变长二进制 | 短二进制数据 | | **TINYBLOB** | ≤ 255 bytes | — | | **BLOB** | ≤ 65KB | 缩略图、序列化对象 | | **MEDIUMBLOB** | ≤ 16MB | 文件附件 | | **LONGBLOB** | ≤ 4GB | 大文件存储 | ```sql -- 存储 SHA-256 hash 的推荐方式 CREATE TABLE file_metadata ( id BIGINT PRIMARY KEY, filename VARCHAR(500), sha256_hash BINARY(32) NOT NULL, -- CHAR(64) HEX 也可以,但 BINARY 省一半 UNIQUE KEY uk_sha256 (sha256_hash) ); ``` ### BINARY vs VARBINARY — 定长与变长的选择 ```sql -- BINARY(32):始终占 32 bytes,不足补 0x00 INSERT INTO t VALUES (X'61'); -- 实际存储: 61 00 00 ... 00(32 bytes) SELECT HEX(col) FROM t; -- 输出: 610000...(永远 64 个十六进制字符) -- VARBINARY(32):只存真实长度 + 1 byte 长度前缀 INSERT INTO t VALUES (X'61'); -- 实际存储: 61 01(2 bytes,01 表示长度) SELECT HEX(col) FROM t; -- 输出: 61(只有 2 个十六进制字符) ``` | 维度 | BINARY(n) | VARBINARY(n) | |------|-----------|-------------| | **存储** | 固定 n 字节,右侧用 `0x00` 补齐 | 实际长度 + 1~2 bytes 前缀 | | **比较规则** | 补齐 0x00 后再逐字节比较 | 按实际长度比较 | | **适用场景** | 哈希值、加密密钥等定长数据 | 短二进制流、序列化片段 | > [!TIP] BINARY vs CHAR 的对称性 > 理解 `BINARY` 就理解了 `CHAR`——它们是对称的定长类型。区别仅在于:**CHAR 补空格,BINARY 补零**。同理 `VARBINARY` 对标 `VARCHAR`。类比记忆比死记硬背更可靠。 > [!WARNING] 大文件不要存在数据库里 > BLOB 系列看似方便,但会带来三个问题: > 1. **备份膨胀**——数据库 dump 体积翻倍,恢复时间成倍增加 > 2. **内存压力**——SELECT 整行时 BLOB 内容也加载到内存,即使你不需要它 > 3. **无法 CDN 加速**——图片/附件走 OSS + Signed URL 是标准做法 > > **最佳实践**:数据库只存文件引用(OSS URL / 文件系统路径),文件本体放对象存储。 ## 关联笔记 - [[hhs/GORM/02-模型定义]] — GORM Struct Tag 如何映射这些 MySQL 类型 - [[hhs/Redis/02-核心数据类型]] — MySQL 与 Redis 数据类型的选型对比