目录

MySQL-04 数据类型详解

MySQL 数据类型详解


目录

  1. 整数类型
  2. CHAR vs VARCHAR 深度解析
  3. TEXT / BLOB 类型
  4. 浮点与精确数值类型
  5. 时间类型
  6. JSON 类型
  7. ENUM 与 SET
  8. 类型选择原则
  9. 高频面试题

1. 整数类型

1.1 类型一览

类型 字节 有符号范围 无符号范围(UNSIGNED)
TINYINT 1 -128 ~ 127 0 ~ 255
SMALLINT 2 -32768 ~ 32767 0 ~ 65535
MEDIUMINT 3 -8388608 ~ 8388607 0 ~ 16777215
INT 4 -2147483648 ~ 2147483647 0 ~ 4294967295
BIGINT 8 -2⁶³ ~ 2⁶³-1 0 ~ 2⁶⁴-1

1.2 显示宽度(M)是什么

INT(11)  -- 括号里的 11 是"显示宽度",不是存储宽度!

INT(1) 和 INT(11) 占用的字节数完全相同(都是 4 字节),存储范围也完全相同。

显示宽度只影响配合 ZEROFILL 使用时的输出格式:

CREATE TABLE t (n INT(5) ZEROFILL);
INSERT INTO t VALUES (42);
SELECT n FROM t;  -- 输出:00042(左补零到 5 位)

MySQL 8.0.17+ 已弃用整数类型的显示宽度,INT(11) 等写法虽然不报错但无意义,直接写 INT 即可。

1.3 AUTO_INCREMENT 注意点

-- 主键推荐 BIGINT UNSIGNED AUTO_INCREMENT
-- 原因:INT 最大约 21 亿,高并发系统可能几年内耗尽

CREATE TABLE orders (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
);

-- 查看当前自增值
SELECT AUTO_INCREMENT FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'myapp' AND TABLE_NAME = 'orders';

-- 重置自增值(只能设为比当前最大值更大)
ALTER TABLE orders AUTO_INCREMENT = 10000;

AUTO_INCREMENT 在 MySQL 8.0 前的坑

  • MySQL 5.7 及以前,AUTO_INCREMENT 计数器只存在内存中,重启后会重置为 max(id)+1。若曾删除过最大 id 的行,重启后可能复用旧 id,导致主键冲突。
  • MySQL 8.0 将 AUTO_INCREMENT 持久化到 Redo Log,重启不再重置。

1.4 BOOL / BOOLEAN

MySQL 中 BOOL / BOOLEANTINYINT(1) 的别名,存储 0 和 1:

CREATE TABLE t (is_active BOOLEAN);
-- 等价于
CREATE TABLE t (is_active TINYINT(1));

INSERT INTO t VALUES (TRUE), (FALSE);  -- 存 1 和 0

Go 驱动处理:使用 ?tinyInt1isBool=true(go-sql-driver 默认 false)或直接用 int8 接收。


2. CHAR vs VARCHAR 深度解析

这是 面试出现频率最高 的数据类型问题。

2.1 存储结构

CHAR(N):定长字符串,分配固定 N 个字符的空间。

CHAR(10) 存 'hello':
┌─────────────────────────────┐
│ h │ e │ l │ l │ o │   │   │   │   │   │
└─────────────────────────────┘
  ←─── 实际内容 ───→  ←─ 填充空格 ─→
写入时右填充空格,读取时自动去掉尾部空格

VARCHAR(N):变长字符串,实际占用 = 内容字节数 + 1~2 字节长度前缀。

VARCHAR(10) 存 'hello'(utf8mb4):
┌──────────────────────────────┐
│ 5 │ h │ e │ l │ l │ o │    │
└──────────────────────────────┘
  ↑                    ↑
长度前缀(1字节)    实际内容(5字节)

总占用:6 字节(长度 ≤ 255 时前缀 1 字节,否则 2 字节)

2.2 N 代表什么

CHAR(N)VARCHAR(N) 中,N 是字符数,不是字节数

-- utf8mb4 字符集,每个汉字占 3 字节
VARCHAR(10)  -- 最多存 10 个字符 = 最多 40 字节(emoji 4字节/字符)

