引言
提出问题
上一篇我们学会了创建表,但在定义列时遇到了很多数据类型:
- 存储用户ID,用 INT 还是 BIGINT?
- 存储金额,为什么用 DECIMAL 而不是 FLOAT?
- 存储用户名,VARCHAR(50) 和 VARCHAR(255) 有什么区别?
- 存储日期,用 DATE、DATETIME 还是 TIMESTAMP?
- CHAR 和 VARCHAR 怎么选择?
选择错误的数据类型会导致:
- 浪费存储空间(INT能搞定的用BIGINT)
- 性能下降(VARCHAR(255)比VARCHAR(50)慢)
- 精度丢失(FLOAT存金额会有误差)
- 查询出错(日期类型选错导致时区问题)
这篇文章将帮你理解MySQL数据类型的底层原理,学会正确选型!
数据类型分类
MySQL数据类型分为五大类:
| 分类 | 常用类型 | 应用场景 |
|---|---|---|
| 数值类型 | INT, BIGINT, DECIMAL, FLOAT | 年龄、金额、计数器 |
| 字符串类型 | CHAR, VARCHAR, TEXT | 用户名、描述、文章内容 |
| 日期时间类型 | DATE, TIME, DATETIME, TIMESTAMP | 创建时间、生日 |
| 二进制类型 | BLOB, BINARY | 图片、文件(不推荐存数据库) |
| JSON类型 | JSON | 配置项、扩展字段 |
数值类型
整数类型
五种整数类型
| 类型 | 存储空间 | 有符号范围 | 无符号范围 | 典型应用 |
|---|---|---|---|---|
| TINYINT | 1字节 | -128 ~ 127 | 0 ~ 255 | 年龄、状态码 |
| SMALLINT | 2字节 | -32,768 ~ 32,767 | 0 ~ 65,535 | 分类ID |
| MEDIUMINT | 3字节 | -838万 ~ 838万 | 0 ~ 1677万 | 较少使用 |
| INT | 4字节 | -21亿 ~ 21亿 | 0 ~ 42亿 | 用户ID、商品ID |
| BIGINT | 8字节 | -922京 ~ 922京 | 0 ~ 1844京 | 订单ID、大数据 |
实战示例
CREATE TABLE user_profile (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID(无符号,0-42亿)',
age TINYINT UNSIGNED COMMENT '年龄(0-255)',
score SMALLINT DEFAULT 0 COMMENT '积分(-32768~32767)',
view_count BIGINT UNSIGNED DEFAULT 0 COMMENT '浏览量(大数据)'
);
有符号 vs 无符号(UNSIGNED)
-- 有符号(默认)
CREATE TABLE test1 (num INT); -- 范围:-2147483648 ~ 2147483647
-- 无符号(推荐用于ID、计数器)
CREATE TABLE test2 (num INT UNSIGNED); -- 范围:0 ~ 4294967295
选择原则:
- ✅ ID、计数器:用
UNSIGNED(扩大一倍范围) - ❌ 可能为负的数值:不用
UNSIGNED(如温度、余额变化)
选择建议
| 数据 | 推荐类型 | 原因 |
|---|---|---|
| 年龄 | TINYINT UNSIGNED | 0-255够用,节省空间 |
| 性别 | TINYINT | 0男1女2未知 |
| 用户ID | INT UNSIGNED | 42亿够用(单表) |
| 订单ID | BIGINT UNSIGNED | 订单量巨大,用BIGINT |
| 点赞数 | INT UNSIGNED | 42亿够用 |
小数类型
DECIMAL(精确小数,推荐金额)
语法:
DECIMAL(M, D)
-- M: 总位数(最大65)
-- D: 小数位数(最大30)
示例:
CREATE TABLE products (
id INT PRIMARY KEY,
price DECIMAL(10, 2) COMMENT '价格(最大99999999.99)'
);
-- 插入数据
INSERT INTO products VALUES (1, 99.99);
INSERT INTO products VALUES (2, 12345678.99);
优点:
- ✅ 精确存储,不会有误差
- ✅ 适合金额、税率等对精度要求高的数据
缺点:
- ❌ 性能比FLOAT/DOUBLE慢
- ❌ 占用空间较大
FLOAT 和 DOUBLE(浮点数,有误差)
| 类型 | 存储空间 | 精度 | 适用场景 |
|---|---|---|---|
| FLOAT | 4字节 | 约7位有效数字 | 科学计算、近似值 |
| DOUBLE | 8字节 | 约15位有效数字 | 科学计算 |
示例:
CREATE TABLE measurements (
id INT PRIMARY KEY,
temperature FLOAT COMMENT '温度(可以有误差)',
distance DOUBLE COMMENT '距离'
);
-- 插入数据
INSERT INTO measurements VALUES (1, 36.5, 123.456789);
-- 查询(注意精度丢失)
SELECT * FROM measurements;
-- 结果:36.5, 123.45678899999999(有误差!)
⚠️ 重要警告:金额绝对不能用 FLOAT/DOUBLE!
-- ❌ 错误示例:金额用FLOAT
CREATE TABLE orders (
total FLOAT -- 会有精度误差!
);
INSERT INTO orders VALUES (0.1 + 0.2);
SELECT * FROM orders;
-- 结果:0.300000011920929(不是0.3!)
-- ✅ 正确示例:金额用DECIMAL
CREATE TABLE orders (
total DECIMAL(10, 2)
);
INSERT INTO orders VALUES (0.1 + 0.2);
SELECT * FROM orders;
-- 结果:0.30(精确!)
选择建议
| 数据类型 | 推荐类型 | 原因 |
|---|---|---|
| 金额 | DECIMAL(10, 2) | 必须精确,不能有误差 |
| 比率 | DECIMAL(5, 2) | 如:税率15.5% |
| 温度 | FLOAT | 可以有误差 |
| 距离 | DOUBLE | 需要高精度但允许误差 |
字符串类型
CHAR 和 VARCHAR
对比
| 特性 | CHAR(N) | VARCHAR(N) |
|---|---|---|
| 长度 | 固定长度 | 可变长度 |
| 存储方式 | 不足N位用空格填充 | 实际长度存储 |
| 存储空间 | N字节 | 实际长度 + 1-2字节 |
| 性能 | 查询快(定长) | 稍慢(变长) |
| 适用场景 | 固定长度的字符串 | 长度变化的字符串 |
实战示例
CREATE TABLE test_char_varchar (
-- CHAR:固定长度
country_code CHAR(2) COMMENT '国家码(CN、US)',
id_card CHAR(18) COMMENT '身份证号(固定18位)',
-- VARCHAR:可变长度
username VARCHAR(50) COMMENT '用户名(长度不固定)',
email VARCHAR(100) COMMENT '邮箱',
description VARCHAR(500) COMMENT '描述'
);
存储空间对比
-- CHAR(10) 存储 "abc"
-- 实际存储:"abc "(10字节,后面填充空格)
-- VARCHAR(10) 存储 "abc"
-- 实际存储:"abc"(3字节 + 1字节长度信息 = 4字节)
选择建议
| 数据 | 推荐类型 | 原因 |
|---|---|---|
| 国家码 | CHAR(2) | 固定2位(CN、US) |
| 邮编 | CHAR(6) | 固定6位 |
| 身份证号 | CHAR(18) | 固定18位 |
| 用户名 | VARCHAR(50) | 长度不固定 |
| 邮箱 | VARCHAR(100) | 长度不固定 |
| 文章标题 | VARCHAR(200) | 长度不固定 |
TEXT 类型
四种TEXT类型
| 类型 | 最大长度 | 存储空间 | 适用场景 |
|---|---|---|---|
| TINYTEXT | 255字节 | 实际长度 + 1字节 | 很少用 |
| TEXT | 65,535字节(64KB) | 实际长度 + 2字节 | 文章摘要 |
| MEDIUMTEXT | 16,777,215字节(16MB) | 实际长度 + 3字节 | 文章内容 |
| LONGTEXT | 4,294,967,295字节(4GB) | 实际长度 + 4字节 | 长文本 |
实战示例
CREATE TABLE articles (
id INT PRIMARY KEY,
title VARCHAR(200) COMMENT '标题',
summary TEXT COMMENT '摘要(64KB以内)',
content MEDIUMTEXT COMMENT '内容(16MB以内)'
);
TEXT vs VARCHAR
| 特性 | TEXT | VARCHAR |
|---|---|---|
| 最大长度 | 64KB(TEXT) | 65,535字节 |
| 索引 | 需指定前缀长度 | 可全字段索引 |
| 排序 | 较慢 | 较快 |
| 默认值 | 不支持 | 支持 |
选择建议:
- ✅ 长度 < 500:用
VARCHAR(500) - ✅ 长度 > 500:用
TEXT - ✅ 需要创建索引:优先
VARCHAR
ENUM 和 SET(枚举类型)
ENUM(单选)
CREATE TABLE users (
id INT PRIMARY KEY,
gender ENUM('male', 'female', 'other') DEFAULT 'other',
status ENUM('active', 'inactive', 'banned') DEFAULT 'active'
);
-- 插入数据
INSERT INTO users VALUES (1, 'male', 'active');
INSERT INTO users VALUES (2, 'female', 'inactive');
-- ❌ 错误:值不在枚举中
INSERT INTO users VALUES (3, 'unknown', 'active');
-- ERROR 1265: Data truncated for column 'gender'
优点:
- ✅ 存储空间小(1-2字节)
- ✅ 值只能从预定义列表选择
- ✅ 可读性好
缺点:
- ❌ 修改枚举值需要ALTER TABLE
SET(多选)
CREATE TABLE user_permissions (
id INT PRIMARY KEY,
permissions SET('read', 'write', 'delete', 'admin')
);
-- 插入数据(可以多选)
INSERT INTO user_permissions VALUES (1, 'read,write');
INSERT INTO user_permissions VALUES (2, 'read,write,delete');
INSERT INTO user_permissions VALUES (3, 'admin'); -- 管理员拥有所有权限
-- 查询拥有write权限的用户
SELECT * FROM user_permissions WHERE FIND_IN_SET('write', permissions);
日期时间类型
五种日期时间类型
| 类型 | 格式 | 范围 | 存储空间 | 时区 | 适用场景 |
|---|---|---|---|---|---|
| DATE | YYYY-MM-DD | 1000-01-01 ~ 9999-12-31 | 3字节 | 无 | 生日、日期 |
| TIME | HH:MM:SS | -838:59:59 ~ 838:59:59 | 3字节 | 无 | 营业时间 |
| DATETIME | YYYY-MM-DD HH:MM:SS | 1000-01-01 ~ 9999-12-31 | 8字节 | 无 | 订单时间 |
| TIMESTAMP | YYYY-MM-DD HH:MM:SS | 1970-01-01 ~ 2038-01-19 | 4字节 | 有 | 创建时间 |
| YEAR | YYYY | 1901 ~ 2155 | 1字节 | 无 | 毕业年份 |
DATETIME vs TIMESTAMP
关键区别
| 特性 | DATETIME | TIMESTAMP |
|---|---|---|
| 时区 | 不受时区影响 | 受时区影响 |
| 范围 | 1000年-9999年 | 1970年-2038年 |
| 存储空间 | 8字节 | 4字节 |
| 自动更新 | 需手动 | 支持自动更新 |
实战示例
CREATE TABLE events (
id INT PRIMARY KEY,
-- DATETIME:不受时区影响
event_time DATETIME COMMENT '事件时间(不受时区影响)',
-- TIMESTAMP:受时区影响,自动更新
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
);
-- 插入数据
INSERT INTO events (id, event_time) VALUES (1, '2024-01-15 10:30:00');
-- 查询
SELECT * FROM events;
-- created_at 和 updated_at 自动填充为当前时间
时区问题演示
-- 查看当前时区
SHOW VARIABLES LIKE 'time_zone';
-- 输出:+08:00(东八区)
-- 插入数据
INSERT INTO time_test VALUES (1, '2024-01-15 10:00:00', '2024-01-15 10:00:00');
-- 切换时区到UTC
SET time_zone = '+00:00';
-- 再次查询
SELECT * FROM time_test;
-- DATETIME:2024-01-15 10:00:00(不变)
-- TIMESTAMP:2024-01-15 02:00:00(变了!比原来早8小时)
选择建议
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 创建时间 | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 自动填充,节省空间 |
| 更新时间 | TIMESTAMP ... ON UPDATE CURRENT_TIMESTAMP | 自动更新 |
| 订单时间 | DATETIME | 不受时区影响,范围更大 |
| 生日 | DATE | 只需要日期 |
| 预约时间 | DATETIME | 需要精确到秒 |
JSON类型(MySQL 5.7.8+)
基础用法
CREATE TABLE user_settings (
user_id INT PRIMARY KEY,
settings JSON COMMENT '用户配置(JSON格式)'
);
-- 插入JSON数据
INSERT INTO user_settings VALUES (1, '{
"theme": "dark",
"language": "zh-CN",
"notifications": {
"email": true,
"sms": false
}
}');
-- 查询JSON字段
SELECT
user_id,
JSON_EXTRACT(settings, '$.theme') AS theme,
JSON_EXTRACT(settings, '$.language') AS language
FROM user_settings;
-- 简写方式(->)
SELECT
user_id,
settings->'$.theme' AS theme,
settings->>'$.language' AS language -- ->> 去掉引号
FROM user_settings;
JSON函数
-- JSON_OBJECT:创建JSON对象
SELECT JSON_OBJECT('name', 'Alice', 'age', 25);
-- 结果:{"name": "Alice", "age": 25}
-- JSON_ARRAY:创建JSON数组
SELECT JSON_ARRAY(1, 2, 3, 'a', 'b');
-- 结果:[1, 2, 3, "a", "b"]
-- JSON_SET:设置JSON值
UPDATE user_settings
SET settings = JSON_SET(settings, '$.theme', 'light')
WHERE user_id = 1;
-- JSON_SEARCH:搜索JSON
SELECT JSON_SEARCH(settings, 'one', 'zh-CN') FROM user_settings;
适用场景
| 场景 | 是否适合JSON |
|---|---|
| ✅ 配置项 | 适合(灵活、无需建表) |
| ✅ 扩展字段 | 适合(不固定的属性) |
| ✅ 日志数据 | 适合(结构不固定) |
| ❌ 频繁查询的字段 | 不适合(性能差) |
| ❌ 需要索引的字段 | 不适合(JSON索引受限) |
实战案例:电商商品表设计
CREATE TABLE products (
-- 主键(自增)
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
-- 字符串类型
name VARCHAR(200) NOT NULL COMMENT '商品名称',
code CHAR(20) NOT NULL UNIQUE COMMENT '商品编码(固定格式)',
category VARCHAR(50) COMMENT '分类',
description TEXT COMMENT '商品描述',
-- 数值类型
price DECIMAL(10, 2) NOT NULL COMMENT '价格(精确到分)',
cost DECIMAL(10, 2) COMMENT '成本',
stock INT UNSIGNED DEFAULT 0 COMMENT '库存',
sales INT UNSIGNED DEFAULT 0 COMMENT '销量',
rating DECIMAL(3, 2) DEFAULT 5.00 COMMENT '评分(0.00-5.00)',
-- 枚举类型
status ENUM('on_sale', 'off_sale', 'out_of_stock') DEFAULT 'on_sale' COMMENT '状态',
-- 日期时间类型
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
published_at DATETIME COMMENT '上架时间',
-- JSON类型(扩展属性)
attributes JSON COMMENT '商品属性(颜色、尺码等)',
-- 索引
INDEX idx_name (name),
INDEX idx_category (category),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-- 插入示例数据
INSERT INTO products (name, code, category, price, cost, stock, attributes) VALUES
('iPhone 15 Pro', 'APPLE-IP15P-256', '手机', 7999.00, 5000.00, 100, '{
"color": ["黑色", "白色", "蓝色"],
"storage": ["128GB", "256GB", "512GB"],
"warranty": "1年"
}');
最佳实践
数据类型选择原则
够用就好:
- ✅ 用户ID:
INT UNSIGNED(42亿够用) - ❌ 不要用:
BIGINT(浪费4字节)
- ✅ 用户ID:
优先选择小类型:
- ✅ 年龄:
TINYINT UNSIGNED - ❌ 不要用:
INT(浪费3字节)
- ✅ 年龄:
定长优于变长(相同情况下):
- ✅ 国家码:
CHAR(2) - ❌ 不要用:
VARCHAR(2)
- ✅ 国家码:
精确计算用DECIMAL:
- ✅ 金额:
DECIMAL(10, 2) - ❌ 不要用:
FLOAT(有误差)
- ✅ 金额:
合理使用TEXT:
- ✅ 文章内容:
MEDIUMTEXT - ❌ 不要用:
VARCHAR(65535)(无法索引)
- ✅ 文章内容:
常见错误
错误1:滥用VARCHAR(255)
-- ❌ 错误
username VARCHAR(255) -- 用户名不需要这么长
-- ✅ 正确
username VARCHAR(50) -- 50个字符够用
错误2:金额用FLOAT
-- ❌ 错误
price FLOAT -- 会有精度误差!
-- ✅ 正确
price DECIMAL(10, 2) -- 精确到分
错误3:日期时间类型选错
-- ❌ 错误:生日用TIMESTAMP
birth_date TIMESTAMP -- 范围只到2038年
-- ✅ 正确:生日用DATE
birth_date DATE -- 范围到9999年
总结
核心要点
数值类型:
- 整数:TINYINT < SMALLINT < INT < BIGINT
- 小数:金额用DECIMAL,科学计算用FLOAT/DOUBLE
- 选择原则:够用就好,优先小类型
字符串类型:
- 固定长度用CHAR,可变长度用VARCHAR
- 长文本用TEXT
- 枚举值用ENUM
日期时间类型:
- 只需日期:DATE
- 需要时间:DATETIME(不受时区影响)
- 自动更新:TIMESTAMP
JSON类型:
- 适合配置项、扩展字段
- 不适合频繁查询的核心字段
记忆口诀
数据类型选择三原则:小、准、快
- 小:够用就好,优先选小类型(节省空间)
- 准:金额用DECIMAL,不用FLOAT(精确计算)
- 快:定长优于变长,合理创建索引(提升性能)
实践建议
参考表设计:
- 查看成熟项目的表结构
- MySQL官方示例数据库(sakila)
性能测试:
- 不同数据类型的存储空间对比
- 查询性能对比(VARCHAR(50) vs VARCHAR(255))
避免过度设计:
- 不要一开始就用BIGINT(除非确实需要)
- 不要所有字符串都VARCHAR(255)
下一步学习
- 前置知识:上一篇《数据库与表的创建:DDL基础》
- 后续推荐:下一篇《基础增删改查:DML操作入门》
- 实战项目:设计一个完整的用户系统表(合理选择数据类型)
参考资料
系列文章导航:
- 上一篇:《数据库与表的创建:DDL基础》
- 下一篇:《基础增删改查:DML操作入门》
- 返回目录:MySQL从入门到精通
💡 提示:本文是 “MySQL从入门到精通” 系列的第 5 篇(共86篇),从第一性原理出发,系统化掌握MySQL。
📚 学习建议:建议动手创建表,尝试不同的数据类型,观察存储空间和查询性能的差异。
🤝 交流讨论:如有问题或建议,欢迎在评论区留言交流。