| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- 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
- }
|