引言

提出问题

上一篇我们学会了创建表,但在定义列时遇到了很多数据类型:

  • 存储用户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配置项、扩展字段

数值类型

整数类型

五种整数类型

类型存储空间有符号范围无符号范围典型应用
TINYINT1字节-128 ~ 1270 ~ 255年龄、状态码
SMALLINT2字节-32,768 ~ 32,7670 ~ 65,535分类ID
MEDIUMINT3字节-838万 ~ 838万0 ~ 1677万较少使用
INT4字节-21亿 ~ 21亿0 ~ 42亿用户ID、商品ID
BIGINT8字节-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 UNSIGNED0-255够用,节省空间
性别TINYINT0男1女2未知
用户IDINT UNSIGNED42亿够用(单表)
订单IDBIGINT UNSIGNED订单量巨大,用BIGINT
点赞数INT UNSIGNED42亿够用

小数类型

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(浮点数,有误差)

类型存储空间精度适用场景
FLOAT4字节约7位有效数字科学计算、近似值
DOUBLE8字节约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类型

类型最大长度存储空间适用场景
TINYTEXT255字节实际长度 + 1字节很少用
TEXT65,535字节(64KB)实际长度 + 2字节文章摘要
MEDIUMTEXT16,777,215字节(16MB)实际长度 + 3字节文章内容
LONGTEXT4,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

特性TEXTVARCHAR
最大长度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);

日期时间类型

五种日期时间类型

类型格式范围存储空间时区适用场景
DATEYYYY-MM-DD1000-01-01 ~ 9999-12-313字节生日、日期
TIMEHH:MM:SS-838:59:59 ~ 838:59:593字节营业时间
DATETIMEYYYY-MM-DD HH:MM:SS1000-01-01 ~ 9999-12-318字节订单时间
TIMESTAMPYYYY-MM-DD HH:MM:SS1970-01-01 ~ 2038-01-194字节创建时间
YEARYYYY1901 ~ 21551字节毕业年份

DATETIME vs TIMESTAMP

关键区别

特性DATETIMETIMESTAMP
时区不受时区影响受时区影响
范围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年"
}');

最佳实践

数据类型选择原则

  1. 够用就好

    • ✅ 用户ID:INT UNSIGNED(42亿够用)
    • ❌ 不要用:BIGINT(浪费4字节)
  2. 优先选择小类型

    • ✅ 年龄:TINYINT UNSIGNED
    • ❌ 不要用:INT(浪费3字节)
  3. 定长优于变长(相同情况下):

    • ✅ 国家码:CHAR(2)
    • ❌ 不要用:VARCHAR(2)
  4. 精确计算用DECIMAL

    • ✅ 金额:DECIMAL(10, 2)
    • ❌ 不要用:FLOAT(有误差)
  5. 合理使用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年

总结

核心要点

  1. 数值类型

    • 整数:TINYINT < SMALLINT < INT < BIGINT
    • 小数:金额用DECIMAL,科学计算用FLOAT/DOUBLE
    • 选择原则:够用就好,优先小类型
  2. 字符串类型

    • 固定长度用CHAR,可变长度用VARCHAR
    • 长文本用TEXT
    • 枚举值用ENUM
  3. 日期时间类型

    • 只需日期:DATE
    • 需要时间:DATETIME(不受时区影响)
    • 自动更新:TIMESTAMP
  4. JSON类型

    • 适合配置项、扩展字段
    • 不适合频繁查询的核心字段

记忆口诀

数据类型选择三原则:小、准、快

  • :够用就好,优先选小类型(节省空间)
  • :金额用DECIMAL,不用FLOAT(精确计算)
  • :定长优于变长,合理创建索引(提升性能)

实践建议

  1. 参考表设计

    • 查看成熟项目的表结构
    • MySQL官方示例数据库(sakila)
  2. 性能测试

    • 不同数据类型的存储空间对比
    • 查询性能对比(VARCHAR(50) vs VARCHAR(255))
  3. 避免过度设计

    • 不要一开始就用BIGINT(除非确实需要)
    • 不要所有字符串都VARCHAR(255)

下一步学习

  • 前置知识:上一篇《数据库与表的创建:DDL基础》
  • 后续推荐:下一篇《基础增删改查:DML操作入门》
  • 实战项目:设计一个完整的用户系统表(合理选择数据类型)

参考资料

  1. MySQL官方文档 - 数据类型
  2. MySQL官方文档 - 数值类型
  3. MySQL官方文档 - 字符串类型
  4. MySQL官方文档 - 日期时间类型

系列文章导航

  • 上一篇:《数据库与表的创建:DDL基础》
  • 下一篇:《基础增删改查:DML操作入门》
  • 返回目录:MySQL从入门到精通

💡 提示:本文是 “MySQL从入门到精通” 系列的第 5 篇(共86篇),从第一性原理出发,系统化掌握MySQL。

📚 学习建议:建议动手创建表,尝试不同的数据类型,观察存储空间和查询性能的差异。

🤝 交流讨论:如有问题或建议,欢迎在评论区留言交流。