MySQL 表设计优化实战指南
前言
上一篇文章我们讲了「表设计范式」,核心是怎么把表设计得干净、规范。但光规范还不够——当数据量从几万涨到几百万、几千万,业务从单机变分布式时,一张"设计正确"的表照样会变慢。这时就需要表设计优化:在规范的基础上,通过数据字典、索引、拆分等手段,让表跑得快、好维护、扛得住并发。
本文围绕四个核心模块展开:
- 数据字典维护:把表的"说明书"管理起来,让团队协作有章可循。
- 索引维护:让查询从"全表扫"变成"精准定位"。
- 垂直拆分:把一张"又宽又重"的表按字段拆开。
- 水平拆分:把一张"行数爆炸"的表按行拆到多个库/表。
最后用一个电商订单表和用户信息表的完整案例,把优化前后的性能指标量化给你看。
一、优化总览:从评估到验证的四步闭环
表设计优化不是"拍脑袋改字段",而是一个标准闭环。建议按下面四步走,每步都有明确的产出:
| 阶段 | 核心动作 | 产出物 |
|---|---|---|
| 评估 | 找出慢查询、大表、热点字段,量化当前瓶颈 | 慢查询日志、表大小统计、性能基线 |
| 规划 | 确定优化手段(加索引/拆字段/分表),评估影响面 | 优化方案、风险清单 |
| 实施 | 数据字典、索引、拆分的具体落地 | 建表 SQL、数据迁移脚本 |
| 验证 | 对比优化前后指标,回滚预案 | 性能对比报告 |
黄金原则:先有指标,再谈优化。 没有性能基线的优化都是耍流氓——你根本不知道优化到底有没有效果。所以本文每个案例都会给出优化前后可量化的指标。
二、数据字典维护
2.1 数据字典的核心作用与维护边界
数据字典就是数据库的"说明书":它记录每张表、每个字段的类型、含义、约束、注释,以及表与表之间的关联关系。
它的作用有三点:
- 统一认知:新人看到表,能快速明白每个字段是什么意思,不用追着老员工问。
- 约束规范:字段类型、长度、是否为空、默认值都有统一标准,避免"A 员工建的表字段叫
name,B 员工叫username"。 - 变更留痕:字段的新增、修改、删除有记录,出了问题能追溯。
维护边界要分清:数据字典管的是表结构(Schema)和元数据,不是业务数据本身。它回答的问题是"这张表有什么字段、字段什么类型、外键指向哪",而不是"这张表里存了哪些具体数据"。
在 MySQL 里,可以用三种方式承载数据字典:
- 官方自带:
information_schema(只读元数据)、performance_schema(性能元数据)、mysql库里的系统表。8.0 之前数据字典是零散的.frm文件,8.0 之后统一放进了 InnoDB 的mysql.ibd。 - 注释规范:在建表 SQL 里写
COMMENT,这是最轻量、最贴近业务的数据字典。 - 第三方工具/文档:用专门的表结构文档工具(如
screw、tbls)自动生成 HTML/Word 版数据字典。
2.2 全流程标准化维护步骤
数据字典的维护要形成固定流程,建议如下:
- 建表即建档:任何新建表,必须带
COMMENT表注释和字段注释,不允许"裸字段"。 - 变更走评审:字段的新增/修改/删除,先提变更单,说明原因、影响面、是否需要数据迁移。
- 同步更新文档:改完表结构,立即同步数据字典文档(或重新生成工具导出),不能"表改了、文档没改"。
- 定期盘点:每月/每季度盘点一次,找出无注释字段、孤儿表、僵尸字段。
- 纳入版本管理:把建表 SQL(DDL)纳入 Git 管理,用 Flyway / Liquibase 等工具做数据库版本迁移。
2.3 字段类型、约束、注释、关联关系的规范要求
这是数据字典落地最核心的部分。给出一套可以直接照抄的规范:
字段类型规范:
| 数据类别 | 推荐类型 | 说明 |
|---|---|---|
| 主键 | BIGINT UNSIGNED AUTO_INCREMENT | 不用业务字段当主键 |
| 短字符串(用户名等) | VARCHAR(50) | 长度按需,不无脑 255 |
| 长文本 | TEXT / MEDIUMTEXT | 大段文字单独考虑拆表 |
| 金额 | DECIMAL(10,2) | 绝不用 FLOAT/DOUBLE |
| 状态/类型 | TINYINT | 配合注释说明枚举含义 |
| 布尔 | TINYINT(1) | 0/1 |
| 时间 | DATETIME | 需要自动更新用 ON UPDATE |
| 唯一标识 | CHAR(32) | 如 MD5、订单号等定长串 |
约束规范:
- 主键:每张表必须有主键,优先自增
BIGINT。 - 唯一约束:手机号、用户名、订单号等业务唯一字段必须加
UNIQUE KEY。 - 非空约束:核心业务字段加
NOT NULL,避免出现"半吊子数据"。 - 默认值:状态、数量等字段给合理
DEFAULT,减少插入时漏写。 - 外键:可选(见下文),但关联列必须建索引。
注释规范:
- 表注释:说明这张表的业务用途,如
COMMENT '订单主表'。 - 字段注释:说明字段含义,枚举字段要把每个值含义写清楚,如
status TINYINT COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成 4已取消'。 - 索引注释:复杂索引说明用途。
关联关系规范:
- 关联字段命名统一:外键统一叫
xxx_id,如user_id、order_id。 - 关联字段类型必须和被引用表主键类型完全一致(否则无法建外键、JOIN 时还会类型转换)。
- 关联关系要在数据字典里明确标注"一对多 / 多对多 / 一对一"。
下面是一段规范化的建表示例,可以作为团队的"标准模板":
CREATE TABLE `order` (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
order_no VARCHAR(32) NOT NULL COMMENT '订单号(业务唯一)',
user_id BIGINT UNSIGNED NOT NULL COMMENT '下单用户ID(关联user.id)',
user_name VARCHAR(50) NOT NULL COMMENT '下单时用户名快照(反范式冗余)',
total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额(元)',
status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成 4已取消',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id),
KEY idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='订单主表';2.4 日常维护检查清单
给团队做一个可勾选的检查清单,每周/每月过一遍:
- [ ] 是否有新建表缺少表注释或字段注释?
- [ ] 是否有字段该加 UNIQUE 却漏了,导致出现重复数据?
- [ ] 是否有字段类型和关联主键不一致?
- [ ] 是否有孤儿表(长期无人访问、无归属)未清理?
- [ ] 是否有僵尸字段(代码已不用,但表里还留着)?
- [ ] 枚举字段的注释是否和代码里的枚举定义一致?
- [ ] 数据字典文档是否和线上表结构同步更新?
- [ ] 是否有未纳入版本管理的 DDL 变更?
三、索引维护
3.1 索引设计的底层逻辑
索引的本质,是用额外的存储空间和写入维护成本,换取查询速度。它的底层是 B+ 树:查询时从根节点一路走到叶子,定位极快;代价是每次 INSERT/UPDATE/DELETE 都要同步维护索引结构。
所以索引设计的核心矛盾永远是:查询快了,写入慢了,还占了空间。设计时永远围绕"这个字段到底查不查、怎么查"来决策。
3.2 常见索引类型与适用场景
| 索引类型 | 说明 | 适用场景 |
|---|---|---|
| 主键索引 | 表的主键,唯一且非空 | 每表必建,InnoDB 数据就是按主键聚簇存储 |
| 唯一索引 | 值不重复,可为空 | 手机号、用户名、订单号等业务唯一字段 |
| 普通索引 | 最基础的非唯一索引 | 高频查询、JOIN、排序字段 |
| 复合索引 | 多个列组成的索引 | 多条件组合查询,注意最左前缀 |
| 覆盖索引 | 查询字段都在索引里,无需回表 | 高频且只查少量字段的查询 |
| 前缀索引 | 只索引长字符串的前 N 个字符 | 长文本、URL 等,减少索引体积 |
| 全文索引 | 针对文本的关键词检索 | 文章、评论的模糊搜索 |
最左前缀原则是复合索引最重要的坑:复合索引 (a, b, c) 相当于同时建立了 a、(a,b)、(a,b,c) 三个索引,但无法单独支持 b、c、(b,c) 的查询。所以复合索引的字段顺序要按"最常被单独查询/区分度高的字段放最左"来设计。
3.3 索引创建、删除、优化的判断标准
该建索引的判断标准(满足越多越该建):
- 字段出现在
WHERE、JOIN、ORDER BY、GROUP BY里。 - 字段区分度高(不同值多,如用户 ID;区分度低的如性别"男/女"就不值得单建)。
- 查询频率高、数据量大(小表全表扫也很快,不必建)。
该删索引的判断标准:
- 完全没被使用(可通过
performance_schema或慢查询日志排查)。 - 冗余索引:被别的索引"前缀"覆盖了。比如有
(a,b)索引,又单独建a索引,a就是冗余的。 - 区分度极低的索引:如给"性别"字段建索引,几乎没用还占空间。
判断区分度的方法:
-- 计算字段区分度,越接近 1 越好
SELECT COUNT(DISTINCT user_id) / COUNT(*) AS selectivity FROM `order`;
-- 如果结果接近 1(如 0.9),值得建索引;接近 0(如 0.001),不值得3.4 冗余索引、无效索引的识别与清理
识别冗余索引:MySQL 没有直接命令,但可以用 SHOW INDEX 看索引列,人工判断。典型冗余:
-- 冗余:idx_a 被 idx_a_b 覆盖
KEY idx_a (a),
KEY idx_a_b (a, b) -- 有它,idx_a 就是多余的清理方法:直接 ALTER TABLE t DROP INDEX idx_a;,然后观察慢查询无变化即可确认安全。
识别无效索引:从 performance_schema 看索引从未被使用:
-- 开启索引使用统计(MySQL 5.6+)
SELECT * FROM sys.schema_unused_indexes;
-- 输出:哪些索引从未被使用,可考虑删除sys.schema_unused_indexes 是 MySQL 自带的视图,直接列出从未用过的索引。注意要在业务高峰期跑一段时间后再看,避免"刚建还没用"被误判。
3.5 索引维护的自动化监控方案
手动排查索引太累,建议做自动化监控:
- 慢查询日志 + pt-query-digest:定期分析慢查询,找出缺索引的 SQL。
sys.schema_unused_indexes定时巡检:写个脚本每天跑一次,把从未使用的索引推送到群里。sys.schema_redundant_indexes:MySQL 自带的冗余索引视图,自动识别冗余索引。EXPLAIN审查机制:上线新 SQL 前强制EXPLAIN,检查是否走索引、是否全表扫描。- 告警阈值:当某张表大小超过阈值(如 100 万行)或某查询耗时超过阈值(如 1s),自动告警提醒优化。
四、垂直拆分
4.1 核心原理与适用场景
垂直拆分:把一张"又宽又重"的表,**按字段(列)**拆成多张表。
它的核心原理是:让热点数据更紧凑、让冷数据不拖累主表。一张表字段越多,单行数据越大,InnoDB 一页(默认 16KB)能装的行数就越少,查询时要读的页就越多,越慢。
适用场景:
- 表字段特别多(几十上百个),其中大部分字段很少被查询。
- 存在大字段(
TEXT、BLOB),主表查询时被迫把大字段一起读出来。 - 部分字段访问频率极高,部分字段几乎没人碰(冷热分明)。
4.2 拆分判断维度(冷热数据、访问频率、数据长度)
拆字段时,从三个维度判断:
- 冷热数据:把"高频访问的字段"放主表,"低频访问的字段"放扩展表。比如用户表里
id/username/phone天天查,而address/bio/avatar_url偶尔才看。 - 访问频率:经常一起查的字段放一起,分开访问的字段分开。
- 数据长度:
TEXT、BLOB等大字段单独放一张表,主表保持"小而紧凑"。
4.3 实施步骤与数据同步方案
以「用户信息表」为例,假设原来是一张大宽表:
-- 原表:字段多,且包含大字段
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
phone VARCHAR(20),
password VARCHAR(100),
avatar_url VARCHAR(500), -- 头像URL
bio TEXT, -- 个人简介(大字段)
address VARCHAR(200), -- 地址
created_at DATETIME
);拆分后的两张表:
-- 主表:只保留高频、小字段
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
phone VARCHAR(20),
password VARCHAR(100),
created_at DATETIME
);
-- 扩展表:低频、大字段
CREATE TABLE user_profile (
user_id BIGINT PRIMARY KEY, -- 与 user.id 一一对应
avatar_url VARCHAR(500),
bio TEXT,
address VARCHAR(200)
);实施步骤:
- 建好新表结构。
- 双写/回填:先把
user的历史数据按字段分别回填到user和user_profile。 - 数据同步:迁移期间让代码同时读写新表(或通过触发器/订阅 binlog 同步)。
- 切换读:读逻辑改为"先查主表,需要时再查扩展表"。
- 下线旧字段:验证无误后,删除旧表的大字段,完成切换。
数据同步方案(迁移过程中保持一致性):
- 一次性回填 + 双写:迁移期间新旧表都写,切完后只写新表。
- binlog 订阅:用 Canal 等工具订阅 binlog,实时同步到新表。
- 触发器:在旧表上建
AFTER INSERT/UPDATE触发器同步到新表(适合小规模)。
4.4 拆分后的关联查询优化策略
拆表后查询"完整用户信息"需要 JOIN,优化策略:
- 主表优先:大多数场景只需要主表字段,避免 JOIN,性能反而更好。
- 按需加载:只有真正需要扩展字段时才
JOIN user_profile,且用user_id等值关联(走主键)。 - 一对一主键关联:
user_profile.user_id直接作为主键,JOIN 时走主键索引,极快。
-- 只在需要扩展信息时才 JOIN
SELECT u.id, u.username, p.bio, p.address
FROM user u
LEFT JOIN user_profile p ON u.id = p.user_id
WHERE u.id = 100;4.5 拆分的边界与不适合拆分的场景
垂直拆分不是万能的,以下情况不要拆:
- 字段本来就不多、访问都均匀:拆了徒增 JOIN,得不偿失。
- 字段强关联、总是一起读写:比如订单金额和订单号,拆开反而增加复杂度。
- 事务频繁涉及多字段:拆开后一个事务要跨表,增加锁和一致性问题。
- 数据量很小:垂直拆分主要解决"行太大",如果数据量很小,收益可忽略。
拆分边界:垂直拆分一般只拆 1~2 次(主表 + 扩展表),不要拆成七八张碎片表,否则 JOIN 复杂度失控。
五、水平拆分(分库分表)
5.1 核心逻辑
水平拆分:把一张"行数爆炸"的表,**按行(记录)**拆分到多个表或多个库。每个分片结构完全相同,只是数据不同。
它解决的是垂直拆分解决不了的问题:单表数据量太大。当单表超过千万级,即使索引再好,B+ 树层级变高、单表操作也会成为瓶颈,此时只能"把一张大表切成多张小表"。
先分清两个概念:
- 分表:同一个库里,把一张表拆成多张表(如
order_0、order_1)。 - 分库:拆到不同数据库实例(如
db_0、db_1),能进一步提升并发承载能力。
5.2 常用分片策略与适用场景
| 分片策略 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 哈希分片 | 对分片键取模/一致性哈希 | 数据分布均匀 | 范围查询需跨片 | 用户、订单等无范围查询需求 |
| 范围分片 | 按分片键范围划分(如时间、ID 段) | 范围查询友好 | 可能数据倾斜 | 按时间归档的日志、订单 |
| 列表分片 | 按枚举值划分(如地区) | 业务清晰 | 分布可能不均 | 按地区、业务线划分 |
哈希分片示例(取模):
-- 按 user_id % 4 分到 4 张表
CREATE TABLE order_0 (...) ; -- user_id % 4 = 0
CREATE TABLE order_1 (...) ; -- user_id % 4 = 1
CREATE TABLE order_2 (...) ; -- user_id % 4 = 2
CREATE TABLE order_3 (...) ; -- user_id % 4 = 3范围分片示例(按时间):
-- 按订单创建时间分表
CREATE TABLE order_202401 (...) ; -- 2024年1月
CREATE TABLE order_202402 (...) ; -- 2024年2月5.3 分布式 ID 生成方案
分表后,原来单表自增主键 AUTO_INCREMENT 就不能用了(多个分片会生成重复 ID)。常见方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 雪花算法(Snowflake) | 时间戳+机器号+序列号组成 64 位 ID | 趋势递增、不依赖 DB、高性能 | 依赖机器时钟 |
| 数据库号段 | 每次从 DB 取一段 ID 区间 | 简单、可控 | 依赖 DB,有单点 |
| Redis 自增 | INCR 命令生成 ID | 简单快速 | 依赖 Redis,需持久化 |
| 美团 Leaf / 滴滴 Tinyid | 号段+双 buffer 优化 | 高性能、高可用 | 需引入组件 |
推荐:大多数场景用雪花算法最省心,趋势递增(对索引友好)、全局唯一、不依赖外部服务。
5.4 跨分片查询优化
分片后最大的坑是跨片查询。优化策略:
- 查询必须带分片键:设计分片时,保证绝大多数查询都带分片键(如用户查询自己的订单,一定带
user_id),这样能精准路由到单个分片。 - 避免跨片聚合:
COUNT、SUM等聚合如果跨片,要么在各片分别算再合并(应用层归并),要么用专门的分析库。 - 冗余反查表:如果业务要"按订单号查订单",但分片键是
user_id,就需要一张订单号→user_id的映射表(或 ES 索引)来先找到 user_id,再定位分片。 - 全局表(广播表):像"地区、字典"这类小表,每个库都放一份,避免跨库 JOIN。
5.5 事务处理方案
分库分表后,跨库事务无法用本地事务保证。方案:
- 尽量避免跨片事务:通过合理的分片键设计,让一笔业务的数据落在同一个分片,走本地事务。
- 最终一致性(推荐):用本地消息表 + 可靠消息(如 RocketMQ 事务消息)保证最终一致,不追求强一致。
- 分布式事务框架:如 Seata(AT/TCC/SAGA 模式),但引入成本和性能开销较大,能不用尽量不用。
核心结论:分库分表后,优先用"单分片本地事务 + 消息最终一致",而不是一上来就上分布式事务框架。
5.6 分片规则的扩展与调整
分片一旦上线,扩展是个难题(尤其哈希取模,加片会导致所有数据重新路由)。应对方法:
- 预分片:一开始就按 2 的幂次分片(如 128 片),前期把多个分片放同一个库,后期再平滑迁移到独立库。
- 一致性哈希:加节点时只影响相邻节点,减少数据迁移。
- 倍数扩容:4 片扩到 8 片,旧数据只需按规则拆分迁移一半。
- 双写 + 灰度迁移:扩容期间双写新旧片,数据逐步搬迁,验证无误后切读。
六、其他优化方向(扩展)
除了四大核心模块,还有几个值得一起做的优化:
6.1 字段类型优化
- 能用
TINYINT不用INT,能用INT不用BIGINT,减少存储和索引体积。 VARCHAR长度按需设置,过长的定义会让内存排序更耗资源。- 定长字符串用
CHAR,不定长用VARCHAR。 - 枚举优先用
TINYINT + 注释,而不是ENUM类型(ENUM 修改枚举值代价大)。
6.2 大字段拆分
TEXT、BLOB 字段即使不经常读,也会拖慢主表。两种处理:
- 拆到扩展表(同垂直拆分)。
- 用
SUBSTRING前缀查询 + 文件/对象存储:大文本、图片二进制直接放 OSS,表里只存 URL。
6.3 归档与冷热分离
历史数据(如 3 年前的订单)访问率极低,却占着主表。做法:
- 定期把冷数据归档到历史表(
order_archive)。 - 或结合范围分片,把冷数据分到独立的冷片,用更低成本的存储。
6.4 SQL 与参数优化
- 避免
SELECT *,只查需要的字段(配合覆盖索引)。 - 避免在索引列上做函数运算(
WHERE DATE(created_at) = ...会让索引失效)。 - 用
EXPLAIN审查执行计划,关注type(最好ref/const,避免ALL全表扫)。 - 避免大事务、长事务,减少锁持有时间。
七、实战案例:电商订单表与用户信息表
下面把前面讲的内容,串到一个完整的电商案例里。
7.1 案例一:用户信息表——垂直拆分
优化前:一张大宽表,字段 30+,包含头像 URL、个人简介等大字段,单行约 2KB。
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
phone VARCHAR(20),
password VARCHAR(100),
avatar_url VARCHAR(500),
bio TEXT,
address VARCHAR(200),
-- ... 还有 20 多个低频字段
created_at DATETIME
);痛点:用户列表页只查 id/username/phone,但因为行很大,InnoDB 一页装不了几行,全表扫时 IO 巨大。
优化后:拆成主表 + 扩展表(见 4.3)。
性能对比(量化):
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 单行大小 | ~2KB | 主表 ~300B | 缩小约 85% |
| 用户列表查询(10 万行) | 380ms | 45ms | 快约 8 倍 |
| 每页可缓存行数 | ~8 行 | ~50 行 | 缓冲池命中率大幅提升 |
| 存储占用 | 3.2GB | 主表 0.6GB + 扩展表 2.4GB | 主表瘦身,热点查询不再读大字段 |
7.2 案例二:订单表——水平拆分
优化前:单表 order 增长到 5000 万行,单表约 40GB,按 user_id 查询已经要 2~3 秒。
分片设计:以 user_id 为分片键,哈希取模分 16 片。
-- 分片键:user_id % 16
-- 建 16 张结构相同的表 order_0 ~ order_15
CREATE TABLE order_0 (
id BIGINT NOT NULL COMMENT '雪花算法生成',
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL, -- 分片键
total_amount DECIMAL(10,2),
status TINYINT,
created_at DATETIME,
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id)
) COMMENT '订单表_分片0';
-- order_1 ~ order_15 结构同上主键改造:AUTO_INCREMENT 改为雪花算法生成全局唯一 ID。
查询路由:SELECT * FROM order_{user_id % 16} WHERE user_id = ?,精准命中单分片。
跨片查询处理:用户列表页按 user_id 查,天然带分片键,无需跨片;后台按 order_no 查单条时,先查一张 order_no → user_id 的映射(Redis/ES),再定位分片。
性能对比(量化):
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 单表行数 | 5000 万 | 单分片约 300 万 | 缩小 16 倍 |
| 单表大小 | 40GB | 单分片约 2.5GB | 缩小 16 倍 |
| 按 user_id 查询 | 2.8s | 45ms | 快约 60 倍 |
| 并发承载(TPS) | ~800 | ~12000 | 提升约 15 倍 |
| 单次写入锁竞争 | 高(单表热点) | 低(分散到 16 片) | 显著缓解 |
7.3 从评估到验证的完整落地步骤
把上面两个案例串成一条标准流程:
1. 评估
-- 找大表
SELECT table_name, table_rows, ROUND(data_length/1024/1024/1024, 2) AS data_gb
FROM information_schema.tables
WHERE table_schema = 'shop' ORDER BY data_length DESC;
-- 找慢查询:开启慢查询日志后分析
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;2. 规划:确定 user 表做垂直拆分、order 表做水平拆分,明确分片键 user_id、分片数 16。
3. 实施:建新表 → 回填数据 → 双写 → 切读 → 下线旧表(见 4.3、5.6)。
4. 验证:用压测工具(sysbench / jmeter)跑基准测试,对比上表的性能指标;同时用 EXPLAIN 确认关键 SQL 都走索引、命中单分片。
5. 回滚预案:保留旧表一段时间,一旦新结构出问题,切回旧表读即可。
八、总结:一套可落地的优化方法论
把全文浓缩成一套可直接执行的优化指南:
四大模块的定位:
| 模块 | 解决什么问题 | 一句话原则 |
|---|---|---|
| 数据字典维护 | 表结构混乱、难维护 | 建表即建档,变更留痕 |
| 索引维护 | 查询慢 | 高频查询字段才建索引,冗余索引及时清 |
| 垂直拆分 | 表太宽、行太大 | 冷热分离,大字段独立 |
| 水平拆分 | 表太大、行太多 | 带分片键路由,避免跨片 |
核心原则汇总:
- 先有指标,再优化:优化前必须记录性能基线,否则无法验证效果。
- 数据字典是地基:规范的类型、约束、注释,是一切优化的前提。
- 索引宁缺毋滥:区分度低、不常用、被覆盖的索引都是负担。
- 垂直拆分解"宽",水平拆分解"大":别用错方向。
- 拆分要带分片键设计:水平拆分前先想清楚"绝大多数查询怎么路由"。
- 能用单分片事务,就不用分布式事务:通过分片键设计把业务圈在一个分片内。
- 一切优化都可量化:查询耗时、存储占用、并发 TPS,用数据说话。
记住:优化不是炫技,而是用最小的改动,解决最痛的瓶颈。评估清楚再动手,指标验证再收工,这才是成熟的表设计优化。