Skip to content

MySQL字段类型与设计规范

字符串类型选择:CHAR vs VARCHAR

在 MySQL 中,存储字符串数据主要使用 CHARVARCHAR 两种类型。它们在存储方式、性能表现和适用场景上存在显著差异。

核心区别

CHAR 是定长字符类型,存储时会自动在末尾填充空格至指定长度。例如 CHAR(50) 定义的字段,无论实际存储多少字符,都会占用 50 个字符的存储空间。

VARCHAR 是变长字符类型,只存储实际的字符内容,不会填充空格。VARCHAR(50) 存储 10 个字符时,实际只占用 10 个字符的空间(加上 1-2 字节的长度信息)。

mermaid
graph TB
    subgraph "CHAR(20) 存储'MySQL'"
        C1([实际内容: MySQL])
        C2([填充: 15个空格])
        C3([总占用: 20字符])
        C1 --> C2 --> C3
    end
    
    subgraph "VARCHAR(20) 存储'MySQL'"
        V1([实际内容: MySQL])
        V2([长度标识: 1字节])
        V3([总占用: 5字符+1字节])
        V1 --> V2 --> V3
    end
    
    classDef charStyle fill:#E85D75,stroke:none,color:#fff
    classDef varcharStyle fill:#50C878,stroke:none,color:#fff
    
    class C1,C2,C3 charStyle
    class V1,V2,V3 varcharStyle

VARCHAR 的特性与权衡

优势:

  1. 节省存储空间:变长存储,实际占用空间更小
  2. 兼容性好:适应不同长度的字符串数据

劣势:

  1. 需要额外开销:1-2 字节用于存储长度信息(长度≤255 用 1 字节,否则 2 字节)
  2. 可能产生内存碎片:频繁的更新操作可能导致存储碎片
  3. 可能触发页分裂:当字符串变长时,原位置无法容纳新数据

页分裂示例:

sql
-- 商品评论表
CREATE TABLE product_reviews (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_code VARCHAR(20),
    reviewer_name VARCHAR(50),
    comment VARCHAR(500)  -- 初始短评论,后续可能追加内容
);

-- 初始插入短评论
INSERT INTO product_reviews VALUES (1, 'SKU-2024-001', '张三', '商品不错');

-- 后续用户追加大量内容,原位置无法存储
UPDATE product_reviews SET comment = '商品不错,经过一个月使用...(此处省略400字)' 
WHERE id = 1;  -- 可能触发页分裂

CHAR 的特性与权衡

优势:

  1. 避免内存碎片:定长存储,空间固定
  2. 无需额外长度字节:节省长度信息开销
  3. 避免页分裂:更新不会改变占用空间

劣势:

  1. 浪费存储空间:短字符串也占用全部空间
  2. 丢失末尾空格:存储 "MySQL " 会丢失末尾空格信息
sql
-- 国家代码表(固定2位)
CREATE TABLE countries (
    code CHAR(2) PRIMARY KEY,  -- 'CN', 'US', 'JP' 固定长度
    name VARCHAR(100)
);

-- 订单流水号(固定20位)
CREATE TABLE order_records (
    order_no CHAR(20) PRIMARY KEY,  -- 'ORD20241120000001' 固定格式
    amount DECIMAL(10, 2)
);

VARCHAR 长度定义的影响

虽然 VARCHAR(10)VARCHAR(100) 在存储相同内容时占用空间相同,但在某些场景下仍有差异:

排序操作的内存开销:

MySQL 执行 ORDER BY 时,会在 sort_buffer 中为每行预留空间。VARCHAR 字段会按照定义的最大长度预留内存:

sql
-- 场景1: VARCHAR(10)
SELECT * FROM users ORDER BY nickname;  
-- sort_buffer 为每个 nickname 预留 10字符 + 长度字节

-- 场景2: VARCHAR(100)  
SELECT * FROM users ORDER BY nickname;
-- sort_buffer 为每个 nickname 预留 100字符 + 长度字节

影响:

  • 当单行长度超过 max_length_for_sort_data,会触发 rowid 排序(需要回表)
  • 当数据量超过 sort_buffer_size,会使用磁盘临时表(性能下降)
