widerow.go 7.3 KB

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