强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

MySQL 完全指南 / 第 6 章:数据类型

第 6 章:数据类型

选对数据类型,是数据库设计的第一步,也是性能优化的基础。


6.1 数值类型

6.1.1 整数类型对比

类型字节有符号范围无符号范围适用场景
TINYINT1-128 ~ 1270 ~ 255状态标记、布尔值
SMALLINT2-32768 ~ 327670 ~ 65535年龄、评分
MEDIUMINT3-8388608 ~ 83886070 ~ 16777215中等整数
INT4-2^31 ~ 2^31-10 ~ 2^32-1普通主键、计数
BIGINT8-2^63 ~ 2^63-10 ~ 2^64-1推荐主键类型
-- 创建表时指定整数类型
CREATE TABLE products (
    id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,  -- 主键推荐 BIGINT UNSIGNED
    price_cents INT UNSIGNED NOT NULL,                       -- 价格用分表示
    stock      INT UNSIGNED DEFAULT 0,                       -- 库存
    status     TINYINT UNSIGNED DEFAULT 1,                   -- 状态
    view_count BIGINT UNSIGNED DEFAULT 0                     -- 访问量
);

6.1.2 INT vs BIGINT 选择指南

场景推荐类型理由
自增主键BIGINT UNSIGNEDINT 最大 ~21亿,不够用会溢出
用户IDBIGINT UNSIGNED亿级用户安全
订单数INT UNSIGNED单表足够
状态/类型TINYINT UNSIGNED0-255 足够
布尔值TINYINT(1)0=false, 1=true

⚠️ INT 溢出风险:INT 最大值约 21 亿。如果日增 100 万行,约 5.8 年就会溢出。新项目统一使用 BIGINT

6.1.3 UNSIGNED 的使用

-- UNSIGNED 将负数范围转为正数,容量翻倍
-- INT:          -2,147,483,648 ~ 2,147,483,647
-- INT UNSIGNED: 0 ~ 4,294,967,295

-- 适用:主键、计数器、金额(用分表示)等不会为负的字段
-- 不适用:温度、盈亏、坐标等可能为负的字段

6.1.4 浮点与定点

-- ❌ 不要用于金额
CREATE TABLE bad_money (amount FLOAT);    -- FLOAT 精度不够
CREATE TABLE bad_money (amount DOUBLE);   -- DOUBLE 精度不够

-- ✅ 使用 DECIMAL
CREATE TABLE accounts (
    balance DECIMAL(12,2) NOT NULL DEFAULT 0.00  -- 整数部分10位,小数2位
);

-- DECIMAL 的存储方式:将数字拆分为整数存储,精确计算
-- DECIMAL(M,D):M=总位数,D=小数位数
-- DECIMAL(12,2):最大 9999999999.99

-- 精确计算示例
SELECT 0.1 + 0.2;                       -- FLOAT: 0.30000000000000004 ❌
SELECT CAST(0.1 AS DECIMAL(5,2)) + CAST(0.2 AS DECIMAL(5,2));  -- 0.30 ✅

💡 金额存储最佳实践

方案示例优点缺点
DECIMAL(12,2)12345.67直观、精确有精度限制
INT 存分1234567无精度问题需要转换显示
BIGINT 存分1234567无精度问题需要转换显示
-- 推荐:用 BIGINT UNSIGNED 存储分为单位的金额
CREATE TABLE payments (
    amount_cents BIGINT UNSIGNED NOT NULL COMMENT '金额(分)'
);
-- 100.50 元存为 10050
-- 显示时:SELECT amount_cents / 100.0 AS amount_yuan FROM payments;

6.2 字符串类型

6.2.1 CHAR vs VARCHAR

维度CHAR(n)VARCHAR(n)
存储方式固定长度可变长度(1-2字节长度前缀 + 数据)
最大长度255 字节65535 字节
空间利用短数据会填充空格按实际长度存储
性能更快(固定偏移)略慢
适用场景定长数据变长数据
-- CHAR 适合定长数据
phone     CHAR(11)         -- 手机号:固定11位
md5_hash  CHAR(32)         -- MD5哈希:固定32位
country   CHAR(2)          -- 国家代码:US, CN

-- VARCHAR 适合变长数据
username  VARCHAR(50)      -- 用户名:长度不定
email     VARCHAR(100)     -- 邮箱:长度不定
address   VARCHAR(500)     -- 地址:长度不定

6.2.2 VARCHAR vs TEXT

维度VARCHAR(n)TEXT
最大长度65535 字节(受行大小限制)65535 字节
默认值可以设不能设默认值
索引前缀索引或完整索引只能前缀索引
存储和其他列一起存储在行中额外存储(off-page)
行大小计入行大小限制不计入行大小限制
推荐短文本(< 1000 字符)长文本(文章、评论)
-- TEXT 类型家族
TINYTEXT    -- 最大 255 字节
TEXT        -- 最大 65535 字节(64KB)
MEDIUMTEXT  -- 最大 16MB
LONGTEXT    -- 最大 4GB

-- 使用建议
summary     TEXT            -- 文章摘要
content     MEDIUMTEXT      -- 文章内容
bio         VARCHAR(500)    -- 个人简介

