直接拆表+延迟加载是最稳妥解法,因TEXT字段存储在溢出页导致额外随机I/O,且SELECT*/ORDER BY/GROUP BY易触发临时表和文件排序;必须垂直分表,主表留小字段,TEXT存扩展表并确保article_id为唯一索引。

直接拆表 + 延迟加载是最稳妥的解法,别指望靠加索引或调参数硬扛 TEXT 字段的查询开销。
为什么 TEXT 字段一查就慢
MySQL 对 TEXT 和 MEDIUMTEXT 这类大字段默认不存进主键 B+ 树页内,而是单独分配溢出页(overflow page)。每次查到带该字段的行,就得额外发起一次或多次随机 I/O 去读这些溢出页——数据量越大,磁盘寻道越频繁,延迟越不可控。更糟的是:SELECT *、ORDER BY、GROUP BY 都可能触发临时表和文件排序,把大文本一并拖进去处理,内存和 CPU 双爆。
- 典型现象:
EXPLAIN显示type = ALL或出现Using temporary; Using filesort - 真实瓶颈不在 SQL 写法,而在存储结构本身:InnoDB 无法把大字段“折叠”进索引覆盖范围
- 全文索引(
FULLTEXT)只解决搜索,不解决 SELECT 性能;前缀索引(text_col(255))对完整读取无效
必须把大文本字段独立建表
垂直分表不是可选项,是强制动作。核心原则:主表只留高频查询、高选择性、小体积字段;所有 TEXT/BLOB 拆到扩展表,用 JOIN 或应用层懒加载按需取。
- 主表保持窄:比如
article(id, title, author_id, created_at, status) - 扩展表仅存大字段:
article_content(article_id, content, summary),article_id设为主键兼外键 - 避免
ON DELETE CASCADE:大文本删除易锁表,改用应用层显式清理 - 如果业务允许,扩展表甚至可以换引擎:比如用
MyISAM存纯内容(无事务需求时 IO 更轻)
查询时别让大字段参与 JOIN 或排序
哪怕已经拆表,写错 SQL 一样会慢。关键在执行计划里确认大字段没被提前拉取。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 错误写法:
SELECT a.*, c.content FROM article a JOIN article_content c ON a.id = c.article_id WHERE a.status = 1 ORDER BY a.created_at DESC LIMIT 20→c.content被强制载入并参与排序缓冲区 - 正确写法(两步):
① 先查主表 ID:SELECT id FROM article WHERE status = 1 ORDER BY created_at DESC LIMIT 20
② 再按 ID 查内容:SELECT content FROM article_content WHERE article_id IN (1001,1002,...) - 若必须单次查询,至少用
STRAIGHT_JOIN强制驱动表顺序,确保先走主表过滤,再关联扩展表
导入/导出阶段就得控制大字段行为
异构迁移(比如从 PostgreSQL 或 Oracle 迁入 MySQL)时,工具默认行为往往加剧问题。
- HeidiSQL 导入 CSV 时,勾选
Ignore duplicate keys和Disable foreign key checks,但不要勾选Load data infile—— 它对大字段批量插入反而更慢且易超时 - 用
mysqldump --skip-extended-insert导出,避免单条 INSERT 包含超长值导致解析卡顿 - 迁移脚本中对大字段做预处理:比如长度超过 10KB 的
content,先COMPRESS()再入库,应用层UNCOMPRESS()解包(注意客户端配置max_allowed_packet) - 达梦或 SQL Server 迁入时,警惕 CLOB 字段自动映射成
LONGTEXT—— 手动改为MEDIUMTEXT并校验实际长度分布
最常被忽略的一点:拆表后,article_content 表的 article_id 索引必须是唯一索引(不是普通 INDEX),否则 JOIN 时优化器可能放弃使用它;而主表的 status 和 created_at 复合索引,要确保 status 在前,不然范围查询失效。










