MySQL-04 数据类型详解
MySQL 数据类型详解
目录
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 / BOOLEAN 是 TINYINT(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~2 字节)
- 定长存储,InnoDB 寻址更快
- 无字符串碎片(定长不会在 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,简化处理逻辑
xingliuhua