Skip to content

MySQL 数据类型选择与优化

字段类型选错,不只是「占空间大」这么简单——它会影响索引效率、查询性能,甚至导致数据溢出。

你可能觉得 VARCHAR(255) 是万能答案,但今天告诉你:没有最好的类型,只有最合适的类型


数字类型:整数

MySQL 整数类型一览

类型字节有符号范围无符号范围适用场景
TINYINT1-128~1270~255状态码、性别
SMALLINT2-32768~327670~65535数量、年龄
MEDIUMINT3-8388608~83886070~16777215中等数量
INT4-21亿~21亿0~42亿主键、订单号
BIGINT8极大0~极大金额、大ID

选择原则

java
// 反面教材:所有数字都用 INT
// 问题:TINYINT 能存的偏偏用 INT,浪费 3 字节/行
public class BadExample {
    // 用户状态:0禁用 1启用 2待审核
    int status;  // ❌ 浪费,用 TINYINT 就够了
    
    // 用户年龄:0-150
    int age;     // ❌ 浪费,用 TINYINT UNSIGNED
    
    // 订单金额(分)
    int amount;  // ❌ 可能溢出,用 DECIMAL
}

// 正确做法
public class GoodExample {
    // 状态码:0-2
    TINYINT status;  // ✅ 1 字节
    
    // 年龄
    TINYINT UNSIGNED age;  // ✅ 0-255
    
    // 金额(精确到分)
    DECIMAL(10, 2) amount;  // ✅ 精确存储
}

主键类型选择

ID 类型适用场景注意事项
INT单表数据 < 20 亿业务增长快慎用
BIGINT超大数据量、安全 ID几乎无上限
自增主键聚簇索引性能最优分布式场景不友好
UUID分布式 ID随机插入,索引性能差

UUID 作为主键是大忌——插入时随机位置导致页分裂和随机 I/O,性能断崖式下降。


字符串类型:VARCHAR vs CHAR

VARCHAR:可变长度

sql
-- VARCHAR 特点:按实际长度存储
CREATE TABLE user1 (
    name VARCHAR(255)  -- 只存 "张三" 用 6 字节,不是 255 字节
);

VARCHAR 存储规则

  • 1~255 字节:1 字节存储长度
  • 256~65535 字节:2 字节存储长度
  • 最大 65535 字节(受行大小限制)

CHAR:固定长度

sql
-- CHAR 特点:不足部分用空格填充
CREATE TABLE user2 (
    code CHAR(6)  -- 存储 "001" 会补成 "001   "
);

选择场景

场景推荐类型原因
用户名、手机号VARCHAR长度不固定
状态码(固定长度)CHAR如 MD5 值、身份证号
性别CHAR(1) 或 TINYINT固定长度
邮政编码CHAR(6)固定 6 位

VARCHAR 最大长度

很多人不知道:VARCHAR(255) 不是最大 255 个字符

sql
-- MySQL 5.7+
VARCHAR(255)  -- 最多 255 个字符(不是字节)

-- 但实际受字符集影响:
-- utf8mb4:每个字符最多 4 字节
-- 所以 VARCHAR(255) 在 utf8mb4 下最多存 255 * 4 = 1020 字节
-- 如果存中文,实际只能存 255 个中文

-- 如果要存更长的文本,用 TEXT
VARCHAR(65535)  -- ERROR: 行大小超限
TEXT  -- ✅ 可存 65535 字节

时间类型

常见时间类型对比

类型范围占用精度适用场景
DATE'1000-01-01' ~ '9999-12-31'3 字节生日
DATETIME'1000-01-01 00:00:00' ~8 字节需要完整时间
TIMESTAMP'1970-01-01 00:00:01' UTC ~4 字节跨时区
BIGINT时间戳8 字节毫秒/微秒高精度需求

时区问题

java
// TIMESTAMP 的坑:自动时区转换
// 服务器在东八区,存储的是 UTC 时间
// 客户端读取时自动转成本地时间

// 如果你的业务需要跨时区,TIMESTAMP 是正确的选择
// 如果你的业务不需要跨时区,DATETIME 更简单

// Java 实体类建议
public class Order {
    // 创建时间:不需要跨时区,用 DATETIME
    private LocalDateTime createTime;
    
    // 修改时间:想记录最后一次变更时刻,用 TIMESTAMP
    private Timestamp updateTime;  // 数据库自动更新
}

时间戳 vs DATETIME

