12 KiB
tags, create time
| tags | 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。表越大,连锁放大效应越惊人。
整数类型
graph TD
INT_TYPES["整数类型家族"]
INT_TYPES --> TINY["TINYINT<br/>1 byte | ±128"]
INT_TYPES --> SMALL["SMALLINT<br/>2 bytes | ±32K"]
INT_TYPES --> MED["MEDIUMINT<br/>3 bytes | ±8M"]
INT_TYPES --> INTN["INT<br/>4 bytes | ±21亿"]
INT_TYPES --> BIG["BIGINT<br/>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 字节) | 金额计算!绝对不要用浮点数存钱 |
-- ❌ 错误示范:浮点数误差累积
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 的选择
-- ✅ 适合 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 | — |
-- 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 类型,支持部分查询和索引能力。
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);
flowchart LR
A["JSON Column<br/>原始存储"] --> 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<br/>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 字段的筛选走索引
枚举与集合
-- 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?这是永恒争论
-- 方案 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/ PythonIntEnum在代码里维护枚举定义;只有在值域极稳定且不需要跨语言共享时再用原生 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 | 大文件存储 |
-- 存储 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 — 定长与变长的选择
-- 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 系列看似方便,但会带来三个问题:
- 备份膨胀——数据库 dump 体积翻倍,恢复时间成倍增加
- 内存压力——SELECT 整行时 BLOB 内容也加载到内存,即使你不需要它
- 无法 CDN 加速——图片/附件走 OSS + Signed URL 是标准做法
最佳实践:数据库只存文件引用(OSS URL / 文件系统路径),文件本体放对象存储。
关联笔记
- hhs/GORM/02-模型定义 — GORM Struct Tag 如何映射这些 MySQL 类型
- hhs/Redis/02-核心数据类型 — MySQL 与 Redis 数据类型的选型对比