-- 列的最大字节长度受行格式限制:
-- InnoDB 行长度上限约 65535 字节(Compact/Dynamic 格式)
-- VARCHAR(N) 的 N 最大:(65535 - 2字节长度前缀 - 1字节NULL标志) / 字节/字符
-- utf8mb4:(65535 - 3) / 4 ≈ 16383
-- latin1:65532 / 1 = 65532

2.3 VARCHAR 最大长度限制

-- 测试 VARCHAR 最大 N(utf8mb4)
CREATE TABLE t (
  a VARCHAR(16383)  -- utf8mb4 下约是上限
) ENGINE=InnoDB CHARSET=utf8mb4;

-- 多列时,所有列的长度之和不能超过行格式限制
CREATE TABLE t (
  a VARCHAR(10000),  -- 40000 字节
  b VARCHAR(10000)   -- 40000 字节
  -- 共 80000 字节 > 65535,报错!
);

2.4 性能对比

维度 CHAR VARCHAR
存储空间 固定,可能浪费 节省,按需分配
写入速度 稍快(定长,无需计算长度) 稍慢
读取速度 稍快(定长,可直接定位) 稍慢(需读长度前缀)
更新 定长不会产生碎片 变长可能产生页碎片
适用场景 固定长度:MD5、手机号、状态码 不固定长度:姓名、地址、描述

2.5 CHAR 的空格行为

-- CHAR 写入时右填充空格,读取时去掉尾部空格
CREATE TABLE t (c CHAR(10));
INSERT INTO t VALUES ('hello     ');  -- 尾部 5 个空格
SELECT c, LENGTH(c) FROM t;
-- c = 'hello', LENGTH = 5(空格被去掉了!)

-- VARCHAR 保留尾部空格
CREATE TABLE t2 (v VARCHAR(10));
INSERT INTO t2 VALUES ('hello     ');
SELECT v, LENGTH(v) FROM t2;
-- v = 'hello     ', LENGTH = 10(空格保留)

-- 比较时的差异(取决于 collation)
-- utf8mb4_unicode_ci 等 ci 排序规则,比较时忽略尾部空格
SELECT 'hello' = 'hello   ';  -- 1(相等!)
-- utf8mb4_bin 严格区分
SELECT 'hello' COLLATE utf8mb4_bin = 'hello   ' COLLATE utf8mb4_bin;  -- 0

2.6 行格式对 VARCHAR 的影响

InnoDB 的 Dynamic(MySQL 5.7+ 默认)和 Compressed 行格式,对于超过 768 字节的 VARCHAR 列,会将数据存在溢出页(Off-page)中:

列数据 ≤ 768 字节:存在数据页(in-page)
列数据 > 768 字节:部分或全部存在溢出页(off-page),数据页只存指针

-- 这就是为什么大 VARCHAR 列查询会多一次 I/O(溢出页读取)
-- 避免在频繁查询的列上使用过大的 VARCHAR
-- 查看行格式
SHOW TABLE STATUS LIKE 'users'\G
-- Row_format: Dynamic

-- 设置行格式
CREATE TABLE t (...) ROW_FORMAT=DYNAMIC;
ALTER TABLE t ROW_FORMAT=COMPRESSED;  -- 压缩,节省空间但有 CPU 开销

2.7 选择 CHAR 还是 VARCHAR?

用 CHAR:
  ✅ 长度固定或接近固定(±1字符)
  ✅ MD5 (CHAR(32))、UUID (CHAR(36))
  ✅ 手机号 (CHAR(11))
  ✅ 状态码、国家代码等枚举值(但 ENUM 更好)
  ✅ 频繁 UPDATE 的短字段(避免 VARCHAR 碎片)

用 VARCHAR:
  ✅ 长度差异较大(如用户名 1~64 字符)
  ✅ 姓名、地址、描述等自然语言文本
  ✅ URL、Email
  ✅ 大多数情况下默认选 VARCHAR

3. TEXT / BLOB 类型

3.1 类型对比

类型 最大长度 说明
TINYTEXT / TINYBLOB 255 字节
TEXT / BLOB 65535 字节(~64KB)
MEDIUMTEXT / MEDIUMBLOB 16777215 字节(~16MB)
LONGTEXT / LONGBLOB 4294967295 字节(~4GB)

TEXT:存储文本(有字符集) BLOB:存储二进制数据(无字符集,按字节比较)