mermaid
graph TB
    Sort([ORDER BY varchar字段]) --> Size{单行长度判断}
    Size -->|小于max_length_for_sort_data| Full([全字段排序<br/>性能较好])
    Size -->|大于max_length_for_sort_data| Rowid([rowid排序<br/>需要回表])
    
    Full --> Buffer{数据量判断}
    Rowid --> Buffer
    Buffer -->|小于sort_buffer_size| Mem([内存排序<br/>速度快])
    Buffer -->|大于sort_buffer_size| Disk([磁盘临时表排序<br/>速度慢、IO大])
    
    classDef processStyle fill:#4A90E2,stroke:none,color:#fff
    classDef goodStyle fill:#50C878,stroke:none,color:#fff
    classDef badStyle fill:#E85D75,stroke:none,color:#fff
    
    class Sort,Size,Buffer processStyle
    class Full,Mem goodStyle
    class Rowid,Disk badStyle

建议:根据实际需要定义 VARCHAR 长度,避免过度设计。

大数据类型:BLOB vs TEXT

BLOB 和 TEXT 都用于存储大量数据,但用途和特性不同。

核心区别

特性BLOBTEXT
存储内容二进制数据(图片、音频、视频)文本数据(文章、评论、描述)
字符集处理不进行字符集转换支持字符集转换
排序支持不支持排序支持排序

类型变种与容量

两者都提供了多个容量级别的变种:

mermaid
graph LR
    subgraph "BLOB系列"
        B1([TINYBLOB<br/>255 B])
        B2([BLOB<br/>64 KB])
        B3([MEDIUMBLOB<br/>16 MB])
        B4([LONGBLOB<br/>4 GB])
        B1 --> B2 --> B3 --> B4
    end
    
    subgraph "TEXT系列"
        T1([TINYTEXT<br/>255 B])
        T2([TEXT<br/>64 KB])
        T3([MEDIUMTEXT<br/>16 MB])
        T4([LONGTEXT<br/>4 GB])
        T1 --> T2 --> T3 --> T4
    end
    
    classDef blobStyle fill:#9B59B6,stroke:none,color:#fff
    classDef textStyle fill:#3498DB,stroke:none,color:#fff
    
    class B1,B2,B3,B4 blobStyle
    class T1,T2,T3,T4 textStyle

使用场景与限制

BLOB 典型场景(不推荐直接存储):

sql
-- 文件元数据表(推荐只存路径)
CREATE TABLE file_metadata (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    file_name VARCHAR(255),
    file_path VARCHAR(500),  -- 存储文件在OSS的URL
    file_size BIGINT,
    upload_time DATETIME
);

-- 不推荐:直接存储二进制内容
-- file_data BLOB  -- 影响数据库性能

TEXT 典型场景:

sql
-- 博客文章表
CREATE TABLE blog_posts (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200),
    author VARCHAR(50),
    content MEDIUMTEXT,  -- 存储文章正文
    created_at DATETIME
);

-- 商品详情表
CREATE TABLE product_details (
    product_id BIGINT PRIMARY KEY,
    basic_info VARCHAR(500),  -- 基本信息
    full_description TEXT     -- 详细描述
);

性能注意事项:

  1. 内存临时表不支持:包含 BLOB/TEXT 字段的查询无法使用内存临时表,必须使用磁盘临时表
  2. 只能使用前缀索引:MySQL 对索引字段长度有限制
  3. 不能有默认值:TEXT 列不支持默认值设置
sql
-- 为 TEXT 字段创建前缀索引
CREATE INDEX idx_content_prefix ON blog_posts(content(100));

-- 查询时避免 SELECT *
SELECT id, title, author FROM blog_posts WHERE id = 1;  -- 推荐
-- SELECT * FROM blog_posts WHERE id = 1;  -- 不推荐,会查询大字段

最佳实践:

  • 将 BLOB/TEXT 字段分离到扩展表
  • 查询时明确指定字段,避免 SELECT *
  • 二进制文件存储到对象存储服务(OSS/S3),数据库只存 URL
