无模式不等于免治理
文档库与搜索库放宽了建表约束,但数据形态仍然随业务演进:新增字段、改类型、拆合字段随时发生。没有迁移计划就会长期停留在「新旧两种形态并存、代码里到处是兼容分支」的状态。通行做法是给文档打版本号,按「双写新形态 → 后台回填存量 → 切读新形态 → 清理旧字段」推进;搜索库受映射限制,类型变更必须走重建索引加别名切换。
迁移推进步骤
- 新增字段先以可选形态上线,写入端同时写新旧两处(双写)
- 后台限速回填存量数据,按主键分批并记录进度与失败项
- 读取端切换为主读新字段,保留短期新旧兼容分支
- 观察稳定后清理旧字段与兼容代码,避免形态长期并存
- 搜索库与列式库单独排期:前者重建索引换别名,后者按版本支持的方式重写列
文档库:版本字段与渐进迁移
文档库:版本字段与渐进迁移
// 文档带 schemaVersion,读取端兼容新旧两种形态
{ _id: 1, schemaVersion: 2, name: "ann", profile: { city: "hz" } }
// 回填:按版本条件分批处理,每批完成后记录进度
// 步骤:双写新字段 → 后台回填存量 → 切读新字段 → 稳定后清理旧字段
db.users.updateMany(
{ schemaVersion: 1 },
{ $set: { profile: { city: "$legacyCity" }, schemaVersion: 2 } }
)
// 回填期间用索引支撑范围挑选,避免每批都全表扫描
// db.users.createIndex({ schemaVersion: 1, _id: 1 })搜索库:映射变更走重建
搜索库:映射变更走重建
# 字段类型无法原地修改:建新索引 → reindex → 别名切换
curl -X PUT "localhost:9200/users_v2" -H 'Content-Type: application/json' \
-d '{"mappings":{"properties":{"profile":{"properties":{"city":{"type":"keyword"}}}}}}'
curl -X POST "localhost:9200/_reindex" -H 'Content-Type: application/json' \
-d '{"source":{"index":"users_v1"},"dest":{"index":"users_v2"}}'
curl -X POST "localhost:9200/_aliases" -H 'Content-Type: application/json' \
-d '{"actions":[{"remove":{"index":"users_v1","alias":"users"}},{"add":{"index":"users_v2","alias":"users"}}]}'变更类型与处理方式
| 变更 | 文档库 | 搜索库 / 列式库 |
|---|---|---|
| 新增可选字段 | 直接写入,读取端按缺省值处理 | 新增映射后新文档可用,历史文档按缺省值返回 |
| 新增必填字段 | 先双写并回填,再在写入端加强校验 | 映射加字段后需要 reindex 才能补齐历史数据 |
| 字段类型变更 | 按版本字段双写后切换 | 通常必须重建索引并切换别名 |
| 字段改名 | 双写两处,切读后再删旧名 | 可加字段别名过渡,仍可能需要重建 |
| 删除字段 | 读取端先不依赖,再清理写入 | 删除映射字段同样需要重建索引 |
ClickHouse 侧新增列可以带上 DEFAULT 先上线,再按需执行 MATERIALIZE COLUMN 重写历史分区;修改列类型要核对当前版本支持的转换范围,不确定时走「新建列 + 回填 + 切换」路线。
回填必须限速并分段:全量回填与线上写入争抢资源,中途失败还要能按进度重跑,因此每批要有幂等条件与进度记录。