Schema 增量迁移
概述
Schema 增量迁移是把一个 Graph Space 从当前结构演进到新结构的过程。它不仅包括创建新的节点类型、边类型和属性,还包括历史数据回填、索引就绪、应用兼容、风险审批和发布验证。
图谱通常会长期运行并持续导入数据。例如,一个小说知识图谱最初只有人物和地点,后续可能增加事件、组织、人物别名、关系置信度和来源信息。直接在生产数据库中手工修改结构,会造成应用声明、数据库实际结构和历史数据彼此不一致。
Graph SDK 提供 Schema 比较、差异结果、迁移计划和增量应用能力,帮助上层系统回答:
- 期望 Schema 与数据库当前结构有什么不同?
- 哪些变化只是新增,哪些需要审核,哪些可能破坏数据?
- 当前后端能否表达这些变化?
- 应用变更后,还需要哪些数据回填和验证?
Graph SDK 不会替业务自动批准或执行所有破坏性 DDL。迁移计划主要用于预览、风险分类和流程编排;备份、审批、灰度、数据回填和回滚仍由上层迁移系统负责。
为什么不能只在启动时重复应用 Schema
在开发环境中,应用启动时使用 ADDITIVE 创建缺失结构很方便;但在生产环境中,Schema 变化可能影响:
- 已经存在的节点和边;
- 正在运行的查询和导入任务;
- 索引构建期间的资源和性能;
- 不同版本应用的兼容性;
- 知识抽取模型和导入映射;
- 依赖旧属性、类型或边方向的业务逻辑。
例如,把 Person.age 从字符串改为整数,不只是一次 DDL:历史值可能包含“未知”“约 30 岁”等内容,新旧应用可能同时读写不同类型,索引也可能需要重建。这类变化必须作为数据迁移处理,而不是交给 ADDITIVE 自动覆盖。
迁移中的三种状态
理解迁移需要区分三个 Schema:
| 状态 | 含义 | 来源 |
|---|---|---|
| 期望 Schema | 新版本应用希望使用的结构 | 代码仓库、配置中心或 Schema Registry |
| 实际 Schema | 数据库当前可观察到的结构 | inspectSchema 反查结果 |
| 已发布版本 | 上层记录的最后一次成功 Schema 版本 | 发布记录或迁移数据库 |
实际 Schema 可能因为手工 DDL、失败迁移或反查能力限制而与已发布版本不同。因此,迁移不能只比较版本号,也不能只相信数据库反查;应同时保留期望制品、发布历史、反查结果和警告。
什么属于增量变化
通常风险较低的新增
- 新增节点类型;
- 新增边类型;
- 给现有节点或边增加可选属性;
- 新增普通索引;
- 增加只供开发工具使用的展示元数据。
即使是新增,也需要评估索引构建成本、Schema 传播时间和新旧应用兼容性。将新属性直接设为业务必填,而历史数据没有值,就不能只按低风险新增处理。
需要审核的结构变化
- 修改属性类型;
- 把可选属性改为必填;
- 修改边的期望起点或终点类型;
- 改变索引字段、目标类型或唯一性;
- 重命名节点类型、边类型或属性;
- 改变边方向或业务语义。
“重命名”在数据库中通常不是简单改名,而是新增结构、迁移数据、切换应用、删除旧结构的组合操作。
可能破坏数据的删除
- 删除节点类型或边类型;
- 删除仍有数据的属性;
- 删除索引或唯一约束;
- 删除旧 Space;
- 清理旧类型数据和历史来源。
删除会使旧查询、旧版本应用或恢复任务失效,必须经过影响分析、备份和明确审批。
差异比较
先读取数据库当前可观察结构,再以“期望 Schema”为目标进行比较:
GraphSchemaInspection inspection =
graph.manager().inspectSchema("company_knowledge");
GraphSchemaDiff diff = GraphSchemaComparator.compare(
expectedSchema,
inspection.getSchema());差异分为:
- additions:期望 Schema 有、实际 Schema 没有的节点、边、属性或索引;
- changes:名称相同但类型、必填、端点或索引定义不同;
- removals:实际 Schema 有、期望 Schema 没有的定义。
比较器只做结构比较,不读取数据库、不执行 DDL,也不判断数据量、历史值能否转换或查询是否仍然兼容。
反查不完整时的差异风险
如果 inspection.isComplete() 为 false,差异结果可能受到后端元数据缺失影响。例如 Nebula 当前反查不包含完整索引元数据,Neo4j 无法可靠恢复关系端点标签。此时不能把“反查中缺失”直接认定为“数据库中不存在”,更不能据此自动删除或重建。
建议:
- 展示全部 inspection warnings 和 unsupported metadata;
- 对无法反查的部分参考上次成功发布记录;
- 使用后端原生管理命令进行补充核验;
- 禁止基于不完整结果自动执行删除。
生成迁移计划
迁移计划将结构差异转换为有顺序的步骤和整体风险等级:
GraphSchemaMigrationPlan plan = GraphSchemaMigrationPlanner.plan(
expectedSchema,
inspection.getSchema());
if (plan.requiresApproval()) {
// 提交上层审批流程,不直接执行。
}风险等级如下:
| 风险 | 条件 | 建议处理 |
|---|---|---|
NONE | 没有结构差异 | 记录检查结果,无需变更 |
ADDITIVE | 只包含新增定义 | 评估容量后可进入兼容发布流程 |
REVIEW_REQUIRED | 包含定义变化 | 必须分析数据转换和应用兼容性 |
DESTRUCTIVE | 包含删除项 | 需要高权限审批、备份和回滚方案 |
计划中的步骤是面向审批和控制面展示的结构化描述,不是一个会自动执行任意 ALTER/DROP 的迁移引擎。当前 applySchemaResult(..., ADDITIVE) 只用于后端能够安全表达的增量应用。
推荐迁移流程
1. 准备版本化的期望 Schema
每次发布都应有稳定 Schema ID 和版本,例如 customer_graph:2.3。版本制品应与应用代码、抽取规则和导入映射一起评审。
2. 检查目标 Space 和后端能力
确认目标 Space、数据库版本、Edition、账号权限和能力矩阵。不要在错误租户或错误环境中执行迁移。
3. 反查实际 Schema
读取当前结构,保存反查时间、后端信息、完整性、警告和不可映射元数据。反查结果应成为本次迁移审计材料的一部分。
4. 生成差异和迁移计划
分别展示新增、变化和删除,不能只给出一个“有差异”的布尔值。对于每项变化,应附带受影响的查询、写入、导入和数据规模评估。
5. 数据预检查
在执行 DDL 前验证:
- 历史属性是否能转换为新类型;
- 将属性设为必填时是否存在空值;
- 唯一约束是否存在重复数据;
- 删除类型或属性时是否仍有数据和调用方;
- 新索引预计构建时间和资源是否可接受。
6. 审批和安排发布窗口
REVIEW_REQUIRED 和 DESTRUCTIVE 必须由上层审批。高成本索引、海量回填和写入不兼容变化也应进入维护窗口,即使结构上看起来只是新增。
7. 执行兼容变更
优先创建新类型、新属性和新索引,让新旧应用都能运行。使用结构化结果记录已执行步骤、警告、错误码和耗时:
GraphSchemaApplyResult result = graph.manager().applySchemaResult(
"company_knowledge",
expectedSchema,
GraphManager.SchemaMode.ADDITIVE);8. 等待后端真正就绪
Nebula 的 Tag、Edge、属性和索引存在异步传播;索引创建后还需要重建。Neo4j 索引也可能经历构建过程。DDL 返回成功不应直接等同于查询已使用新结构。
9. 回填和转换历史数据
新增属性、类型替换或模型重构通常需要独立数据任务。回填应具备:
- 可重复执行的批次;
- checkpoint 和失败记录;
- 新旧字段一致性检查;
- 限速和数据库容量保护;
- 明确的撤销或重跑方案。
10. 切换应用并验证
执行代表性写入、查询、分页、聚合和 Explain,确认新版本应用与真实后端共同工作。不要只验证 Schema 列表中出现了新字段。
11. 清理旧结构
旧属性、旧边和旧索引应经过观察期后再清理。清理属于单独的破坏性发布,不应与新增结构在同一步自动完成。
12. 保存发布记录
至少记录:Space、后端、Schema ID/版本、差异、计划、审批人、执行人、开始结束时间、结果、警告、回填任务和验证结论。
Expand and Contract 模式
对生产系统,推荐使用“扩展后收缩”模式完成不兼容变化。
以 Person.fullName 替换 Person.name 为例:
Expand:扩展
- 新增
fullName,保留name; - 新应用同时兼容两个字段;
- 新写入开始写
fullName,必要时双写; - 回填历史节点;
- 建立并验证新索引。
Migrate:迁移
- 检查所有历史节点已回填;
- 切换查询和抽取规则使用
fullName; - 观察旧字段读取量和错误率;
- 停止对
name的写入。
Contract:收缩
- 确认没有旧应用和恢复任务依赖
name; - 备份或导出必要数据;
- 单独审批删除旧索引和旧属性;
- 更新期望 Schema 和发布记录。
这种方式比原地修改类型或字段名更容易灰度、回滚和兼容多版本应用。
常见迁移场景
新增可选属性
通常属于 ADDITIVE。应用 Schema 后,应等待属性传播并验证新写入;如果查询依赖该属性,还需要建立适当索引。
新增必填属性
结构上是新增,但业务上不是低风险操作。应先按可选属性发布,完成历史数据回填和新写入适配,再提升应用校验要求。不要假设后端会自动为历史节点补值。
修改属性类型
不要依赖 ADDITIVE 原地修改。新增目标类型属性,转换和回填数据,切换读写,最后清理旧属性。需要处理无法转换的历史值。
重命名节点或边类型
通常需要创建新类型、复制或重写数据、重建关系与索引、切换查询,再删除旧类型。大图上的复制成本可能很高,应评估是否真的需要物理重命名。
修改边端点或方向
这会改变图的查询语义。应作为数据重构处理,创建新边、校验数量和方向、切换查询,最后删除旧边。
新增索引
先评估索引大小和构建资源,再创建并等待可用。通过 Explain 或真实查询验证索引是否被使用。Nebula 还需要关注索引重建和传播。
删除索引
删除前确认没有关键查询依赖它,并准备性能回退方案。删除后应运行代表性查询和性能基线对比。
Neo4j 与 Nebula 的迁移差异
Neo4j
- 属性模型较动态,Portable Schema 主要管理稳定 ID 约束和索引;
- 新属性可能随数据写入自然出现,但这不等于完成了数据回填;
- 节点唯一约束可用,关系唯一约束不属于当前 Portable 能力;
- 关系端点标签无法由 Portable Schema 完整强制和反查;
- 大型索引和约束仍需在真实版本中评估构建状态和资源消耗。
Nebula
- Tag 和 Edge 属性需要显式定义;
CREATE ... IF NOT EXISTS不会自动合并新属性,SDK 的增量应用会检查并执行属性 ADD;- Schema 从 MetaD 向数据面传播存在时间窗口;
- 索引创建后需要重建,属性查询依赖可用索引;
- Portable Schema 不支持唯一索引;
- 当前反查不包含完整索引元数据,因此迁移预览必须结合发布记录或原生检查。
回滚策略
DDL 回滚往往不能等同于应用版本回滚。一个可靠方案应分别考虑:
- 应用回滚:旧版本能否读取扩展后的 Schema;
- 数据回滚:回填或转换后的数据如何恢复;
- 索引回滚:删除或重建索引是否影响查询;
- 结构回滚:后端是否支持安全撤销 DDL;
- 任务回滚:正在运行的导入任务使用哪个 Schema 版本。
最安全的方式通常是保留兼容旧结构,在确认新版本稳定后再执行收缩。破坏性操作前必须使用数据库原生工具完成备份和恢复演练;Graph SDK 不负责恢复被删除的结构和数据。
常见误区
计划为 ADDITIVE,就一定可以自动上线吗?
不一定。新增索引可能消耗大量资源,新增“必填”属性可能需要全量回填,新边类型可能影响抽取和查询。风险等级反映结构差异,不替代容量和业务评估。
Diff 中没有变化,就证明数据库符合业务要求吗?
不一定。反查可能不完整,历史数据可能违反业务规则,索引可能尚未就绪,应用和数据库版本也可能不兼容。
可以根据 removals 自动执行删除吗?
不可以。反查缺失和真正多余是两回事。删除前必须确认 inspection 完整性、数据量、调用方、备份和审批。
Schema 版本可以直接从数据库反查吗?
不应依赖这一点。Graph Schema 的业务版本属于上层发布元数据,当前不会自动编译为后端 DDL。应用应独立持久化版本历史。
生产检查清单
- 是否明确期望 Schema、实际 Schema 和已发布版本;
- 是否保存并展示反查完整性、warning 和 unsupported metadata;
- 是否分别审查 additions、changes 和 removals;
- 是否检查历史数据能否满足新类型、必填和唯一性要求;
- 是否为索引构建和数据回填评估容量及时间;
- 是否使用 Expand and Contract 处理不兼容变化;
- 是否让新旧应用在发布窗口内保持兼容;
- 是否将 DDL、数据回填和破坏性清理拆成独立阶段;
- 是否为
REVIEW_REQUIRED和DESTRUCTIVE设置审批; - 是否准备并演练应用、数据和数据库恢复方案;
- 是否在真实后端运行关键写入、查询和 Explain 回归;
- 是否记录 Space、Schema 版本、执行人、结果和验证结论。
迁移的第一步是获得可信的数据库当前状态。下一章 Schema 反查 将说明反查结果包含什么、为什么可能不完整,以及如何安全使用它。