个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [MySQL 8.0 数据类型选型指南:INT、DECIMAL、VARCHAR 与时间字段怎么选](#MySQL 8.0 数据类型选型指南:INT、DECIMAL、VARCHAR 与时间字段怎么选)
-
- 一、选类型先看四个维度
- 二、整数类型:范围比显示宽度重要
-
- [2.1 INT(11) 不是最多 11 位](#2.1 INT(11) 不是最多 11 位)
- [2.2 UNSIGNED 不是无脑最佳实践](#2.2 UNSIGNED 不是无脑最佳实践)
- [2.3 布尔值实际怎么存](#2.3 布尔值实际怎么存)
- 三、精确数值与近似数值
-
- [3.1 金额为什么必须优先 DECIMAL](#3.1 金额为什么必须优先 DECIMAL)
- [3.2 什么时候可以用 DOUBLE](#3.2 什么时候可以用 DOUBLE)
- [四、CHAR、VARCHAR 与 TEXT 的真实差异](#四、CHAR、VARCHAR 与 TEXT 的真实差异)
-
- [4.1 VARCHAR(N) 的 N 是字符数](#4.1 VARCHAR(N) 的 N 是字符数)
- [4.2 VARCHAR 最大值为什么不是固定 65535 个字符](#4.2 VARCHAR 最大值为什么不是固定 65535 个字符)
- [4.3 长度前缀按最大字节长度判断](#4.3 长度前缀按最大字节长度判断)
- [4.4 CHAR 适合真正定长的值](#4.4 CHAR 适合真正定长的值)
- [4.5 TEXT 不是更大的 VARCHAR 而已](#4.5 TEXT 不是更大的 VARCHAR 而已)
- 五、二进制数据不要伪装成字符串
- 六、时间类型:先决定"绝对时刻"还是"日历时间"
-
- [6.1 DATE、DATETIME、TIMESTAMP 怎么选](#6.1 DATE、DATETIME、TIMESTAMP 怎么选)
- [6.2 TIMESTAMP 的范围限制](#6.2 TIMESTAMP 的范围限制)
- [6.3 Unix 时间戳不应默认用 INT](#6.3 Unix 时间戳不应默认用 INT)
- [6.4 小数秒精度要显式选择](#6.4 小数秒精度要显式选择)
- [七、JSON、ENUM 与 SET 怎么取舍](#七、JSON、ENUM 与 SET 怎么取舍)
-
- [7.1 JSON 适合扩展属性,不适合核心字段](#7.1 JSON 适合扩展属性,不适合核心字段)
- [7.2 ENUM 的收益与代价](#7.2 ENUM 的收益与代价)
- 八、类型会直接影响索引
-
- [8.1 相同语义的关联列必须同类型](#8.1 相同语义的关联列必须同类型)
- [8.2 VARCHAR 长度不是越大越省心](#8.2 VARCHAR 长度不是越大越省心)
- [8.3 前缀索引要验证区分度](#8.3 前缀索引要验证区分度)
- 九、一张较合理的订单表示例
- [十、修改类型前不要只看 DDL 能否执行](#十、修改类型前不要只看 DDL 能否执行)
- 十一、常见误区
- 十二、上线前类型检查清单
- 十三、小结
MySQL 8.0 数据类型选型指南:INT、DECIMAL、VARCHAR 与时间字段怎么选
数据类型不是建表时随手填的"容器大小"。
它会同时影响数据正确性、单行宽度、索引体积、比较规则和 SQL 可优化性。
本文从业务语义出发,给出 MySQL 8.0 的类型选择方法,
并专门拆解
VARCHAR(N)、金额、时间和大字段最容易踩的坑。
一、选类型先看四个维度
一个字段的类型至少要回答四个问题:
| 维度 | 要问的问题 | 选错后的典型后果 |
|---|---|---|
| 语义 | 它是数量、金额、时间还是标识? | 无法正确比较或约束 |
| 范围 | 三年后可能增长到多大? | 溢出或被迫改表 |
| 精度 | 是否允许近似值? | 金额出现舍入误差 |
| 访问方式 | 是否频繁过滤、排序、建索引? | 索引膨胀、隐式转换 |
| "占用字节最少"不是唯一目标。为了省一个字节而让字段半年后溢出,通常比一开始选宽一点更贵。 | ||
| 推荐顺序是: |
- 先确定业务语义和合法范围。
- 再选择能准确表达语义的最小类型。
- 最后结合索引、字符集和行大小做调整。
二、整数类型:范围比显示宽度重要
MySQL 常用整数类型如下:
| 类型 | 字节 | 有符号范围 | 无符号范围 |
|---|---|---|---|
TINYINT |
1 | -128~127 | 0~255 |
SMALLINT |
2 | -32768~32767 | 0~65535 |
MEDIUMINT |
3 | 约 -838 万~838 万 | 0~约 1677 万 |
INT |
4 | 约 -21.47 亿~21.47 亿 | 0~约 42.94 亿 |
BIGINT |
8 | 约 ±922 亿亿 | 0~约 1844 亿亿 |
2.1 INT(11) 不是最多 11 位
旧表里经常看到:
sql
id INT(11) NOT NULL
括号里的 11 曾经只是显示宽度,不限制存储范围,也不节省空间。MySQL 8.0 已弃用并逐步移除整数显示宽度语义,建新表直接写 INT。
sql
age TINYINT UNSIGNED NOT NULL
这才是真正通过类型和符号位约束范围。
2.2 UNSIGNED 不是无脑最佳实践
非负字段可以考虑 UNSIGNED,它能把正数上限扩大一倍。但要同时考虑语言和生态兼容性:
- Java 没有原生无符号整数类型。
BIGINT UNSIGNED的最大值超过 Javalong。- 两个无符号值做减法,负结果可能触发异常或类型提升问题。
- 不同数据库之间迁移时,
UNSIGNED兼容性较差。
状态值、年龄、计数等明确非负的数据很适合;跨数据库模型或可能出现负差值的字段则要谨慎。
2.3 布尔值实际怎么存
MySQL 的 BOOLEAN、BOOL 是 TINYINT(1) 的同义写法,并不会把列限制为只能存 0 和 1。
sql
CREATE TABLE feature_switch (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
enabled BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
CONSTRAINT chk_enabled CHECK (enabled IN (0, 1))
);
MySQL 8.0.16 起才真正执行 CHECK 约束;更早的 8.0 和 MySQL 5.7 会解析但忽略它。关键业务仍应在应用入口校验,并用约束兜底。
三、精确数值与近似数值
3.1 金额为什么必须优先 DECIMAL
FLOAT 和 DOUBLE 使用二进制浮点表示,很多十进制小数无法精确存储。
sql
SELECT CAST(0.1 AS DOUBLE) + CAST(0.2 AS DOUBLE);
结果在显示时可能被格式化得像 0.3,但底层比较和连续计算可能暴露误差。金额、税率、结算数量应使用 DECIMAL:
sql
price DECIMAL(12, 2) NOT NULL,
tax_rate DECIMAL(7, 6) NOT NULL,
quantity DECIMAL(14, 3) NOT NULL
DECIMAL(M,D) 中:
M是总有效数字位数。D是小数位数。- 整数部分最多
M-D位。 - 小数点和负号不计入
M。
DECIMAL(12,2)可表示到 99 亿级别且保留两位小数,但具体上限还要考虑是否允许负数。
3.2 什么时候可以用 DOUBLE
传感器采样、科学计算、统计模型等允许近似值的场景可以使用 DOUBLE。
| 数据 | 推荐类型 | 原因 |
|---|---|---|
| 商品金额 | DECIMAL |
必须十进制精确 |
| 经纬度计算中间值 | DOUBLE |
更关注范围和计算速度 |
| 比例配置 | DECIMAL |
配置值需要可重复 |
| 实验测量值 | DOUBLE |
本身就有测量误差 |
不要用 = 判断浮点计算结果,通常应比较误差范围。 |
四、CHAR、VARCHAR 与 TEXT 的真实差异
4.1 VARCHAR(N) 的 N 是字符数
在现代 MySQL 中:
sql
name VARCHAR(50)
表示最多 50 个字符,不是 50 个字节。50 个英文字符、50 个汉字在字符数量上都合法,只是使用 utf8mb4 时实际字节数不同。
可直接验证:
sql
SELECT
CHAR_LENGTH('数据库A') AS characters,
LENGTH('数据库A') AS bytes;
在 utf8mb4 下,前者返回 4,后者通常返回 10。CHAR_LENGTH 看字符数,LENGTH 看字节数。
4.2 VARCHAR 最大值为什么不是固定 65535 个字符
MySQL 非大对象列的逻辑行大小上限约为 65535 字节,它是整行共享的预算,不是每个 VARCHAR 各有 65535 字节。
预算还要扣除:
- 其他列的最大字节长度;
VARCHAR的 1 或 2 字节长度前缀;NULL位图和记录额外信息;- 字符集的最大每字符字节数。
在utf8mb4下,一个字符最多需要 4 字节,所以孤立的VARCHAR也不可能稳定声明为VARCHAR(65535)。实际业务更不应靠手算上限设计超宽表。
可以用实验观察限制:
sql
CREATE TABLE too_wide_demo (
c1 VARCHAR(10000),
c2 VARCHAR(10000)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
具体能否创建与列定义、行格式和版本有关,超限时常见错误是 ERROR 1118: Row size too large。
4.3 长度前缀按最大字节长度判断
VARCHAR 除实际内容外,还要保存长度前缀。最大字节长度不超过 255 时通常使用 1 字节,否则使用 2 字节。
因此在 utf8mb4 下:
VARCHAR(63)最大 252 字节,通常是 1 字节前缀;VARCHAR(64)最大 256 字节,通常需要 2 字节前缀。
这不是要求大家把字段都改成 63,而是说明N、字符集和物理存储之间确实有关联。字段长度首先应符合业务边界。
4.4 CHAR 适合真正定长的值
适合 CHAR 的典型数据:
- 固定长度国家码;
- 固定格式摘要值;
- 长度始终一致的内部代码。
手机号虽然经常是 11 位,但国际号码、区号和前导+会改变长度,通常使用VARCHAR更符合语义。
| 对比项 |CHAR|VARCHAR|
|---|---|---|
| 长度 | 固定 | 可变 |
| 存储特征 | 会按定义长度处理 | 实际数据加长度前缀 |
| 适合数据 | 真正定长代码 | 姓名、邮箱、地址 |
| 尾部空格 | 受 SQL 模式和语义影响 | 通常保留,比较规则仍可能忽略 |
不要用"CHAR 一定比 VARCHAR 快"这种脱离数据分布的结论做设计。现代 InnoDB 下,正确语义和行宽通常更重要。
4.5 TEXT 不是更大的 VARCHAR 而已
TEXT 家族适合长文本,但有不同的使用成本:
| 类型 | 最大长度级别 | 常见用途 |
|---|---|---|
TINYTEXT |
255 字节 | 很少单独使用 |
TEXT |
约 64 KB | 描述、正文片段 |
MEDIUMTEXT |
约 16 MB | 较长文章 |
LONGTEXT |
约 4 GB | 极大文本,需严格评估 |
长列可能被放到溢出页,聚簇索引记录保留指针或前缀,具体行为受 ROW_FORMAT、页大小和整行宽度影响。 |
||
不要因为"能存 4 GB"就把文件塞进 LONGTEXT。图片和大文件通常放对象存储,表里保存对象键、大小、摘要和元数据。 |
||
MySQL 8.0 允许为 TEXT 指定表达式默认值,写法和小版本有关;MySQL 5.7 通常不允许 TEXT/BLOB 默认值。跨版本 DDL 不应依赖这个差异。 |
五、二进制数据不要伪装成字符串
哈希、UUID、密钥摘要是字节序列时,优先考虑 BINARY 或 VARBINARY:
sql
request_id BINARY(16) NOT NULL,
file_hash BINARY(32) NOT NULL
相比十六进制字符串:
BINARY(16)存 UUID 只需 16 字节;CHAR(36)带连字符 UUID 需要 36 个字符;- 二进制索引更短,比较不受字符排序规则影响。
转换示例:
sql
SELECT UUID_TO_BIN(UUID()) AS raw_uuid;
SELECT BIN_TO_UUID(UUID_TO_BIN(UUID())) AS uuid_text;
MySQL 5.7 没有 UUID_TO_BIN/BIN_TO_UUID,需要由应用完成十六进制与字节数组转换。
六、时间类型:先决定"绝对时刻"还是"日历时间"
6.1 DATE、DATETIME、TIMESTAMP 怎么选
| 类型 | 语义 | 时区转换 | 常见用途 |
|---|---|---|---|
DATE |
日历日期 | 否 | 生日、账期、营业日 |
TIME |
时长或时刻片段 | 否 | 时长、每日时间 |
DATETIME |
不带时区的日期时间 | 否 | 预约时间、业务发生时间 |
TIMESTAMP |
UTC 时刻,经会话时区显示 | 是 | 创建时间、更新时间 |
TIMESTAMP 写入时从会话时区转为 UTC,读取时再转回会话时区。这对"同一个绝对时刻"很方便,但也意味着连接时区不一致会让展示值变化。 |
sql
SET time_zone = '+00:00';
SELECT CURRENT_TIMESTAMP;
SET time_zone = '+08:00';
SELECT CURRENT_TIMESTAMP;
生产连接池应显式设置时区,不能依赖服务器操作系统默认值。
6.2 TIMESTAMP 的范围限制
常规 MySQL 8.0 中 TIMESTAMP 仍受 2038 年附近的上限约束,而 DATETIME 的范围可到 9999 年。
未来预约、证件有效期、历史档案等跨度大的业务应使用 DATETIME。创建和更新时间如果确认生命周期范围,则可使用 TIMESTAMP。
6.3 Unix 时间戳不应默认用 INT
把时间存成 INT 或 BIGINT 并不天然更高效:
- 人工查询不直观;
- 日期函数和范围表达更难读;
- 秒、毫秒、微秒单位容易混淆;
INT秒级时间也有 2038 问题;- 时区与业务语义被推给每个调用方处理。
只有协议明确要求 epoch、跨系统都以数值时间交换时才考虑,毫秒时间戳必须使用BIGINT。
6.4 小数秒精度要显式选择
sql
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL
DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3)
(3) 表示毫秒精度,(6) 表示微秒精度。精度越高,存储和索引成本略增;没有业务需求就不要盲目用微秒。
七、JSON、ENUM 与 SET 怎么取舍
7.1 JSON 适合扩展属性,不适合核心字段
MySQL 5.7 起提供原生 JSON 类型,MySQL 8.0 的函数和索引能力更完整。适合存储结构不稳定、查询频率低的扩展属性:
sql
CREATE TABLE product_ext (
product_id BIGINT UNSIGNED NOT NULL,
attributes JSON NOT NULL,
color VARCHAR(32)
GENERATED ALWAYS AS (
JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color'))
) STORED,
PRIMARY KEY (product_id),
KEY idx_color (color)
);
核心状态、金额、关联 ID 不要藏在 JSON 里。否则约束、类型检查、索引和数据治理都会变难。
7.2 ENUM 的收益与代价
ENUM 存储紧凑,也能限定值集合,但增减枚举值需要 DDL,并让业务字典与表结构绑定。
| 场景 | 建议 |
|---|---|
| 极稳定且值很少 | 可用 ENUM |
| 状态频繁增加 | TINYINT/SMALLINT + 字典约束 |
| 需要多语言文案 | 数值代码,不在库里存展示文案 |
| 外部协议字符串 | VARCHAR + 校验 |
不要通过 ORDER BY enum_col 猜业务顺序,ENUM 默认按内部定义顺序排序,可能与字典演进后的业务顺序不同。 |
八、类型会直接影响索引
InnoDB 二级索引叶子节点会保存索引列和主键值。字段越宽,单页能容纳的记录越少,B+Tree 更大,缓存命中率可能越低。
8.1 相同语义的关联列必须同类型
以下设计会埋下隐式转换风险:
sql
-- 用户表
id BIGINT UNSIGNED
-- 订单表错误示例
user_id VARCHAR(20)
JOIN 时要在数字和字符串之间转换,可能无法按预期使用索引。正确做法是让类型、符号属性和字符排序规则全部一致。
8.2 VARCHAR 长度不是越大越省心
实际只保存 20 个字符时,VARCHAR(20) 与 VARCHAR(2000)存储正文的差异可能不大,但最大长度会影响:
- 可否建立完整索引;
- 内部临时表和排序内存估算;
- ORM 参数校验和接口契约;
- 整行最大长度预算。
长度应来自业务上限,而不是统一写 255 或 5000。
8.3 前缀索引要验证区分度
sql
SELECT
COUNT(DISTINCT LEFT(email, 12)) / COUNT(*) AS selectivity_12,
COUNT(DISTINCT email) / COUNT(*) AS selectivity_full
FROM customer;
当前缀区分度接近完整列时,才有理由使用前缀索引。唯一性要求不能靠可能碰撞的短前缀保证。
九、一张较合理的订单表示例
sql
CREATE TABLE customer_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
customer_id BIGINT UNSIGNED NOT NULL,
status TINYINT UNSIGNED NOT NULL,
currency CHAR(3) NOT NULL,
amount DECIMAL(14, 2) NOT NULL,
paid_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
remark VARCHAR(500) NULL,
ext JSON NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_customer_created (customer_id, created_at, id),
CONSTRAINT chk_order_status CHECK (status BETWEEN 0 AND 9),
CONSTRAINT chk_order_amount CHECK (amount >= 0)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
这张表体现了几个原则:
- 技术主键和业务单号分开;
- 金额使用精确十进制;
- 币种是固定三字符代码;
- 可空时间表示事件尚未发生;
- JSON 只承载非核心扩展属性;
- 索引服务于客户时间线查询。
十、修改类型前不要只看 DDL 能否执行
类型迁移可能导致重建表、长时间持锁、截断或语义变化。上线前至少检查:
sql
-- 找出超过新长度的数据
SELECT id, CHAR_LENGTH(remark)
FROM customer_order
WHERE CHAR_LENGTH(remark) > 200;
-- 检查负数,准备改为 UNSIGNED
SELECT MIN(amount), COUNT(*)
FROM customer_order
WHERE amount < 0;
-- 检查无法转换的文本数字
SELECT legacy_id
FROM legacy_table
WHERE legacy_id NOT REGEXP '^[0-9]+$';
还要在测试环境验证 ALTER TABLE 使用的算法、锁级别、耗时、复制延迟和回滚方案,而不是看到 ALGORITHM=INPLACE 就认为没有影响。
十一、常见误区
误区 1:INT(4) 只能存四位数。
错误。括号不是范围,范围由整数类型和是否 UNSIGNED 决定。
误区 2:VARCHAR(50) 只能存 16 个中文。
错误。50 是字符数;字节数影响的是行大小与索引成本。
误区 3:金额使用 DOUBLE 更快,所以值得。
金额正确性优先,绝大多数交易字段应使用 DECIMAL。
误区 4:所有时间存 BIGINT 最通用。
它会丢失数据库时间语义并增加单位、可读性和时区管理成本。
误区 5:TEXT 放页外,所以主表完全不受影响。
是否页外取决于行格式和整行大小,记录仍需保存额外信息,读取也有 I/O 成本。
误区 6:所有非负数字都必须 UNSIGNED。
要结合语言映射、运算语义和跨数据库兼容性决定。
十二、上线前类型检查清单
- 字段类型是否直接表达业务语义?
- 三年后的最大值是否仍在范围内?
- 金额和比例是否使用了足够精度的
DECIMAL? VARCHAR(N)的 N 是否来自真实业务边界?- 是否用
utf8mb4的最坏 4 字节评估行宽和索引? - 关联字段的类型、符号属性、字符集和排序规则是否一致?
- 时间字段表达的是绝对时刻还是本地日历时间?
- 连接时区是否固定并可观测?
- JSON 中是否藏了需要约束和索引的核心字段?
- 大文本或文件是否应该拆表或放对象存储?
- 类型变更前是否扫描了不兼容的存量数据?
十三、小结
- 数据类型首先服务于正确语义,其次才是空间优化。
- 整数范围由类型与
UNSIGNED决定,INT(11)不是长度限制。 - 金额使用
DECIMAL,允许近似的科学计算才考虑DOUBLE。 VARCHAR(N)的 N 是字符数,但行大小和索引限制按字节计算。CHAR适合真正定长值,TEXT适合长文本,不要用固定口号替代数据分析。- 二进制标识和摘要优先用
BINARY/VARBINARY,避免字符排序规则参与比较。 TIMESTAMP带会话时区转换且有范围限制,DATETIME更适合宽时间范围的业务时间。- JSON 应作为扩展边界,而不是逃避关系模型设计的万能抽屉。
- 类型一旦进入索引和海量数据,修改成本很高,建表时值得多做一次容量推演。