widerow.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. package migrate
  2. import (
  3. "TsDB2/DB/core/types"
  4. "fmt"
  5. )
  6. // ============================ 行宽超限时的自动降级 ============================
  7. //
  8. // 【问题】VRTX 单行的【总占用】上限是 types.MaxRowDataSize (8171 = 一页至少装下两行,
  9. // 见 types.CheckRowSize)。它是 【schema 属性】而不是行数据属性: 一张建出来就超限的表,
  10. // 之后每一行都写不进去, 所以引擎在 CreateTable / AddField 就拒绝。引擎那句提示
  11. // ("请把宽度超过 2048 字节的列改为动态列, 或把大列拆到附表") 对"自己写代码建表"够用 ——
  12. // 改 schema 重跑即可。
  13. //
  14. // 但迁移场景不一样: schema 是从源库读来的。用户没法"把源表的一个列宽改小"就为了迁过去,
  15. // 于是 40 列的表里只因 8 个大 varchar 就整张迁不动, 数据跟着陪葬。
  16. //
  17. // 【处理】建表前先算一次行宽; 超限就把【行内宽度 > minInlineBytes(256) 字节的定长字符串 /
  18. // 二进制列】降为动态列 (FieldType_Dynamicbyte: 行内只留 6 字节堆引用, 内容整体落堆页)。
  19. // 降完仍超限才报错 —— 那时已无列可降, 说明这张表的结构本身与"一页两行"冲突, 只能拆表。
  20. //
  21. // 【为什么这条线是 256】不是拍出来的:
  22. // - 256 及以下是"够小到不该进堆"的档位。16..256 这几档的列统共才几十到两百多字节,
  23. // 降成动态反而多付一整套堆开销 (行内 6B 引用 + 堆块 4B 长度前缀 + 堆页记账), 还把
  24. // 本来可索引、可定长比较的短列拖进堆;
  25. // - 真正吃掉行宽的是 512 / 1024 / 2048 这几档: 几列 2048 就能顶到 8171。只降它们,
  26. // 既解决问题, 又只牺牲"本来就该进堆"的大列;
  27. // - 它也与引擎自己的分界同向: 声明超过 types.MaxInlineColumnWidth(2048) 的列本来就落
  28. // 动态列 (见 bucketColumn), 这里只是把 512..2048 这几档【按需】也推进去。
  29. //
  30. // 【降级必须报出来】降级是【改类型】(存的东西换了一种放法), 所以每一列都记进
  31. // sc.DegradedColumns, 由 runTable 汇进迁移报告 —— 与 prepareForVRTX 里"主键降级"同一条
  32. // 原则: 静默改结构比报错更糟。
  33. //
  34. // 【副作用同样是可见的】动态列不能做索引键 (core.CreateIndex 直接拒, 见 ErrIndexDynamicField),
  35. // 所以被降级的列上原有的索引会在 VRTX.CreateTable 里被跳过并记进 SkippedIndexes。用户看到的是
  36. // "这一列降级了" + "那条索引没建成"两句, 而不是"数据没迁过来"。
  37. //
  38. // 【为什么不像"最小改动"那样只降够用的几列】降哪些列取决于列的遍历顺序, 同一个源库两次迁移
  39. // 可能降出不同的结构; 而"所有 >256 的列一起降"是源库结构的确定性函数 —— 可复现、可对照, 也给
  40. // 留了余量 (之后加列不至于又贴上限)。代价是可能多降几列, 由报告如实告知。
  41. // minInlineBytes 行内宽度超过它才值得降为动态列。取值理由见文件头。
  42. const minInlineBytes = 256
  43. // fitRowWidthForVRTX 行宽闸门: 装得下就什么都不做; 装不下就把宽列降为动态列; 降完还装不下才报错。
  44. //
  45. // 返回被降级的列数 (0 表示没动过), 供调用方记日志。表结构就地修改。
  46. func fitRowWidthForVRTX(s *TableSchema) (int, error) {
  47. if types.CheckRowSize(vrtxFields(s)) == nil {
  48. return 0, nil
  49. }
  50. demoted := demoteWideColumns(s)
  51. if demoted == 0 {
  52. // 一个可降的列都没有 (>256 的定长字符串列不存在), 原样把引擎的口径报出去 ——
  53. // 它已经列出了各列宽度与"拆表"的建议。
  54. return 0, fmt.Errorf("表 %s: %w", s.Name, types.CheckRowSize(vrtxFields(s)))
  55. }
  56. if err := types.CheckRowSize(vrtxFields(s)); err != nil {
  57. // 已经降过还是装不下: 必须说清"降过了"。否则用户会照着引擎那句"请把宽度超过
  58. // 2048 字节的列改为动态列"去找一列, 而那一列早已是动态列 —— 无处可改。
  59. return demoted, fmt.Errorf("表 %s: %w (已把 %d 个宽度超过 %d 字节的定长字符串列降为动态列, "+
  60. "行仍装不进一页两行 —— 只能拆表, 或只迁其中一部分列)",
  61. s.Name, err, demoted, minInlineBytes)
  62. }
  63. return demoted, nil
  64. }
  65. // demoteWideColumns 把宽度 > minInlineBytes 的定长字符串 / 二进制列降为动态列, 返回降了几个。
  66. //
  67. // 按列顺序遍历 (确定性), 且只降"类型本身能变长"的列:
  68. // - 已是动态列的跳过 —— 重复调用是幂等的, 不会把一次降级重复记账;
  69. // - 非字符串家族 (整数 / 浮点 / 定点 / UUID / 时间...) 不碰 —— 变长档装不了它们,
  70. // 降了是改数据语义 (见 types.IsStringType 的说明);
  71. // - 行内宽度 <= minInlineBytes 的跳过, 理由见文件头。
  72. //
  73. // 行存储键列 (主键 / 时序键) 天然不会命中: prepareForVRTX 已经保证它是 8 字节整数列。
  74. func demoteWideColumns(s *TableSchema) int {
  75. n := 0
  76. for i := range s.Columns {
  77. c := &s.Columns[i]
  78. if c.IsDynamic() || !types.IsStringType(c.Type) || c.Size() <= minInlineBytes {
  79. continue
  80. }
  81. width := c.Size()
  82. c.Type = types.FieldType_Dynamicbyte
  83. // 动态列【不保留声明长度】: Flag 低 5 位必须为 0 (见 types.ValidateColumnParam 的
  84. // default 分支)。留着原宽度码会让 FieldSize 按定长算, 行宽又变回去。
  85. c.Param = 0
  86. n++
  87. s.DegradedColumns = append(s.DegradedColumns, fmt.Sprintf(
  88. "%s: 行内宽度 %d 字节, 为使整行装进一页两行已降为动态列 "+
  89. "(行内只留 6 字节堆引用, 内容整体落堆页; 该列不再是索引键)",
  90. c.Name, width))
  91. }
  92. return n
  93. }