应将 blog content 拆至独立扩展表,避免与 title 等字段同存主表;因 longtext 行外存储引发额外 io 与内存暴涨,拆表后主表轻量、索引高效,并支持压缩、分区及分离读写压力。

直接把长文本塞进主表的 LONGTEXT 字段,不出三个月就会遇到查询变慢、内存暴涨、备份卡死、主从延迟飙升等问题——这不是容量问题,是设计模式错了。
为什么不能把 blog content 和 title 放同一张表里?
MySQL 的 TEXT/LONGTEXT 字段在 InnoDB 中默认采用“行外存储”:值不存主数据页,而是单独分配溢出页(off-page),每次访问都要额外 IO。更关键的是,只要 SELECT * 或哪怕只查一行带该字段的记录,MySQL 就必须加载整个文本内容到连接内存中——10MB 内容 × 50 并发 = 500MB 瞬间吃光 buffer pool,SHOW PROCESSLIST 里全是 Writing to net 状态。
常见错误现象包括:
-
EXPLAIN显示type=ALL且rows极大,即使加了WHERE id = ? - 慢查询日志里频繁出现含
content字段的语句,但Rows_examined为 0 - 应用层 ORM 自动映射全字段后 OOM,或 MySQL 进程被系统 OOM Killer 杀掉
用扩展表分离 content 字段(推荐)
最轻量、兼容性最强、见效最快的方案:把 content 拆到独立表,主键复用,物理隔离读写压力。
实操步骤:
- 建扩展表:
CREATE TABLE blog_contents (id BIGINT PRIMARY KEY, content LONGTEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8; - 迁移数据:
INSERT INTO blog_contents SELECT id, content, created_at FROM blogs;(注意时间字段对齐) - 删原字段:
ALTER TABLE blogs DROP COLUMN content; - 查正文时显式 JOIN:
SELECT b.id, b.title, b.author, c.content FROM blogs b LEFT JOIN blog_contents c ON b.id = c.id WHERE b.id = 123;
好处是:主表保持轻量,索引高效;blog_contents 可单独压缩、分区(如按 updated_at RANGE 分区)、甚至迁移到低配从库;写入大文本不再拖慢主表事务。
全文检索别硬刚 content 字段
在 blog_contents.content 上直接建 FULLTEXT 索引,短期内能用,但千万级数据后维护成本陡增:索引体积大、OPTIMIZE TABLE 耗时长、ngram 分词器内存占用高,且无法支持高亮、同义词、拼音搜索等基础体验。
更可持续的做法是:
- 用应用层提取关键特征:比如前 500 字 + 标题关键词 + 标签数组,存入主表的
search_summary字段,并建INDEX(search_summary) - 对真正需要模糊匹配的场景,走外部引擎:把
content同步到 Elasticsearch,用_update_by_query保持一致性,MySQL 只负责强一致元数据 - 若必须保留在 MySQL 内,至少禁用自然语言模式,改用布尔模式 + 前缀索引:
ALTER TABLE blog_contents ADD FULLTEXT(content) WITH PARSER ngram; SELECT * FROM blog_contents WHERE MATCH(content) AGAINST('+mysql +优化' IN BOOLEAN MODE);
读取时永远避免 SELECT * + 大字段
这是最容易被忽略、却造成 80% 性能事故的操作。哪怕你已经拆表,只要某次查询写了 SELECT * 或漏掉 LIMIT,就可能触发雪崩。
安全写法只有三种:
- 元数据查询:只取
id, title, author, excerpt, created_at,完全避开content - 预览查询:显式截断,如
SELECT id, title, SUBSTRING(content, 1, 2000) AS preview FROM blog_contents WHERE id = 123; - 流式读取(应用层配合):用
SELECT content FROM blog_contents WHERE id = ?单独查,客户端以 chunk 方式接收,不缓存全文到内存
特别注意:ORM 框架(如 Django ORM、MyBatis)默认 eager load 全字段,务必显式指定 only() 或 defer("content");Spring Data JPA 用 @Query 手写 SQL 更可控。











