MySQL 8.0 数据类型选型指南:INT、DECIMAL、VARCHAR 与时间字段怎么选

个人主页: > 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)、金额、时间和大字段最容易踩的坑。

一、选类型先看四个维度

一个字段的类型至少要回答四个问题:

维度 要问的问题 选错后的典型后果
语义 它是数量、金额、时间还是标识? 无法正确比较或约束
范围 三年后可能增长到多大? 溢出或被迫改表
精度 是否允许近似值? 金额出现舍入误差
访问方式 是否频繁过滤、排序、建索引? 索引膨胀、隐式转换
"占用字节最少"不是唯一目标。为了省一个字节而让字段半年后溢出,通常比一开始选宽一点更贵。
推荐顺序是:
  1. 先确定业务语义和合法范围。
  2. 再选择能准确表达语义的最小类型。
  3. 最后结合索引、字符集和行大小做调整。

二、整数类型:范围比显示宽度重要

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 的最大值超过 Java long。
  • 两个无符号值做减法,负结果可能触发异常或类型提升问题。
  • 不同数据库之间迁移时,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 应作为扩展边界,而不是逃避关系模型设计的万能抽屉。
  • 类型一旦进入索引和海量数据,修改成本很高,建表时值得多做一次容量推演。
相关推荐
张张123y2 小时前
# 后端第七课:FastAPI 实战入门(用刚学的 Python 写第一个 API 接口)
开发语言·python·mysql·api·fastapi
渣渣盟2 小时前
2000万QPS背后的存储引擎博弈:InnoDB、MyRocks、WiredTiger到底谁更强?
数据库·mysql
张张123y2 小时前
# 后端第八课:数据库入门(给 FastAPI 接口加上“记忆“,存下问答历史)
数据库·sql·mysql·oracle·fastapi·sqllite
paopaokaka_luck3 小时前
校园新闻推荐系统(DeepSeek新闻助手、个性化推荐算法、 Redis热词排行榜、ECharts运营分析、互动与实时通知、新闻运营闭环)
java·spring boot·mysql·spring·tomcat·echarts·intellij idea
RestCloud3 小时前
数据同步零中断:ETL服务宕机重启后异常任务自动重跑
数据仓库·mysql·etl·etlcloud·数据传输·etl工具
猫咪很可爱4 小时前
MySQL 定时 DDL 导致 mysqldump 备份失败:场景模拟、排查与优化建议
mysql
数据库小学妹5 小时前
MySQL内存占用过高怎么排查?分层定位与OOM根因
数据库·经验分享·mysql·内存·oom
毕设单片机5 小时前
springboot志愿服务平台的设计与实现 36731
java·spring boot·mysql·志愿服务平台
Dxy12393102167 小时前
MySQL分库分表如何选择ID
mysql