3.2 TEXT 和 VARCHAR 的区别

对比项 VARCHAR TEXT
最大长度 65535 字节 最大 4GB
默认值 可以有 不能有
索引 直接建索引 只能建前缀索引
排序 用 sort buffer 用临时磁盘文件
存储位置 行内 超过阈值放溢出页
行长度计数 计入行长度 不完全计入
-- TEXT 列不能有默认值
CREATE TABLE t (
  content TEXT NOT NULL DEFAULT ''  -- ERROR!
);

-- TEXT 只能建前缀索引
CREATE INDEX idx_content ON articles (content(100));  -- 只索引前 100 字符

3.3 TEXT 的使用建议

不要在 TEXT 列上做 WHERE 过滤(无法有效利用索引)
不要在 SELECT * 时顺带查出 TEXT 列(大字段传输开销大)
大文本考虑存 OSS/S3,数据库只存 URL

// Go 中读取 TEXT 列到 []byte 或 string
var content string
rows.Scan(&content)

4. 浮点与精确数值类型

4.1 FLOAT / DOUBLE(不精确)

CREATE TABLE t (price FLOAT);
INSERT INTO t VALUES (0.1);
SELECT price FROM t;  -- 0.1(看起来对,但内部是近似值)

SELECT 0.1 + 0.2;  -- 0.30000000000000004(IEEE 754 浮点问题)

-- 危险!金额用 FLOAT 会出现精度错误
UPDATE accounts SET balance = balance - 0.1 WHERE id = 1;
-- 多次操作后,余额可能是 99.9000000001 或 99.8999999999

结论:永远不要用 FLOAT/DOUBLE 存储金额、价格等需要精确计算的值。

4.2 DECIMAL(精确)

DECIMAL(M, D)
-- M:总位数(精度),最大 65
-- D:小数位数(标度),最大 30,且 D ≤ M

DECIMAL(10, 2)  -- 最大存 99999999.99,精确到分
DECIMAL(18, 6)  -- 适合汇率、经纬度等高精度场景

-- 存储:每 9 位十进制数用 4 字节存储
-- DECIMAL(18, 2):整数部分 16 位 + 小数部分 2 位
--   整数 16 位 = ceil(16/9) × 4 = 8 字节
--   小数 2  位 = ceil(2/9)  × 4 = 4 字节
--   共 12 字节(但实际按范围压缩)
-- 精确计算
SELECT 0.1 + 0.2;                              -- 0.3(DECIMAL 运算)
SELECT CAST(0.1 AS DECIMAL(10,1)) + CAST(0.2 AS DECIMAL(10,1));  -- 0.3

-- 金额字段定义
amount DECIMAL(12, 2) NOT NULL DEFAULT '0.00'

Go 中处理 DECIMAL

// ❌ 用 float64 接收,有精度损失
var price float64
rows.Scan(&price)

// ✅ 用 string 接收,再转 decimal
import "github.com/shopspring/decimal"
var priceStr string
rows.Scan(&priceStr)
price, _ := decimal.NewFromString(priceStr)
total := price.Mul(decimal.NewFromInt(100))

// ✅ GORM 中使用 decimal 类型
type Product struct {
    Price decimal.Decimal `gorm:"type:decimal(10,2)"`
}

5. 时间类型

5.1 类型对比

类型 字节 范围 时区敏感 精度
DATE 3 1000-01-01 ~ 9999-12-31
TIME 3 -838:59:59 ~ 838:59:59 秒(可加微秒)
DATETIME 5~8 1000-01-01 ~ 9999-12-31 秒(可加微秒)
TIMESTAMP 4 1970-01-01 ~ 2038-01-19 秒(可加微秒)
YEAR 1 1901 ~ 2155

5.2 DATETIME vs TIMESTAMP 核心区别

TIMESTAMP 存储的是 UTC Unix 时间戳(4字节),读取时根据 time_zone 变量自动转换为本地时间。

DATETIME 存储的是字面时间值(5字节),不做时区转换。

-- 演示时区差异
SET time_zone = '+8:00';
CREATE TABLE t (
  dt  DATETIME,
  ts  TIMESTAMP
);
INSERT INTO t VALUES ('2024-01-15 10:00:00', '2024-01-15 10:00:00');