sql
-- TIMESTAMP 自动更新(MySQL 5.6.5+)
CREATE TABLE log (
    id BIGINT,
    action VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 插入时不指定 created_at,自动取当前时间
-- 更新时不指定 created_at,自动更新为当前时间

ENUM 和 SET

ENUM:单选

sql
-- ENUM 存储的是数字,但显示为字符串
CREATE TABLE order_status (
    status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled')
);

-- 内部存储:
-- pending = 1
-- paid = 2
-- shipped = 3
-- ...

-- 优点:比 VARCHAR 更省空间,查询效率高
-- 缺点:修改需要 ALTER TABLE

SET:多选

sql
-- SET 存储多个选项
CREATE TABLE user_permissions (
    permissions SET('read', 'write', 'delete', 'admin')
);

-- 存储 "read,write" = 数字 3
-- 存储 "read,write,admin" = 数字 13

NULL 的坑

NULL 与空值的区别

sql
-- 很多人搞不清楚 NULL 和空字符串的区别
CREATE TABLE test (
    col1 VARCHAR(10) DEFAULT NULL,   -- NULL:未知、未设置
    col2 VARCHAR(10) DEFAULT ''     -- 空字符串:有值,是空串
);

-- 统计时要注意:
SELECT COUNT(col1) FROM test;  -- 不计 NULL
SELECT COUNT(col2) FROM test;  -- 计空字符串

NOT NULL 的重要性

java
// 字段尽量 NOT NULL,原因:
// 1. 索引效率更高(NULL 值需要特殊处理)
// 2. 统计运算更简单
// 3. 减少程序 bug(空指针、类型转换异常)

// 反面例子
public class User {
    String name;      // ❌ 可能是 null
    Integer age;      // ❌ 可能是 null
    
    public int getAge() {
        return age != null ? age : 0;  // 到处要判空
    }
}

// 正面例子
public class User {
    String name;           // 初始化为空字符串
    Integer age = 0;      // 初始化为 0
}

字段类型选择 checklist

拿到一个新字段,应该问自己:

1. 取值范围多大?
   └→ 决定用 TINYINT 还是 BIGINT

2. 长度固定吗?
   ├→ 固定 → CHAR
   └→ 不固定 → VARCHAR

3. 需要精确小数吗?
   ├→ 金额/汇率 → DECIMAL
   └→ 科学计算 → FLOAT/DOUBLE

4. 跨时区吗?
   ├→ 是 → TIMESTAMP
   └→ 否 → DATETIME

5. 需要全文搜索吗?
   └→ 用 TEXT + 全文索引

6. 真的需要允许 NULL 吗?
   └→ 能 NOT NULL 就 NOT NULL

实战优化案例

案例 1:手机号存储

sql
-- ❌ 错误:用 VARCHAR(255)
mobile VARCHAR(255)

-- ✅ 正确:用 CHAR(11) 或 VARCHAR(20)
mobile CHAR(11)      -- 国内手机号固定 11 位

-- 如果要存国际号码
mobile VARCHAR(20)   -- 最多 20 位字符

案例 2:IP地址存储

sql
-- ❌ 错误:用 VARCHAR(45)
ip_address VARCHAR(45)

-- ✅ 正确:用 UNSIGNED INT 或 VARBINARY(16)
ip_address INT UNSIGNED

-- 查询时转换
SELECT INET_NTOA(ip_address) FROM users WHERE ip_address = INET_ATON('192.168.1.1');

案例 3:金额存储

sql
-- ❌ 错误:用 DOUBLE
price DOUBLE  -- 精度问题:0.1 + 0.2 = 0.30000000000000004

-- ✅ 正确:用 DECIMAL
price DECIMAL(10, 2)  -- 总共 10 位,小数点后 2 位

-- Java 中用 BigDecimal
private BigDecimal price;  // 不要用 Double!

面试高频追问

Q1:VARCHAR(255) 中的 255 是字节还是字符?

在 MySQL 中,VARCHAR 的长度指定的是字符数,不是字节数。

但实际存储受字符集影响:

sql
-- utf8mb4 字符集
VARCHAR(255)  -- 最多 255 个字符
               -- 但如果都是中文,只能存 63 个(255 * 4 > 65535)

-- latin1 字符集
VARCHAR(255)  -- 最多 255 个字符,255 个字节(一一对应)

Q2:为什么金额要用 DECIMAL 而不是 DOUBLE?

DOUBLE 是浮点数,存在精度丢失问题:

java
// Java 示例
System.out.println(0.1 + 0.2);  // 0.30000000000000004
System.out.println(1.0 - 0.9);   // 0.09999999999999998

// DECIMAL 是定点数,精确存储
// 0.1 + 0.2 = 0.3 ✅

Q3:TEXT 和 VARCHAR 怎么选?

类型最大长度存储位置适用场景
VARCHAR65535 字节行内短文本、可建索引
TEXT65535 字节行外长文本、不建索引
MEDIUMTEXT16MB行外文章内容
LONGTEXT4GB行外大文本

总结

原则说明
够用就好选能容纳值的最小的类型
避免 NULL字段尽量 NOT NULL
固定长度用 CHAR如 MD5、邮政编码
变长用 VARCHAR如用户名、地址
金额用 DECIMAL避免精度问题
时间看时区跨时区用 TIMESTAMP

基于 VitePress 构建