This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/04-数据类型全景.md
T
2026-05-17 00:06:11 +08:00

306 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<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 字节) | **金额计算!绝对不要用浮点数存钱** |
```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<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 字段的筛选走索引
## 枚举与集合
```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 数据类型的选型对比