SET time_zone = '+0:00';  -- 改时区
SELECT * FROM t;
-- dt:  2024-01-15 10:00:00  (不变)
-- ts:  2024-01-15 02:00:00  (自动转换为 UTC+0)

如何选择?

TIMESTAMP:
  ✅ 跨时区系统(自动转换)
  ✅ 节省空间(4 字节 vs 5 字节)
  ❌ 2038 年问题(Unix 时间戳 32 位溢出)
  ❌ 范围小(只到 2038 年)

DATETIME:
  ✅ 不受时区影响(存的是"所见即所得"的时间)
  ✅ 范围更大(到 9999 年)
  ✅ 生产推荐,更直观、无 2038 问题
  ❌ 多占 1 字节

推荐:新项目统一用 DATETIME,同时在应用层或 MySQL 配置中统一时区(如 +8:00)。

5.3 微秒精度

MySQL 5.6.4+ 支持时间类型的微秒精度(fractional seconds):

DATETIME(6)    -- 精确到微秒,8 字节
TIMESTAMP(3)   -- 精确到毫秒,6 字节
TIME(6)        -- 精确到微秒

CREATE TABLE events (
  id         BIGINT AUTO_INCREMENT PRIMARY KEY,
  event_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
);

INSERT INTO events VALUES (DEFAULT, NOW(6));
-- 2024-01-15 10:30:00.123456

Go 中的时间处理

// DSN 必须加 parseTime=True,否则时间列扫描为 []byte
dsn := "...?parseTime=True&loc=Local"

var t time.Time
rows.Scan(&t)

// GORM 中时间列自动映射为 time.Time
type User struct {
    CreatedAt time.Time  // 自动处理
    UpdatedAt time.Time
}

// 存储时注意时区:time.Now() 取的是本地时间,配合 loc=Local 是正确的
// 如果 MySQL 服务器和应用不在同一时区,需要显式转换
t := time.Now().UTC()  // 或 .In(loc)

5.4 TIMESTAMP 的自动行为

