本文目录导读:
表结构优化是数据库查询提速的“地基”,很多时候比加索引更根本,下面从几个核心维度拆解,怎样通过优化表结构来让查询变快。
选择合适的数据类型:少就是快
数据库最终是读写磁盘的,数据占用空间越小,一次I/O能读到的行数就越多,查询自然更快。
-- ❌ 糟糕的设计:全是最大长度
CREATE TABLE user (
id BIGINT, -- 如果用户量<1000万,INT就够
name VARCHAR(255), -- 实际人名最多20个字
status VARCHAR(10), -- 实际只有0/1
created_at VARCHAR(20) -- 时间存成了字符串
);
-- ✅ 优化后
CREATE TABLE user (
id INT UNSIGNED, -- 缩小数据类型
name VARCHAR(20),
status TINYINT UNSIGNED, -- 用数字代替字符串
created_at DATETIME -- 用时间类型
);
几个实用原则:
- 能选
INT不选BIGINT,能选TINYINT不选INT - 能用
DATETIME不存字符串格式的时间 VARCHAR长度设得够用就行,不是越大越好- 对于固定长度的字段(如MD5、手机号),用
CHAR比VARCHAR快
垂直拆分:把不常用的字段请出去
一个表字段太多,一行数据就很大,全表扫描或索引回表时会更慢。
场景示例:用户表包含个人资料、登录日志、通知配置等
-- ❌ 全在一张表
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100),
last_login_ip VARCHAR(15), -- 很少用到
last_login_time DATETIME, -- 很少用到
notify_email TINYINT, -- 很少用到
notify_sms TINYINT, -- 很少用到
intro TEXT, -- 大字段,很少查询
avatar_url VARCHAR(200) -- 大字段
);
-- ✅ 垂直拆分:核心表 + 扩展表
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100)
);
CREATE TABLE user_profile (
user_id INT PRIMARY KEY,
intro TEXT,
avatar_url VARCHAR(200)
);
CREATE TABLE user_system (
user_id INT PRIMARY KEY,
last_login_ip VARCHAR(15),
last_login_time DATETIME,
notify_email TINYINT,
notify_sms TINYINT
);
效果:查询用户列表时,只扫user表(行小、页数少),无需把intro这种大字段拖进来。
水平拆分:给单表减减肥
当一张表数据量达到千万甚至亿级别,即使有索引,B+树层数增加,查询效率也会下降。
常见拆分策略:
- 按时间:日志表按月拆
log_202401、log_202402 - 按范围:订单表按用户ID分片
- 按哈希:
user_id % 4分4张表
-- 示例:电商订单按月拆分
CREATE TABLE orders_202501 (
id BIGINT,
user_id INT,
amount DECIMAL(10,2),
created_at DATETIME,
PRIMARY KEY (id)
);
CREATE TABLE orders_202502 ( ... );
注意:水平拆分需要应用层或中间件配合路由,不能简单粗暴。
反范式化(适度冗余):拿空间换时间
3NF规范设计下,多张关联表的JOIN可能很慢,对于查询频繁的场景,可以适当冗余。
-- 范式设计:查询订单列表要联表 SELECT o.id, u.name, p.product_name FROM orders o JOIN user u ON o.user_id = u.id JOIN product p ON o.product_id = p.id; -- ✅ 反范式:冗余核心字段 ALTER TABLE orders ADD COLUMN user_name VARCHAR(50); ALTER TABLE orders ADD COLUMN product_name VARCHAR(100); -- 写入时多写一点,查询时直接拿 SELECT id, user_name, product_name FROM orders;
权衡点:
- 写入频率低、查询频率高 → 适合反范式
- 写入频繁 → 冗余字段的维护成本很高
选择合适的表引擎
不同存储引擎的查询特性完全不同:
| 引擎 | 适用场景 | 对查询的影响 |
|---|---|---|
| InnoDB | 事务、行锁、高并发 | 支持聚簇索引,按主键排序极快,适合OLTP |
| MyISAM | 读多写少、全表扫描 | 表锁,但COUNT(*)飞快,适合日志/统计分析 |
| Memory | 临时表、高频查询 | 纯内存,查询极快但数据不持久 |
建议:大多数Web业务用InnoDB,如果明确做统计分析且能接受锁粒度,可考虑MyISAM。
预计算与汇总表(聚合写优化)
对于需要大量聚合计算的场景(如报表),不要在查询时实时计算。
-- ❌ 实时聚合:千万级订单表,每次查都全表扫描
SELECT COUNT(*), SUM(amount) FROM orders WHERE created_at BETWEEN ? AND ?;
-- ✅ 预计算:建一张天汇总表
CREATE TABLE daily_order_summary (
date DATE PRIMARY KEY,
order_count INT,
total_amount DECIMAL(15,2),
created_at DATETIME
);
-- 定时任务或触发器,每天汇总一次
INSERT INTO daily_order_summary (date, order_count, total_amount)
SELECT DATE(created_at), COUNT(*), SUM(amount)
FROM orders
WHERE DATE(created_at) = CURDATE() - INTERVAL 1 DAY;
查询时从汇总表拿数据,从千万级降到365行。
控制字段顺序(MySQL特性)
MySQL中,可变长度字段(VARCHAR、TEXT、BLOB)放在后面,可以优化行存储。
-- ❌ 变长字段在前
CREATE TABLE user (
name VARCHAR(50), -- 变长
age TINYINT, -- 定长
email VARCHAR(100) -- 变长
);
-- ✅ 定长在前,变长在后
CREATE TABLE user (
age TINYINT, -- 定长
name VARCHAR(50), -- 变长
email VARCHAR(100) -- 变长
);
原理:定长字段位置固定,读取更快;变长字段需要额外偏移计算。
优化优先级
- 数据类型精简(成本最低见效最快)
- 垂直拆分(把大字段、不常用字段移走)
- 冗余反范式(高频联表查询时考虑)
- 预计算/汇总表(聚合统计场景)
- 水平拆分(当单表数据量过大时)
提醒:优化前最好先用EXPLAIN分析慢查询,明确瓶颈到底在哪里,不要盲目动手,表结构的调整通常涉及线上变更,建议用pt-online-schema-change等工具进行,避免锁表。
标签: 表结构优化