⚠️ 行大小限制:MySQL 单行数据最大约 65535 字节(不含 BLOB/TEXT 外部存储)。VARCHAR(20000) + VARCHAR(20000) + VARCHAR(20000) 就会超出限制。

6.2.3 BLOB 与二进制数据

-- 通常不建议在数据库中存储文件,应存文件路径
-- 如确需存储二进制数据:
avatar_data  MEDIUMBLOB      -- 头像图片数据
document     LONGBLOB        -- 文档数据

-- 推荐方案:存文件路径
avatar_url   VARCHAR(500)    -- 头像URL,实际文件存在 OSS/S3

6.3 日期时间类型

6.3.1 DATETIME vs TIMESTAMP 详细对比

-- 创建测试表
CREATE TABLE time_test (
    id INT PRIMARY KEY AUTO_INCREMENT,
    dt DATETIME,
    ts TIMESTAMP
);

-- 插入数据
INSERT INTO time_test (dt, ts) VALUES ('2026-05-10 14:30:00', '2026-05-10 14:30:00');

-- 修改时区
SET SESSION time_zone = '+09:00';  -- 切换到 UTC+9(东京)

SELECT * FROM time_test;
-- DATETIME 值不变:2026-05-10 14:30:00
-- TIMESTAMP 值转换:2026-05-10 15:30:00(东京比北京时间快1小时)
维度DATETIMETIMESTAMP
存储字节5 字节 + 小数秒4 字节 + 小数秒
范围1000-01-01 ~ 9999-12-311970-01-01 ~ 2038-01-19
时区不受时区影响自动时区转换
默认值需手动设可设 DEFAULT CURRENT_TIMESTAMP
2038 问题有(32位时间戳溢出)
NULL 存储支持支持
-- 推荐的表设计
CREATE TABLE orders (
    id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_no    VARCHAR(32) NOT NULL,
    created_at  DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at  DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted_at  DATETIME    DEFAULT NULL COMMENT '软删除时间'
);

6.3.2 常见日期时间操作

-- 本周的订单
SELECT * FROM orders 
WHERE YEARWEEK(created_at, 1) = YEARWEEK(CURDATE(), 1);

-- 本月的订单
SELECT * FROM orders 
WHERE YEAR(created_at) = YEAR(CURDATE()) 
  AND MONTH(created_at) = MONTH(CURDATE());

-- 近 30 天的订单
SELECT * FROM orders 
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);

-- 按天统计(最近7天)
SELECT 
    DATE(created_at) AS day,
    COUNT(*) AS order_count
FROM orders
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY DATE(created_at)
ORDER BY day;

-- 按小时统计(今天各时段分布)
SELECT 
    HOUR(created_at) AS hour,
    COUNT(*) AS order_count
FROM orders
WHERE DATE(created_at) = CURDATE()
GROUP BY HOUR(created_at)
ORDER BY hour;

6.4 JSON 类型

MySQL 5.7.8+ 支持原生 JSON 类型。

6.4.1 JSON 基础操作

-- 创建 JSON 字段
CREATE TABLE user_profiles (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    profile JSON,
    settings JSON
);

-- 插入 JSON 数据
INSERT INTO user_profiles (username, profile, settings) VALUES
('zhangsan', 
 '{"age": 25, "city": "Beijing", "hobbies": ["reading", "coding"]}',
 '{"theme": "dark", "language": "zh-CN", "notifications": true}');

-- 查询 JSON 字段
SELECT username, 
       profile->'$.city' AS city,           -- 返回带引号: "Beijing"
       profile->>'$.city' AS city_clean,    -- 返回不带引号: Beijing
       JSON_EXTRACT(profile, '$.hobbies') AS hobbies
FROM user_profiles;

-- JSON 数组查询
SELECT username, profile->>'$.hobbies[0]' AS first_hobby
FROM user_profiles;

6.4.2 JSON 修改操作

-- 添加字段
UPDATE user_profiles 
SET profile = JSON_SET(profile, '$.email', 'zhangsan@example.com')
WHERE username = 'zhangsan';

-- 替换字段(不存在则不操作)
UPDATE user_profiles 
SET profile = JSON_REPLACE(profile, '$.age', 26)
WHERE username = 'zhangsan';

-- 删除字段
UPDATE user_profiles 
SET profile = JSON_REMOVE(profile, '$.hobbies')
WHERE username = 'zhangsan';

-- 数组追加
UPDATE user_profiles 
SET profile = JSON_ARRAY_APPEND(profile, '$.hobbies', 'travel')
WHERE username = 'zhangsan';

-- JSON_MERGE_PATCH:合并 JSON(后者覆盖前者)
SELECT JSON_MERGE_PATCH('{"a":1,"b":2}', '{"b":3,"c":4}');
-- {"a": 1, "b": 3, "c": 4}

-- JSON_MERGE_PRESERVE:合并 JSON(保留所有值,数组合并)
SELECT JSON_MERGE_PRESERVE('{"a":[1]}', '{"a":[2]}');
-- {"a": [1, 2]}

6.4.3 JSON 索引(Generated Column)