-- MySQL 特性:TIMESTAMP 列可以自动初始化和更新
CREATE TABLE t (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 插入时:created_at 和 updated_at 自动设为当前时间
-- 更新时:updated_at 自动更新为当前时间,created_at 不变

-- DATETIME 同样支持(MySQL 5.6+)
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

6. JSON 类型

MySQL 5.7.8+ 原生支持 JSON 类型,存储时自动校验 JSON 格式,内部以二进制格式存储(比文本更快)。

-- 定义 JSON 列
CREATE TABLE products (
  id    INT PRIMARY KEY,
  attrs JSON
);

-- 插入
INSERT INTO products VALUES (1, '{"color":"red","size":42}');

-- 提取
SELECT attrs->>'$.color' FROM products WHERE id = 1;  -- red

-- 虚拟列 + 索引(JSON 列本身不能直接建索引)
ALTER TABLE products
  ADD COLUMN color VARCHAR(20) GENERATED ALWAYS AS (attrs->>'$.color') VIRTUAL,
  ADD INDEX idx_color (color);

详见 SQL 语法篇 JSON 章节。


7. ENUM 与 SET

7.1 ENUM

ENUM 存储预定义列表中的一个值,内部用整数存储(1~2 字节):

CREATE TABLE users (
  gender ENUM('M', 'F', 'Unknown') NOT NULL DEFAULT 'Unknown'
);

-- 内部存储:'M'→1, 'F'→2, 'Unknown'→3,0 代表空字符串(错误值)
-- 存储效率高:最多 255 个值用 1 字节,最多 65535 个值用 2 字节

-- 插入校验
INSERT INTO users (gender) VALUES ('X');  -- ERROR(不在列表中)
INSERT INTO users (gender) VALUES (1);    -- 等价于 'M'(用数字插入)

ENUM 的坑

-- 增加 ENUM 值需要 ALTER TABLE(MySQL 5.6+ 在末尾加值可以 INPLACE)
ALTER TABLE users MODIFY gender ENUM('M', 'F', 'Unknown', 'Other');

-- ENUM 排序按内部编号排序,不是字典序
SELECT gender FROM users ORDER BY gender;
-- 结果按 M(1)、F(2)、Unknown(3) 顺序,不是字母顺序

-- 解决:
ORDER BY FIELD(gender, 'F', 'M', 'Unknown')  -- 自定义顺序
-- 或
ORDER BY CAST(gender AS CHAR)                 -- 字典序

什么时候用 ENUM?

✅ 值固定、不常变化的状态字段(性别、星期、月份)
✅ 对存储效率要求高(大表中每行节省 3+ 字节)
❌ 需要频繁增减选项的字段(每次都要 DDL)
❌ 值很多(超过 20 个)
❌ 推荐替代方案:TINYINT + 业务层枚举(更灵活)

7.2 SET

SET 允许存储列表中的多个值(多选),内部用位图存储:

CREATE TABLE articles (
  tags SET('tech', 'life', 'sport', 'food')
);
INSERT INTO articles (tags) VALUES ('tech,life');
SELECT * FROM articles WHERE FIND_IN_SET('tech', tags);

SET 使用较少,多选场景推荐用关联表(article_tags)更灵活。


8. 类型选择原则

8.1 通用原则

1. 能用小类型就不用大类型
   TINYINT > SMALLINT > INT > BIGINT
   能确定不超过 127 就用 TINYINT,省空间 = 更多行进入 Buffer Pool

2. 数字用数字类型,不要用字符串存数字
   手机号虽然是 11 位,但不需要运算,用 CHAR(11) 更合适
   订单号如果有前缀('ORD-20240115-00001'),用 VARCHAR

3. 金额必须用 DECIMAL,不能用 FLOAT/DOUBLE

4. 时间推荐 DATETIME,避免 TIMESTAMP 的时区问题和 2038 问题

5. 字符串:短且固定长度用 CHAR,其余用 VARCHAR

6. 大文本(> 1KB)考虑是否真的需要存库,还是存 OSS + URL

7. 尽量用 NOT NULL + DEFAULT
   NULL 值占额外字节,且 NULL 参与计算结果都是 NULL
   索引对 NULL 值处理有特殊逻辑

8.2 常见字段建议

-- ID:主键用 BIGINT UNSIGNED AUTO_INCREMENT
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY

-- 布尔:TINYINT(1) 或直接 TINYINT
is_active TINYINT NOT NULL DEFAULT 1

-- 状态:TINYINT(配合业务层枚举)
status TINYINT NOT NULL DEFAULT 0 COMMENT '0:待处理 1:处理中 2:完成'

-- 手机号:CHAR(11)(固定长度,不做运算)
phone CHAR(11)

-- 邮箱:VARCHAR(128)(变长)
email VARCHAR(128) NOT NULL

-- 金额:DECIMAL(12, 2)(精确到分,最大 9999999999.99)
amount DECIMAL(12, 2) NOT NULL DEFAULT '0.00'

-- 时间:DATETIME
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

-- IP 地址:用 INT UNSIGNED 存(省空间,支持范围查询)
-- INET_ATON('192.168.1.1') → 整数;INET_NTOA(整数) → IP 字符串
ip_addr INT UNSIGNED

-- URL/路径:VARCHAR(512)
avatar_url VARCHAR(512)

9. 高频面试题

Q1:CHAR 和 VARCHAR 的区别?

CHAR VARCHAR
长度 定长,不足右填充空格 变长,按实际长度存
存储 N × 字符字节数 内容字节数 + 1~2 字节长度前缀
读取 自动去掉尾部空格 保留尾部空格
性能 稍快(定长,寻址简单) 稍慢,但节省空间
适用 固定长度(MD5、手机号) 可变长度(姓名、描述)

Q2:VARCHAR(10) 和 VARCHAR(100) 有什么区别?

  • 存储相同的内容(如 “hello”)时,占用空间完全相同(5字节内容 + 1字节长度前缀)
  • 区别在于内存分配:MySQL 某些操作(如排序、内存临时表)会按声明的最大长度分配内存
  • 因此 VARCHAR(100) 在内存操作时比 VARCHAR(10) 更占内存,即使实际内容一样
  • 建议:不要随意声明很大的 VARCHAR,合理评估实际最大长度

Q3:为什么不推荐用 FLOAT/DOUBLE 存金额?

FLOAT 和 DOUBLE 遵循 IEEE 754 浮点标准,是近似值,存在精度问题:

SELECT 0.1 + 0.2;  -- 0.30000000000000004

金额计算需要精确,应使用 DECIMAL(精确数值类型,内部用十进制字符串存储)。

Go 中配合 shopspring/decimal 库使用,避免 float64 引入的精度问题。


Q4:DATETIME 和 TIMESTAMP 的区别?

DATETIME TIMESTAMP
字节 5(+ 0~3 微秒精度) 4(+ 0~3 微秒精度)
范围 1000 ~ 9999 年 1970 ~ 2038 年
时区 不受影响,存什么读什么 time_zone 影响,自动转换
默认值 支持 CURRENT_TIMESTAMP 支持,且早期版本会自动设置

推荐 DATETIME:范围更大、不受时区影响、无 2038 问题,更直观。


Q5:INT(11) 中的 11 代表什么?

11 是显示宽度,不影响存储范围,INT 永远是 4 字节,范围是 -2147483648 ~ 2147483647(有符号)。

显示宽度只在配合 ZEROFILL 属性时有意义(补前导零)。MySQL 8.0.17+ 已弃用整数类型的显示宽度,直接写 INT 即可。


Q6:为什么建议用 BIGINT 而不是 INT 做主键?

INT 有符号最大约 21 亿,无符号约 42 亿。高并发系统(如每天百万级写入)可能在数年内耗尽。BIGINT 最大约 922 亿亿,基本不会耗尽,代价仅多 4 字节(8 vs 4 字节)。


Q7:TEXT 和 VARCHAR 的区别?

VARCHAR TEXT
最大长度 65535 字节 最大 4GB(LONGTEXT)
默认值 可以有 不支持
索引 可直接建 只能建前缀索引
排序 用 sort buffer 用磁盘临时文件,更慢
行长度 计入 65535 限制 不完全计入

实际建表时:不超过 1KB 的文本用 VARCHAR,更长的用 TEXT,但大文本尽量存 OSS。


Q8:MySQL 中 NULL 和空字符串(’’)的区别?

NULL 空字符串
含义 未知/不存在 确实存在,且为空
存储 每列额外 1 bit 的 NULL 标志位 正常存储
索引 可以索引(但处理逻辑特殊) 可以索引
比较 IS NULL,不能用 = NULL = ''= ""
计算 任何值与 NULL 运算结果都是 NULL 正常参与运算
COUNT COUNT(col) 不计 NULL 计入
SELECT NULL = NULL;   -- NULL(不是 1!)
SELECT NULL IS NULL;  -- 1
SELECT '' IS NULL;    -- 0
SELECT '' = '';       -- 1

建议:字段尽量 NOT NULL + DEFAULT,减少 NULL 带来的复杂性。


Q9:VARCHAR 字段存汉字,N 最大能设多少?

取决于字符集:

  • utf8mb4:每个汉字最多 3 字节(BMP 汉字),emoji 4 字节,VARCHAR 最大 N ≈ 16383(65532 / 4)
  • utf8(即 utf8mb3):每个汉字 3 字节,VARCHAR 最大 N ≈ 21844(65532 / 3)
  • gbk:每个汉字 2 字节,VARCHAR 最大 N ≈ 32766(65532 / 2)

同时,一行中所有列加起来不超过 65535 字节,VARCHAR(N) 的 N 也受其他列长度影响。


Q10:为什么 CHAR 适合存 MD5?

MD5 结果永远是 32 个十六进制字符,长度固定。用 CHAR(32):

  1. 不需要长度前缀(省 1~2 字节)
  2. 定长存储,InnoDB 寻址更快
  3. 无字符串碎片(定长不会在 UPDATE 时扩展)

对比:

CHAR(32):  32 字节
VARCHAR(32):32 + 1(长度前缀)= 33 字节

小结

整数:按需选型,BIGINT 做主键更安全
CHAR vs VARCHAR:
  固定长度 → CHAR;可变长度 → VARCHAR
  VARCHAR(N) 的 N 是字符数不是字节数
  VARCHAR 有 1~2 字节长度前缀
  CHAR 读取自动去尾部空格
金额:必须 DECIMAL,拒绝 FLOAT/DOUBLE
时间:优先 DATETIME(无时区问题、无 2038 问题)
NULL:尽量 NOT NULL + DEFAULT,简化处理逻辑