sql
-- 主表:常用字段
CREATE TABLE articles (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200),
    author VARCHAR(50),
    summary VARCHAR(500),
    created_at DATETIME
);

-- 扩展表:大字段
CREATE TABLE article_contents (
    article_id BIGINT PRIMARY KEY,
    full_content MEDIUMTEXT,
    FOREIGN KEY (article_id) REFERENCES articles(id)
);

其他字段类型设计建议

避免使用 ENUM 类型

虽然 ENUM 看似方便,但存在诸多限制:

sql
-- 不推荐:使用 ENUM
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    status ENUM('pending', 'paid', 'shipped', 'completed')
);

-- 修改 ENUM 值需要 ALTER TABLE(锁表)
ALTER TABLE orders MODIFY status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled');

推荐方案:使用 TINYINT + 常量定义

sql
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    status TINYINT NOT NULL COMMENT '0-待付款 1-已付款 2-已发货 3-已完成 4-已取消'
);

-- 应用层定义常量
public class OrderStatus {
    public static final int PENDING = 0;
    public static final int PAID = 1;
    public static final int SHIPPED = 2;
    public static final int COMPLETED = 3;
    public static final int CANCELLED = 4;
}

优先使用 NOT NULL

除非有特殊理由,否则字段应定义为 NOT NULL:

sql
-- 用户信息表
CREATE TABLE user_profiles (
    user_id BIGINT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    phone VARCHAR(20) NOT NULL DEFAULT '',  -- 用空串代替 NULL
    nickname VARCHAR(50) NOT NULL DEFAULT '',
    bio VARCHAR(500) NOT NULL DEFAULT '',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

原因:

  1. NULL 需要额外存储空间标识
  2. NULL 参与比较和计算需要特殊处理
  3. 索引 NULL 列需要额外空间

选择合适的数值类型

sql
-- 用户表
CREATE TABLE users (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,  -- 非负整数用 UNSIGNED
    age TINYINT UNSIGNED,      -- 年龄 0-255
    status TINYINT,            -- 状态标识
    login_count INT UNSIGNED,  -- 登录次数
    balance DECIMAL(10, 2)     -- 余额,使用精确小数
);

-- IP地址存储优化
CREATE TABLE access_logs (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    ip_address INT UNSIGNED,  -- 使用 INET_ATON() 转换
    access_time DATETIME
);

-- 插入数据
INSERT INTO access_logs (ip_address, access_time) 
VALUES (INET_ATON('192.168.1.100'), NOW());

-- 查询数据
SELECT id, INET_NTOA(ip_address) AS ip, access_time 
FROM access_logs;

时间类型选择

sql
-- 订单表
CREATE TABLE order_records (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(32) NOT NULL,
    created_at DATETIME NOT NULL,      -- 不需要时区,范围大
    paid_at TIMESTAMP NULL,            -- 需要时区转换
    amount DECIMAL(10, 2) NOT NULL
);

类型对比:

类型存储空间范围时区支持推荐场景
DATETIME5-8字节1000-9999年业务时间,不需要时区转换
TIMESTAMP4-7字节1970-2038年需要时区转换的场景
INT4字节时间戳高性能场景,应用层转换

总结

字段类型选择直接影响数据库的性能和存储效率:

  1. 字符串类型:
    • 定长数据用 CHAR(国家代码、订单号)
    • 变长数据用 VARCHAR(姓名、地址)
    • VARCHAR 长度按需定义,避免过度设计
  2. 大字段类型:
    • 二进制数据推荐存储到对象存储,数据库只存 URL
    • TEXT 字段分离到扩展表
    • 避免 SELECT *,明确指定字段
  3. 通用原则:
    • 优先使用 NOT NULL
    • 非负数使用 UNSIGNED
    • 金额使用 DECIMAL
    • IP 地址转换为 INT
    • 避免使用 ENUM

合理的字段类型设计能够有效提升数据库性能,减少存储开销,降低维护成本。

更新: 2025-12-04 17:38:06
原文: https://www.yuque.com/u22210564/zoxfmt/doc-01-mysql-15-mysql-02

Java 后端面试知识库