-- JSON 字段不能直接建索引,需要通过生成列(Generated Column)
ALTER TABLE user_profiles
ADD COLUMN city VARCHAR(100) GENERATED ALWAYS AS (profile->>'$.city') VIRTUAL,
ADD INDEX idx_city (city);

-- 查询会自动使用索引
SELECT * FROM user_profiles WHERE city = 'Beijing';

6.4.4 JSON 表函数

-- JSON_TABLE:将 JSON 数组展开为行
SELECT jt.*
FROM user_profiles,
JSON_TABLE(profile, '$.hobbies[*]' COLUMNS (
    hobby VARCHAR(50) PATH '$'
)) AS jt
WHERE username = 'zhangsan';
-- 结果:
-- reading
-- coding

⚠️ JSON 使用建议

适合用 JSON不适合用 JSON
半结构化配置数据需要频繁查询/过滤的字段
动态属性(商品扩展属性)需要外键关联的数据
日志/审计详情需要聚合统计的数据
第三方 API 响应有固定模式的核心数据

6.5 ENUM 和 SET

6.5.1 ENUM

-- ENUM:枚举类型,只能取预定义值之一
CREATE TABLE articles (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    status ENUM('draft', 'published', 'archived') DEFAULT 'draft'
);

INSERT INTO articles (title, status) VALUES ('MySQL Guide', 'published');

-- ENUM 内部存储为整数(1, 2, 3...),空间效率高
SELECT status, status + 0 AS internal_value FROM articles;
-- published → 2

⚠️ ENUM 的争议

优点缺点
存储空间小(1-2字节)修改枚举值需要 ALTER TABLE
值约束(数据完整性)与 ORM 框架配合不佳
可读性好新增/删除值需要 DDL 操作

💡 建议:核心业务状态字段优先使用 TINYINT + 应用层常量,灵活且易扩展。

6.5.2 SET

-- SET:集合类型,可以取多个预定义值
CREATE TABLE products (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    features SET('waterproof', 'bluetooth', 'wifi', 'gps', 'fingerprint')
);

INSERT INTO products (name, features) 
VALUES ('Smart Watch', 'waterproof,bluetooth,gps');

-- 查询包含特定特性的产品
SELECT * FROM products WHERE FIND_IN_SET('bluetooth', features) > 0;

6.6 选择数据类型的原则

核心原则

1. 更小的通常更好(在满足需求的前提下选择最小的类型)
2. 简单就好(INT 比 VARCHAR 存 IP 更高效)
3. 尽量避免 NULL(NOT NULL + DEFAULT 值)
4. 精确计算用 DECIMAL(金额等)
5. 主键统一用 BIGINT UNSIGNED AUTO_INCREMENT
6. 时间用 DATETIME(避免 TIMESTAMP 的 2038 问题)
7. 字符集用 utf8mb4

常见字段类型速查

字段推荐类型说明
主键 IDBIGINT UNSIGNED AUTO_INCREMENT通用主键
用户名VARCHAR(50)不超过 50 字符
邮箱VARCHAR(100)标准邮箱长度
手机号CHAR(11)固定 11 位
密码哈希VARCHAR(255)bcrypt/sha256 哈希
金额DECIMAL(12,2) 或 BIGINT 存分精确
状态TINYINT UNSIGNED0-255
IP 地址INT UNSIGNED 或 VARBINARY(16)IPv4 用 INT,IPv6 用 VARBINARY
头像 URLVARCHAR(500)存路径,文件存 OSS
简介VARCHAR(500) 或 TEXT根据长度选择
文章内容MEDIUMTEXT长文本
JSON 配置JSON半结构化数据
创建时间DATETIME DEFAULT CURRENT_TIMESTAMP统一规范
更新时间DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP自动更新
软删除DATETIME DEFAULT NULLNULL 表示未删除

业务场景

场景 1:电商平台商品属性设计

-- 方案 A:JSON(适合属性不确定的商品)
CREATE TABLE products_json (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    attributes JSON COMMENT '商品属性,如颜色、尺寸等'
);

INSERT INTO products_json (name, price, attributes) VALUES
('T恤', 99.00, '{"color":"红色","size":"M","material":"棉"}'),
('手机', 3999.00, '{"brand":"Apple","storage":"128GB","color":"黑色"}');

-- 方案 B:EAV 模式(适合属性非常灵活的场景)
CREATE TABLE product_attributes (
    product_id BIGINT UNSIGNED NOT NULL,
    attr_name VARCHAR(50) NOT NULL,
    attr_value VARCHAR(500),
    PRIMARY KEY (product_id, attr_name)
);

场景 2:地理位置数据

-- 存储坐标
CREATE TABLE locations (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    lat DECIMAL(10,7) NOT NULL COMMENT '纬度',
    lng DECIMAL(10,7) NOT NULL COMMENT '经度',
    SPATIAL INDEX idx_geo (POINT(lat, lng))  -- 空间索引(需要 POINT 类型)
);

-- 使用 POINT 类型(更推荐)
CREATE TABLE locations_geo (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    position POINT NOT NULL SRID 4326,
    SPATIAL INDEX idx_position (position)
);

扩展阅读