
垂直拆分前先确认哪些字段真能挪走
不是所有“不常用”字段都适合拆出去——得看查询模式和事务边界。SELECT * 频繁的表,硬拆反而增加 JOIN 开销;而像 user_profile 里 avatar_url、bio、resume_pdf 这类大字段,如果主业务(如登录、权限校验)从不访问,就是典型可拆目标。
实操建议:
- 用
SELECT慢日志 +EXPLAIN分析高频 SQL,标记出从未出现在WHERE、JOIN或SELECT列表里的字段 - 检查应用层 ORM 映射:有些字段虽不查,但被框架默认加载(比如 Django 的
select_related或 Laravel 的with()),得同步改配置 - 大文本/二进制字段(
TEXT、BLOB)优先拆,它们会拖慢整行读取,尤其当innodb_page_size不足时,容易触发额外 IO
拆分后关联字段必须加索引且类型严格一致
拆成 users 和 users_ext 后,靠 user_id 关联,但常见错误是:users.user_id 是 BIGINT UNSIGNED,而 users_ext.user_id 是 INT 或没设 UNSIGNED——这会导致 MySQL 无法使用索引,JOIN 变全表扫描。
实操建议:
- 两个表的外键字段必须完全同类型、同符号、同长度,用
SHOW COLUMNS FROM users和SHOW COLUMNS FROM users_ext对比 -
user_id在users_ext上必须建INDEX(不只是FOREIGN KEY约束),否则LEFT JOIN时性能断崖下跌 - 避免在
users_ext表上加过多二级索引——大字段表本身 IO 压力大,索引维护成本高
应用层要处理 NULL 和延迟加载逻辑
拆分后 users_ext 行可能不存在(比如用户刚注册还没填资料),直接 INNER JOIN 会丢数据;而 LEFT JOIN 又让所有查询都带额外 IO。更糟的是,ORM 往往把扩展字段当成必填属性,一查就报 NULL 异常。
实操建议:
- 主表查询默认不 JOIN 扩展表,只在明确需要时(如个人中心页)显式
LEFT JOIN,并检查users_ext.*字段是否为NULL - 应用代码里给扩展字段设默认值或空对象(如 Python 的
getattr(user_ext, 'bio', '')),别依赖数据库级COALESCE - 考虑用缓存兜底:对
users_ext单独建 Redis key(如user_ext:123),减少数据库压力
ALTER TABLE 拆分过程要避开主从延迟高峰
把大字段字段从原表 DROP 掉看似简单,但 MySQL 5.7+ 的 ALGORITHM=INPLACE 对 TEXT/BLOB 列仍可能触发表重建,锁表时间远超预期;而主从延迟大的时候执行,从库可能卡住数分钟。
实操建议:
- 先在从库上跑
ALTER TABLE users DROP COLUMN bio,观察Seconds_Behind_Master是否飙升,再决定是否切主库 - 用
pt-online-schema-change工具操作,它会自动分 chunk 搬数据,但注意:目标表不能有触发器,且需预留双倍磁盘空间 - 拆分后立刻检查
information_schema.INNODB_SYS_TABLES,确认新表的ROW_FORMAT是DYNAMIC(而非COMPACT),否则大字段仍可能影响主表页分裂
真正麻烦的不是拆,是拆完发现某些报表 SQL 写死了 SELECT *,或者 DBA 忘了给新表开监控指标——字段挪走了,告警规则还盯在旧表上,问题就藏得更深了。










