大字段真正的问题,从来不是靠“分库分表”或者“数据库原生分区”就能自动解决的。核心不在于把数据切碎,而在于把存储方式切对。MySQL里TEXT一超过64KB就容易惹出Row size too large或innodb log file is full的报错;PostgreSQL里JSONB单值的安全上限大概在200MB左右,再往上走,就随时可能碰壁。遇到这种情况,该做的不是硬塞,是把大内容从主表剥离出去,存到对象存储,再用hash索引做个轻量级映射。

大字段——比如TEXT文本、JSON数据、超长的Markdown内容——靠分库分表或数据库分区根本拆不动。原因很简单:它们只是一行里的一个列,再大的串也只属于一条记录。真正棘手的是四个连锁问题:单条记录太大写不进去、主从复制被拖慢、备份体积失控、查询性能断崖式下跌。所以,核心思路不是分裂数据,而是切换存储方式。
MySQL 中 TEXT 超过 64KB 就会触发 innodb_log_file_size 报错
InnoDB对单条记录有硬指标,页内大约只能装8000字节。超长的TEXT虽然会用外部溢出页来存放,但写入时必须完整加载进buffer pool,一不小心就会甩出ERROR 1118 (42000): Row size too large或者innodb log file is full。常见有几个误区:
- 别以为换成MEDIUMTEXT或LONGTEXT就万事大吉——它们只是把崩溃点往后推了半步,根本不解决根本问题。
- 写入前做截断校验是底线:
if len(content) > 1024*1024 { return errors.New("content too long") }。 - 真正超长的内容——比如日志原文、导出文件——就该和主表分家。用
content_hash(像SHA-256)做索引,实际数据扔到S3或者MinIO这类对象存储里,数据库里只留一份轻量元数据。 - 如果实在没办法必须塞进DB,可以用
COMPRESS()加UNCOMPRESS()(MySQL 5.7以上都支持),但得小心:Go驱动默认不认二进制返回,需要显式设上parseTime=true&binary=true。
PostgreSQL 中 JSONB 超过 2GB 会直接拒绝插入
PostgreSQL单值的硬上限是1GB,但实际安全线建议控制在200MB以内,JSONB还要额外消耗解析开销。错误信息通常是ERROR: string is too long,或者事务跑到一半突然panic: pq: out of memory。有几点值得注意:
- 别让ORM自动序列化大结构体——GORM或sqlc默认会把整个struct当成JSONB插进去,内存一爆就是事故现场。
- 写入前用
json.Marshal配合len()检查长度,超过10MB就该考虑走外部存储了。 - 需要局部更新时,用
jsonb_set()替代全量覆盖。尤其是高频更新的场景,能省掉大量重写整字段的开销。 - 如果字段里塞的是大量重复结构——比如聊天消息列表——不如反范式一把:拆成独立的子表,用
message_id关联,比塞进一个JSONB数组要合理得多。
Go 层如何安全路由大字段读写路径
不是每个字段都该进主表,也不是所有大内容都适合“分片”。关键在Go代码写入之前就做好决策:存DB还是存对象存储?存哪张物理表?用哪种压缩或编码?
具体可以从这几个角度入手:
- 定义一个统一接口:
type BlobStore interface { Put(key string, data []byte) error; Get(key string) ([]byte, error) },然后实现S3、本地文件、甚至Redis等后端。 - 业务逻辑里做判断:
if len(msg.Content) > 512*1024 { store.Put(hash, msg.Content); msg.ContentRef = hash }。 - 查询时做透明还原:
if msg.ContentRef != "" { msg.Content, _ = store.Get(msg.ContentRef) },这样就不会出现N+1查询问题。 - 千万别把大字段当成普通参数直接拼进INSERT语句——pgx的CopyFrom不支持JSONB大对象,MySQL的LOAD DATA也不处理嵌套结构,必须在前面切分或转储。
最容易被忽略的一点:大字段的「可检索性」和「一致性」不是靠分片能补救的。你把一个50MB的PDF元数据拆到10张表里,它依然是无法被全文搜索的;你把日志按天分区,但单条日志超限照样写不进去。真正的分片,从设计第一行结构时就该拒绝让它长大。
