package migrate import ( "git.ycl.pub/yl32940/TsDB2/DB/core/types" "fmt" ) // ============================ 行宽超限时的自动降级 ============================ // // 【问题】VRTX 的行内预算上限是 types.MaxInlineRowData (8159 = 一页至少装下两行, 见 // types.CheckRowSpillFloor)。它是 【schema 属性】而不是行数据属性: 一张建出来就超限的表, // 之后每一行都写不进去, 所以引擎在 CreateTable / AddField 就拒绝。引擎那句提示 // ("请把宽度超过 2048 字节的列改为动态列, 或把大列拆到附表") 对"自己写代码建表"够用 —— // 改 schema 重跑即可。 // // v9 起引擎会【按实际行宽】把未建索引的字符串列溢出到堆页 (行内只留 6B 引用), 所以"声明 // 字段区之和"不再等于行宽: 几十列 2048 的字符串表, 只要都没建索引, 建表与写入都不再需要 // 任何干预。真正撑满行内下界、必须在这里降级的, 是【不可溢出的列】—— 定长列、行存储键, // 以及【建了索引的列】(索引键按行内字节拼, 一溢出键就成了页号)。于是触发条件从"声明行宽 // 超限"改为"溢出下界超限", 判据见 rowSpillFloor。 // // 但迁移场景不一样: schema 是从源库读来的。用户没法"把源表的一个列宽改小"就为了迁过去, // 于是 40 列的表里只因 8 个大 varchar 就整张迁不动, 数据跟着陪葬。 // // 【处理】建表前先算一次行宽; 超限就把【行内宽度 > minInlineBytes(256) 字节的定长字符串 / // 二进制列】降为动态列 (FieldType_Dynamicbyte: 行内只留 6 字节堆引用, 内容整体落堆页)。 // 降完仍超限才报错 —— 那时已无列可降, 说明这张表的结构本身与"一页两行"冲突, 只能拆表。 // // 【为什么这条线是 256】不是拍出来的: // - 256 及以下是"够小到不该进堆"的档位。16..256 这几档的列统共才几十到两百多字节, // 降成动态反而多付一整套堆开销 (行内 6B 引用 + 堆块 4B 长度前缀 + 堆页记账), 还把 // 本来可索引、可定长比较的短列拖进堆; // - 真正吃掉行宽的是 512 / 1024 / 2048 这几档: 几列 2048 就能顶到 8171。只降它们, // 既解决问题, 又只牺牲"本来就该进堆"的大列; // - 它也与引擎自己的分界同向: 声明超过 types.MaxInlineColumnWidth(2048) 的列本来就落 // 动态列 (见 bucketColumn), 这里只是把 512..2048 这几档【按需】也推进去。 // // 【降级必须报出来】降级是【改类型】(存的东西换了一种放法), 所以每一列都记进 // sc.DegradedColumns, 由 runTable 汇进迁移报告 —— 与 prepareForVRTX 里"主键降级"同一条 // 原则: 静默改结构比报错更糟。 // // 【副作用同样是可见的】动态列不能做索引键 (core.CreateIndex 直接拒, 见 ErrIndexDynamicField), // 所以被降级的列上原有的索引会在 VRTX.CreateTable 里被跳过并记进 SkippedIndexes。用户看到的是 // "这一列降级了" + "那条索引没建成"两句, 而不是"数据没迁过来"。 // // 【为什么不像"最小改动"那样只降够用的几列】降哪些列取决于列的遍历顺序, 同一个源库两次迁移 // 可能降出不同的结构; 而"所有 >256 的列一起降"是源库结构的确定性函数 —— 可复现、可对照, 也给 // 留了余量 (之后加列不至于又贴上限)。代价是可能多降几列, 由报告如实告知。 // minInlineBytes 行内宽度超过它才值得降为动态列。取值理由见文件头。 const minInlineBytes = 256 // rowSpillFloor 这张表在 VRTX 里的【行内下界】: 值级溢出之后一行最少占多少字节。 // // 与 types.CalcSpillFloor 同口径, 但带上"不可溢出列" = 行存储键列 (主键 / 时序键) + 所有 // 二级索引的分量列。用它而不是 types.CheckRowSize (后者假定"全部字符串列都可溢出") 判断 // 要不要降级 —— 否则带索引的宽字符串表会被判成"溢出就装下了", 直到建索引那一步才炸。 func rowSpillFloor(s *TableSchema) int { return types.CalcSpillFloor(vrtxFields(s), unspillableColumns(s)) } // unspillableColumns 标出不可溢出的列 (行存储键 + 二级索引分量), 位置与 vrtxFields 对齐。 func unspillableColumns(s *TableSchema) []bool { un := make([]bool, len(s.Columns)) mark := func(name string) { if i := columnIndex(s, name); i >= 0 { un[i] = true } } mark(s.PKColumn) for _, name := range s.RowStoreColumns { mark(name) } for _, ix := range s.Indexes { for _, name := range ix.Columns { mark(name) } } return un } // fitRowWidthForVRTX 行宽闸门: 装得下就什么都不做; 装不下就把宽列降为动态列; 降完还装不下才报错。 // // 返回被降级的列数 (0 表示没动过), 供调用方记日志。表结构就地修改。 func fitRowWidthForVRTX(s *TableSchema) (int, error) { if types.CheckRowSpillFloor(vrtxFields(s), unspillableColumns(s)) == nil { return 0, nil } demoted := demoteWideColumns(s) if demoted == 0 { // 一个可降的列都没有 (>256 的定长字符串列不存在), 原样把引擎的口径报出去 —— // 它已经列出了各列宽度与"拆表"的建议。 return 0, fmt.Errorf("表 %s: %w", s.Name, types.CheckRowSpillFloor(vrtxFields(s), unspillableColumns(s))) } if err := types.CheckRowSpillFloor(vrtxFields(s), unspillableColumns(s)); err != nil { // 已经降过还是装不下: 必须说清"降过了"。否则用户会照着引擎那句"请把宽度超过 // 2048 字节的列改为动态列"去找一列, 而那一列早已是动态列 —— 无处可改。 return demoted, fmt.Errorf("表 %s: %w (已把 %d 个宽度超过 %d 字节的定长字符串列降为动态列, "+ "行仍装不进一页两行 —— 只能拆表, 或只迁其中一部分列)", s.Name, err, demoted, minInlineBytes) } return demoted, nil } // demoteWideColumns 把宽度 > minInlineBytes 的定长字符串 / 二进制列降为动态列, 返回降了几个。 // // 按列顺序遍历 (确定性), 且只降"类型本身能变长"的列: // - 已是动态列的跳过 —— 重复调用是幂等的, 不会把一次降级重复记账; // - 非字符串家族 (整数 / 浮点 / 定点 / UUID / 时间...) 不碰 —— 变长档装不了它们, // 降了是改数据语义 (见 types.IsStringType 的说明); // - 行内宽度 <= minInlineBytes 的跳过, 理由见文件头。 // // 行存储键列 (主键 / 时序键) 天然不会命中: prepareForVRTX 已经保证它是 8 字节整数列。 func demoteWideColumns(s *TableSchema) int { n := 0 for i := range s.Columns { c := &s.Columns[i] if c.IsDynamic() || !types.IsStringType(c.Type) || c.Size() <= minInlineBytes { continue } width := c.Size() c.Type = types.FieldType_Dynamicbyte // 动态列【不保留声明长度】: Flag 低 5 位必须为 0 (见 types.ValidateColumnParam 的 // default 分支)。留着原宽度码会让 FieldSize 按定长算, 行宽又变回去。 c.Param = 0 n++ s.DegradedColumns = append(s.DegradedColumns, fmt.Sprintf( "%s: 行内宽度 %d 字节, 为使整行装进一页两行已降为动态列 "+ "(行内只留 6 字节堆引用, 内容整体落堆页; 该列不再是索引键)", c.Name, width)) } return n }