为什么数据类型选型如此重要
在MySQL数据库设计中,数据类型的选择往往被开发者视为一项”小事”——随便选个INT或VARCHAR似乎就能跑起来。然而,数据类型不仅决定了磁盘空间的占用,还直接影响内存利用率、索引效率、查询性能乃至数据完整性。一个不恰当的类型选择,可能让你的表多占3倍存储,让索引扫描慢5倍,甚至导致隐式类型转换使索引完全失效。
本文将从InnoDB存储引擎的底层实现出发,系统梳理MySQL 8.0中各类数据类型的存储机制、性能特征和选型策略,结合大量实战案例,帮助你在架构设计阶段做出最优决策。

数值类型:存储边界与精度陷阱
整数类型的存储开销与选择
MySQL提供了TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT五种整数类型,它们在磁盘和内存中的占用差异显著:
| 类型 | 字节数 | 有符号范围 | 无符号范围 |
|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 0 ~ 255 |
| SMALLINT | 2 | -32768 ~ 32767 | 0 ~ 65535 |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 0 ~ 16777215 |
| INT | 4 | -2^31 ~ 2^31-1 | 0 ~ 4294967295 |
| BIGINT | 8 | -2^63 ~ 2^63-1 | 0 ~ 2^64-1 |
一个常见的误区是给INT加显示宽度,如
|
1
|
INT(11)
|
。在MySQL 8.0中,整数类型的显示宽度已被废弃(8.0.17+会给出deprecation warning),它不影响存储范围,仅在与
|
1
|
ZEROFILL
|
(也已废弃)搭配时有意义。选型时应纯粹根据数据范围决定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 -- 不推荐:显示宽度无实际意义
CREATE TABLE users (
id INT(11) NOT NULL AUTO_INCREMENT,
age TINYINT(3),
PRIMARY KEY (id)
);
-- 推荐:不加显示宽度,根据实际范围选类型
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
age TINYINT UNSIGNED NOT NULL COMMENT '0-255足够表示年龄',
status TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '状态码0-99',
PRIMARY KEY (id)
);
UNSIGNED的正确使用场景
UNSIGNED修饰符能让整数类型的正数范围翻倍。对于主键、状态码、计数器等不可能为负的场景,UNSIGNED是理想选择。但要注意:UNSIGNED与SIGNED列之间的比较会导致隐式类型转换,可能使索引失效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 -- 陷阱:UNSIGNED与SIGNED比较导致索引失效
CREATE TABLE orders (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
PRIMARY KEY (id),
KEY idx_user (user_id)
);
-- 这个查询能用索引吗?取决于参数类型
-- 如果应用层传入的参数是SIGNED INT,可能触发隐式转换
EXPLAIN SELECT * FROM orders WHERE id = 1000;
-- 结果:type=const,走主键索引,没问题
-- 但在JOIN中要小心
CREATE TABLE legacy_users (
id INT NOT NULL AUTO_INCREMENT, -- SIGNED!
PRIMARY KEY (id)
);
-- UNSIGNED = SIGNED 的JOIN条件可能导致范围扫描而非等值查找
EXPLAIN SELECT * FROM orders o
JOIN legacy_users u ON o.user_id = u.id
WHERE u.id = 100;
DECIMAL vs FLOAT/DOUBLE:精度的代价
金融、库存等需要精确计算的场景必须使用DECIMAL。FLOAT/DOUBLE使用IEEE 754浮点格式,存在精度丢失:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 -- 浮点精度陷阱演示
SELECT 0.1 + 0.2 = 0.3; -- 结果:0(FALSE!)
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2)) = 0.3; -- 结果:1
-- DECIMAL存储:每9位数字占4字节,整数和小数部分分别计算
-- DECIMAL(18,2):整数部分16位(18-2)->2x4=8字节,小数部分2位->1字节,共9字节
-- DECIMAL(65,30):最大支持,约29字节
CREATE TABLE financial_records (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
amount DECIMAL(15,2) NOT NULL COMMENT '金额,15位总精度2位小数',
exchange_rate DECIMAL(12,8) NOT NULL COMMENT '汇率,8位小数精度',
quantity INT UNSIGNED NOT NULL,
PRIMARY KEY (id)
);
-- 永远不要用FLOAT存金额
-- 错误示例:FLOAT累计100万行后偏差可达数元
SELECT SUM(amount) FROM financial_records; -- DECIMAL: 精确
-- 同样数据如果用FLOAT:结果可能差0.01~0.05
DECIMAL的代价是计算速度比FLOAT慢约3-5倍,且占用更多存储。在科学计算、统计分析等容忍微量误差的场景,FLOAT/DOUBLE更高效。
字符串类型:VARCHAR vs CHAR与隐式转换黑洞
VARCHAR与CHAR的存储机制差异
VARCHAR使用变长存储,实际占用=数据长度+1~2字节长度前缀;CHAR使用定长存储,不足部分用空格填充。在InnoDB中,两者的选择策略如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 -- 定长且短:用CHAR(避免VARCHAR的长度前缀开销和行迁移)
CREATE TABLE config (
config_key CHAR(32) NOT NULL COMMENT 'MD5哈希键,固定32字符',
config_value VARCHAR(500) NOT NULL,
PRIMARY KEY (config_key)
);
-- 变长或较长:用VARCHAR
CREATE TABLE articles (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY (id)
);
关键规则:当列长度真正固定且较短(如MD5、SHA256哈希、国家代码、性别字段)时,CHAR更高效;其余场景一律用VARCHAR。不要用
|
1
|
VARCHAR(4)
|
存定长数据——1~2字节的长度前缀完全浪费。
字符串长度定义的性能影响
一个容易被忽视的事实:VARCHAR的长度定义影响索引的前缀长度限制和临时表的使用方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 -- VARCHAR(255)的陷阱
CREATE TABLE user_profiles (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
nickname VARCHAR(255) NOT NULL,
bio VARCHAR(5000),
PRIMARY KEY (id),
KEY idx_nickname (nickname) -- 使用前缀索引
);
-- InnoDB索引列总长度限制3072字节
-- utf8mb4下,VARCHAR(255)占255x4=1020字节
-- 如果多个VARCHAR(255)建联合索引,很快超限
-- 更优:根据业务实际长度定义
CREATE TABLE user_profiles (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
nickname VARCHAR(50) NOT NULL COMMENT '昵称,实际不超过20字',
bio TEXT COMMENT '个人简介,长文本用TEXT',
PRIMARY KEY (id),
KEY idx_nickname (nickname) -- 50x4=200字节,完整索引
);
更重要的是,MySQL在创建内部临时表时(GROUP BY、ORDER BY、UNION等),VARCHAR列会按定义的最大长度分配空间。VARCHAR(255)与VARCHAR(50)存储相同数据,在内存临时表中前者占用4倍空间。这直接导致大查询更易触发磁盘临时表,性能下降10-100倍。
TEXT与BLOB类型的注意事项
TEXT和BLOB类型在InnoDB中有特殊处理:当行数据超过约半页大小(约8KB)时,大字段会被存储到溢出页(off-page),主记录只保留20字节指针。这带来几个问题:
- 读取TEXT列需要额外一次随机I/O访问溢出页
- TEXT列不能设置DEFAULT值(MySQL 8.0已放宽此限制)
- TEXT列上的索引必须是前缀索引
- MEMORY引擎不支持TEXT/BLOB,含这些列的查询临时表必须用MyISAM或InnoDB
1
2
3
4
5
6
7
8
9
10
11
12
13
14 -- TEXT列的优化策略
CREATE TABLE blog_posts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
summary VARCHAR(500) COMMENT '摘要单独存,避免读全文',
content MEDIUMTEXT NOT NULL COMMENT '正文,可能很长',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_created (created_at)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; -- DYNAMIC格式对溢出页更友好
-- 查询时只读需要的列,避免触发溢出页读取
-- 坏:SELECT * 会让每行都读溢出页
-- 好:SELECT id, title, summary, created_at FROM blog_posts ORDER BY created_at DESC LIMIT 20;
时间类型:DATETIME、TIMESTAMP与MySQL 8.0新特性
三种时间类型的存储差异
| 类型 | 字节数 | 范围 | 时区处理 | 自动更新 |
|---|---|---|---|---|
| DATE | 3 | 1000-01-01 ~ 9999-12-31 | 无 | 否 |
| DATETIME | 5~8 | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 无(原样存储) | 否 |
| TIMESTAMP | 4 | 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07 | 存储UTC,读取转时区 | 是 |
TIMESTAMP只占4字节但存在2038年上限问题。MySQL 8.0.28+已将TIMESTAMP范围扩展到
|
1
|
‘3001-01-18 23:59:59 UTC’
|
,但存储仍为4字节(利用了之前未使用的位),这是一个非常值得升级的理由。
时区陷阱与最佳实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 -- 时区陷阱演示
SET time_zone = '+08:00'; -- 北京时间
INSERT INTO events (id, created_at_ts, created_at_dt)
VALUES (1, NOW(), NOW());
-- 切换时区查看
SET time_zone = '+00:00'; -- UTC
SELECT created_at_ts, created_at_dt FROM events WHERE id = 1;
-- created_at_ts: 2026-09-02 11:00:00 (自动转换!减了8小时)
-- created_at_dt: 2026-09-02 19:00:00 (不变,存什么读什么)
-- 推荐方案:用DATETIME存业务时间,TIMESTAMP存系统时间
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
COMMENT '系统创建时间,自动时区转换',
delivery_date DATE NOT NULL COMMENT '配送日期,业务语义',
scheduled_time DATETIME NOT NULL COMMENT '预约配送时间,不涉及时区转换',
PRIMARY KEY (id)
);
MySQL 8.0时间函数增强
MySQL 8.0引入了大量新的时间函数,大幅提升了时间处理能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 -- 8.0新增的时间函数
-- 1. 时间戳差值计算
SELECT TIMESTAMPDIFF(SECOND, '2026-09-01 00:00:00', '2026-09-02 12:30:00');
-- 结果:132600
-- 2. 格式化日期(8.0增强了格式说明符)
SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日 %H:%i:%s');
-- 3. 8.0的INTERVAL语法增强
SELECT NOW() + INTERVAL 2 MONTH + INTERVAL 3 DAY;
-- 4. 8.0.19+支持VALUES表构造器中引用别名
INSERT INTO logs (event_time, event_type)
VALUES (NOW(), 'login'), (NOW() + INTERVAL 1 SECOND, 'query');
JSON类型:文档数据库模式的正确打开方式
JSON vs JSONB(其他数据库)vs TEXT
MySQL 8.0的JSON类型不同于PostgreSQL的JSONB——MySQL的JSON在磁盘上以二进制格式存储(内部称Binary JSON,类似JSONB),但查询时自动解析。相比存储在TEXT中的JSON字符串,原生JSON类型有以下优势:
- 写入时自动校验JSON格式,非法JSON直接报错
- 二进制存储,解析速度比TEXT快2-3倍
- 支持虚拟生成列+函数索引,实现部分索引能力
- 支持JSON路径表达式查询(
1->
和
1->>操作符)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 -- JSON列与虚拟列索引配合
CREATE TABLE products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
attrs JSON NOT NULL COMMENT '产品属性,灵活结构',
-- 虚拟生成列:从JSON提取常用查询字段
category VARCHAR(50) GENERATED ALWAYS AS (attrs->>'$.category') STORED,
price DECIMAL(10,2) GENERATED ALWAYS AS (attrs->>'$.price') STORED,
PRIMARY KEY (id),
KEY idx_category (category),
KEY idx_price (price)
) ENGINE=InnoDB;
-- 插入数据
INSERT INTO products (name, attrs) VALUES
('MacBook Pro', '{"category": "laptop", "price": 14999.00, "color": "space-gray", "ram": 32}'),
('iPhone 15', '{"category": "phone", "price": 5999.00, "color": "blue", "storage": 256}');
-- 利用虚拟列索引高效查询
EXPLAIN SELECT * FROM products WHERE category = 'laptop';
-- type=ref,走索引
-- JSON路径查询(不利用索引,全表扫描)
EXPLAIN SELECT * FROM products WHERE attrs->>'$.category' = 'laptop';
-- type=ALL,不走索引!必须用虚拟列
JSON的存储开销与适用边界
JSON类型的存储开销比等价的关系列大30%-50%,因为键名和结构元数据也被存储。当JSON文档中的字段稳定且需要频繁查询时,应拆分为普通列;只有当结构真正灵活多变时才用JSON。经验法则:如果一个JSON字段被查询的频率大于总查询的20%,就该拆出来。
ENUM与SET:小众但高效的特殊类型
ENUM的内部实现与性能优势
ENUM类型在内部用1-2字节的整数存储,每个枚举值映射为1、2、3…。这使得ENUM在存储和索引效率上远优于VARCHAR:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 -- 用ENUM代替VARCHAR存状态字段
-- 方案A:VARCHAR(每个状态占4-10字节+长度前缀)
CREATE TABLE orders_varchar (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
PRIMARY KEY (id),
KEY idx_status (status)
);
-- 方案B:ENUM(每个状态只占1-2字节整数)
CREATE TABLE orders_enum (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
status ENUM('pending','processing','shipped','delivered','cancelled')
NOT NULL DEFAULT 'pending',
PRIMARY KEY (id),
KEY idx_status (status)
);
-- 性能差异:
-- 100万行数据,索引大小:ENUM约8MB,VARCHAR约20MB
-- 范围扫描速度:ENUM快40%+
-- 存储空间:ENUM节省60%+
-- ENUM的陷阱:新增值需要ALTER TABLE
ALTER TABLE orders_enum MODIFY COLUMN
status ENUM('pending','processing','shipped','delivered','cancelled','refunded');
-- MySQL 8.0中,ENUM修改通常需要重建表
SET类型:多值存储的利弊
SET类型用位图存储多个值,最多64个成员,每个成员占1位。适合”多选标签”场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 -- SET类型:权限标签
CREATE TABLE user_permissions (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id INT UNSIGNED NOT NULL,
permissions SET('read','write','delete','admin','export','import') NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO user_permissions (user_id, permissions)
VALUES (1, 'read,write'), (2, 'read,write,delete,admin');
-- 位运算查询
SELECT * FROM user_permissions WHERE FIND_IN_SET('admin', permissions) > 0;
-- 等价于
SELECT * FROM user_permissions WHERE permissions & 16; -- admin是第4位(2^4=16)
-- 注意:SET类型不能建高效索引,
-- 因为索引的是位图值而非单个成员,
-- FIND_IN_SET查询会全表扫描
SET的索引问题使其不适合大规模筛选场景。当需要按单个标签高效查询时,应使用关联表(多对多映射),这是更规范的关系型设计。
数据类型与索引的交互:隐式转换是性能杀手
数据类型选型最致命的后果是隐式类型转换导致索引失效。这是生产环境最常见的”突然变慢”原因之一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 -- 经典陷阱:字符串与数字的比较
CREATE TABLE payments (
order_id VARCHAR(32) NOT NULL PRIMARY KEY,
amount DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 索引有效:字符串与字符串比较
EXPLAIN SELECT * FROM payments WHERE order_id = 'ORD20260902001';
-- type=const,走主键
-- 索引失效:数字与字符串比较
EXPLAIN SELECT * FROM payments WHERE order_id = 20260902001;
-- type=ALL,全表扫描!MySQL将order_id转为数字再比较
-- 另一个常见陷阱:JOIN两侧类型不匹配
CREATE TABLE order_details (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_id VARCHAR(32) NOT NULL, -- VARCHAR!
product_id INT UNSIGNED NOT NULL, -- INT UNSIGNED
PRIMARY KEY (id),
KEY idx_order (order_id),
KEY idx_product (product_id)
);
-- 如果另一张表的关联字段是INT类型
CREATE TABLE payment_records (
order_id INT UNSIGNED NOT NULL, -- INT! 类型不匹配
PRIMARY KEY (order_id)
);
-- 这个JOIN无法使用索引做等值查找
EXPLAIN SELECT * FROM order_details od
JOIN payment_records pr ON od.order_id = pr.order_id;
-- type=ALL或ref但rows极高,因为隐式转换
-- 修复:统一类型
ALTER TABLE payment_records MODIFY order_id VARCHAR(32) NOT NULL;
隐式转换的检测方法
MySQL 8.0提供了
|
1
|
SHOW WARNINGS
|
和
|
1
|
EXPLAIN ANALYZE
|
帮助检测隐式转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 -- 方法1:EXPLAIN + SHOW WARNINGS
EXPLAIN SELECT * FROM payments WHERE order_id = 20260902001;
SHOW WARNINGS;
-- 如果看到 "Cast condition removed" 或类型转换提示,说明有隐式转换
-- 方法2:EXPLAIN ANALYZE(8.0.16+)
EXPLAIN ANALYZE SELECT * FROM payments WHERE order_id = 'ORD20260902001';
-- 关注实际执行时间和行数,与估算值对比
-- 方法3:performance_schema监控
-- 开启语句事件采集
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES'
WHERE NAME LIKE '%statements%';
-- 查找全表扫描的高频SQL
SELECT DIGEST_TEXT, COUNT_STAR, SUM_ROWS_EXAMINED/SUM_STAR avg_rows
FROM performance_schema.events_statements_summary_by_digest
WHERE SUM_ROWS_EXAMINED > 1000000
ORDER BY avg_rows DESC
LIMIT 20;
实战案例:一个电商订单表的数据类型重构
重构前的问题表
1
2
3
4
5
6
7
8
9
10
11 CREATE TABLE orders_bad (
id VARCHAR(50) NOT NULL COMMENT 'UUID字符串主键',
user_id VARCHAR(50) NOT NULL COMMENT '用户ID,字符串',
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '订单状态',
total_price FLOAT COMMENT '总价,浮点数',
created_at DATETIME COMMENT '创建时间',
items TEXT COMMENT '商品列表JSON字符串',
PRIMARY KEY (id),
KEY idx_user (user_id),
KEY idx_status (status)
) ENGINE=InnoDB;
问题清单:
- UUID字符串主键:36字节 vs BIGINT的8字节,索引大4.5倍,B+树更深层
- user_id用VARCHAR:JOIN时隐式转换风险大
- status用VARCHAR:每行浪费15+字节,索引效率低
- total_price用FLOAT:金融数据精度丢失
- items用TEXT存JSON:无校验、无索引、溢出页开销
重构后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 CREATE TABLE orders_optimized (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
order_no CHAR(20) NOT NULL COMMENT '业务订单号,固定格式如ORD+17位数字',
user_id INT UNSIGNED NOT NULL COMMENT '用户ID,整数',
status ENUM('pending','paid','shipped','delivered','cancelled','refunded')
NOT NULL DEFAULT 'pending' COMMENT '订单状态',
total_price DECIMAL(12,2) NOT NULL COMMENT '总价,精确到分',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
items JSON NOT NULL COMMENT '商品列表,原生JSON',
-- 虚拟列索引
item_count INT GENERATED ALWAYS AS (JSON_LENGTH(items)) STORED,
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user (user_id),
KEY idx_status_created (status, created_at),
KEY idx_item_count (item_count)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
重构效果对比
| 指标 | 重构前 | 重构后 | 提升 |
|---|---|---|---|
| 主键索引大小(100万行) | ~55MB | ~12MB | 4.6倍 |
| status索引大小 | ~22MB | ~8MB | 2.75倍 |
| 全表占用 | ~320MB | ~180MB | 1.8倍 |
| 主键点查延迟 | 0.8ms | 0.3ms | 2.7倍 |
| 状态范围扫描 | 45ms | 12ms | 3.75倍 |

ROW_FORMAT对数据类型存储的影响
InnoDB的行格式直接影响数据类型的物理存储方式:
- COMPACT/REDUNDANT:传统格式,溢出页阈值约768字节前缀+指针
- DYNAMIC(8.0默认):长字段直接存储到溢出页,主记录仅20字节指针,更适合TEXT/BLOB/长VARCHAR
- COMPRESSED:在DYNAMIC基础上增加页级压缩,节省磁盘但增加CPU开销
1
2
3
4
5
6
7
8
9
10
11
12
13 -- 查看表的行格式
SELECT TABLE_NAME, ROW_FORMAT, TABLE_ROWS,
DATA_LENGTH, INDEX_LENGTH
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'orders';
-- 修改行格式
ALTER TABLE orders ROW_FORMAT=DYNAMIC;
-- 压缩行格式(适合读多写少的大表)
ALTER TABLE logs ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
-- 注意:压缩格式增加约10%的CPU开销,但可减少50%+的磁盘占用
-- 适合历史数据表、日志表等写后少改的场景
MySQL 8.0数据类型选型决策树
综合以上分析,以下是一个简化的数据类型选型决策流程:
- 整数数据:按最小范围选类型(TINYINT < SMALLINT < MEDIUMINT < INT < BIGINT),不需要负数加UNSIGNED
- 精确小数:金融/库存选DECIMAL,科学计算选DOUBLE
- 短字符串:定长且64字符内选CHAR,变长选VARCHAR,长度按业务实际定义
- 长文本:VARCHAR(65535上限内)选VARCHAR,超出选TEXT/MEDIUMTEXT/LONGTEXT
- 时间:纯日期选DATE,需要时区选TIMESTAMP,不需要时区选DATETIME
- 有限枚举:单选选ENUM,多选选关联表(避免SET的索引问题)
- 半结构化:字段稳定拆为普通列,真正灵活用JSON+虚拟列索引
- 二进制:图片/文件路径用VARCHAR存路径,二进制内容用BLOB
牢记一个核心原则:最小够用原则——选择能表示数据范围的最小类型,这同时优化了存储、内存和索引三个维度。数据类型选型是数据库设计中投入产出比最高的优化,因为它从源头减少了所有下游操作的开销。
汤不热吧