table_index.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. package core
  2. import (
  3. "cmp"
  4. "encoding/binary"
  5. "fmt"
  6. "slices"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  8. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  10. "git.ycl.pub/yl32940/TsDB2/DB/core/util"
  11. )
  12. // CreateIndex 建二级索引。
  13. //
  14. // 这里先校验分量列能不能做键, 再交给 IndexMetaPage 建 —— 拆成两层是因为
  15. // IndexMetaPage 只拿得到索引自己的元信息, 看不到表的字段类型:
  16. // - 分量列不存在 → ErrFieldNotFound;
  17. // - 分量是动态列 → ErrIndexDynamicField。动态列在行内只存 6B 堆引用, 值 != 引用,
  18. // 按值查找永远不可能命中; 建出来既查不到也不会被查询侧选中 (buildKeyLayout 会跳过),
  19. // 只会白占空间、白写。拦在建的这一步, 而不是等用到才发现;
  20. // - 分量是 blob 列 → ErrIndexBlobField。blob 列行内只存 8B BlobRef (页号 + 长度),
  21. // 拿它当索引键等于按页号排序/查找 —— 索引里存的不是内容, 命中与否全看内容落在哪一页;
  22. // - 复合索引 (IndexTypeCompose) 的分量列宽度不再强制 8 字节 ——
  23. // 底层已升级为 B+ 树, 按分量真实宽度拼定长键, 溢出闸门 (checkIndexSpillFloor)
  24. // 负责兜住"一页两行"的行宽上限。
  25. //
  26. // unique 为可选 (只取第一个值), 见 IndexMetaPage.CreateIndex: 传 true 建的是唯一索引,
  27. // 同一索引键只允许一行; 任一分量为 NULL 的行不进该索引, 因此多个 NULL 天然共存。
  28. // 唯一约束在写入路径 (InsertRow / UpdateByKey) 上校验, 违反时返回 ErrDuplicatedKey。
  29. func (table *Table) CreateIndex(idxName string, typ page.IndexType, fields []byte, unique ...bool) (*IndexMeta, error) {
  30. if table.db != nil && table.db.HasActiveTransaction() {
  31. return nil, errDDLInTransaction("CreateIndex")
  32. }
  33. defer table.db.lockOp()()
  34. for i, fi := range fields {
  35. if int(fi) >= len(table.Field) {
  36. return nil, fmt.Errorf("%w: 索引 %q 的第 %d 个分量", ErrFieldNotFound, idxName, i+1)
  37. }
  38. ft := table.Field[fi].FieldType
  39. if types.IsDynamic(ft) {
  40. return nil, fmt.Errorf("%w (索引 %q 的第 %d 个分量)", ErrIndexDynamicField, idxName, i+1)
  41. }
  42. // v10: blob 列行内只有 8B BlobRef (页号 + 长度), 拿它当索引键等于按页号排序/查找 ——
  43. // 索引里存的不是内容, 命中与否全看内容落在哪一页. 与动态列同等拦下.
  44. if types.IsBlob(ft) {
  45. return nil, fmt.Errorf("%w (索引 %q 的第 %d 个分量)", ErrIndexBlobField, idxName, i+1)
  46. }
  47. }
  48. // 行存储索引 (主键/时序) 的分量列不允许可空: 它是行的定位键, 写入路径按它
  49. // 提取索引键 (extractIndexKey), NULL 分量不提取 —— 键提取不出来时删除/更新
  50. // 对目标索引静默失效 (ErrNotFound 按索引逐个吞掉, 不报错也不动数据)。
  51. // 建索引这一步就拦下, 比"数据写进去之后删不掉"好查得多。
  52. if h, ok := index.Compose.GetHandler(typ); ok && handlerStoresRows(h) {
  53. for _, fid := range fields {
  54. if int(fid) < len(table.Field) && table.Field[fid].IsNullable() {
  55. return nil, fmt.Errorf("行存储索引 %q 的分量列 %q 声明了可空 (定位键必须非空, 请去掉 nullable 标记)",
  56. idxName, table.Field[fid].FieldName)
  57. }
  58. }
  59. }
  60. // 溢出闸门: 分量列一旦建索引就不能再值级溢出, 行内下界随之抬升。
  61. // 下界超过"一页两行"的配额时必须在这里拒绝 —— 否则这张表从建索引成功起就再也压不
  62. // 进两行 (单条插入仍会"成功"地落在 8159..16332 之间, 页内顺序/有序插入/页内重建
  63. // 一起退化, 而且从现象上完全看不出是索引造成的)。
  64. if err := table.checkIndexSpillFloor(fields); err != nil {
  65. return nil, fmt.Errorf("%w: 索引 %q: %v", ErrIndexRowTooWide, idxName, err)
  66. }
  67. m, err := table.indexMeta.CreateIndex(idxName, typ, fields, unique...)
  68. if err != nil {
  69. return nil, err
  70. }
  71. // 回填存量行。
  72. // 旧实现只建元数据, 索引里一条记录都没有: 建索引之前就存在的行永远查不到,
  73. // 而引擎又没有公开的"重建全部二级索引"接口 —— 数据在、索引空, 是最难排查的
  74. // 一类不一致。建索引这一步必须自己把存量行灌进去。
  75. if err := table.backfillIndex(m); err != nil {
  76. // 回填失败不能留下"半个索引": 元数据还在但内容不全, 比没有索引更危险
  77. // (查询会选中它, 于是稳定地查不到行)。摘掉元数据, 让错误显式冒出来。
  78. // 走 table.DropIndex 而不是 indexMeta.DropIndex: 后者只软删元数据, 会把
  79. // 回填到一半的树页留成孤儿页。
  80. _ = table.DropIndex(idxName)
  81. return nil, err
  82. }
  83. if table.db != nil {
  84. if err := table.db.ddlBoundary("CreateIndex"); err != nil {
  85. return nil, err
  86. }
  87. }
  88. return m, nil
  89. }
  90. // backfillIndex 把表里已有的行灌进一个刚建好的二级索引。
  91. //
  92. // 与写入侧共用 extractIndexKey + 处理器 Insert, 因此:
  93. // - 键编码与后续单条插入完全一致, 不会出现"回填进去了却查不到"的错位;
  94. // - 老行 (该列在其自身的 schema 版本里还不存在, 例如 AddField 之前插入的行)
  95. // 自动跳过, 与"单条插入时缺失列不落索引"的语义保持一致;
  96. // - 动态列在 CreateIndex 已被拒绝, 这里不必再判。
  97. //
  98. // 行存储索引 (主键/时序) 不在此处理: 它们的键就是 pkID, 行数据本就由写入路径维护。
  99. func (table *Table) backfillIndex(m *IndexMeta) error {
  100. if m == nil || indexStoresRows(m) {
  101. return nil
  102. }
  103. primMeta, primHandler, ok := table.indexMeta.rowStoreMeta()
  104. if !ok || primMeta == nil || primHandler == nil {
  105. return nil // 表里没有承载行的索引, 也就没有存量行
  106. }
  107. // 收集 → 排序 → 批量建树。
  108. // 不能逐条 handler.Insert: 那是 n 次 O(log n) 的树写放大, 千万行级别慢到
  109. // 不可接受; 索引重建本来就有 O(n) 的批量路径, 这里直接复用。
  110. var entries [][]byte
  111. for _, dpID := range primHandler.GetDataPageIDs(table.handle, primMeta.EntryPage) {
  112. for _, storeRow := range primHandler.ReadDataPageRows(table.handle, dpID) {
  113. if len(storeRow) < 8 {
  114. continue
  115. }
  116. secKey := extractIndexKey(storeRow[8:], m, table.Field, table.versions)
  117. if secKey == nil {
  118. continue // 该行没有这个键 (列不在它的 schema 版本里)
  119. }
  120. entry := make([]byte, len(secKey)+8)
  121. copy(entry, secKey)
  122. // 条目尾部是 pkID (8B BE), 与 rebuildIndexes 的二级条目同一布局
  123. binary.BigEndian.PutUint64(entry[len(secKey):], binary.BigEndian.Uint64(storeRow[:8]))
  124. entries = append(entries, entry)
  125. }
  126. }
  127. if len(entries) == 0 {
  128. // 没有任何存量行进这个索引 → 保持空树 (EntryPage==0), 后续插入照常开根
  129. return nil
  130. }
  131. // 按复合键 (secKey, pkID) 升序排序: 二级树的复合分隔键与叶内复合二分要求
  132. // 全树按复合键全局有序, 同 key 的条目必须再按 pkID 排
  133. slices.SortStableFunc(entries, func(a, b []byte) int {
  134. if c := compareBytes(a[:len(a)-8], b[:len(b)-8]); c != 0 {
  135. return c
  136. }
  137. return cmp.Compare(binary.BigEndian.Uint64(a[len(a)-8:]), binary.BigEndian.Uint64(b[len(b)-8:]))
  138. })
  139. // 唯一索引进来了要顺手验一遍存量数据: 库里本来就有重复值时, 这个约束是【建不出来】的。
  140. // 硬建下去只会得到一个"违反自身约束"的索引 (同一键挂着两行), 之后点查可能返回其中
  141. // 任意一行, 而且从索引上完全看不出它已经坏了。宁可在建索引这一步报错, 由调用方决定
  142. // 是清理数据还是改用非唯一索引。排序后重复键必然相邻, 一次线性扫描即可。
  143. if m.Unique() {
  144. for i := 1; i < len(entries); i++ {
  145. if compareBytes(entries[i-1][:len(entries[i-1])-8], entries[i][:len(entries[i])-8]) == 0 {
  146. return fmt.Errorf("%w: 索引 %q 的存量数据中已存在重复键 (pkID %d 与 %d), 无法建成唯一索引",
  147. ErrDuplicatedKey, util.BytesToString(m.IndexName[:]),
  148. binary.BigEndian.Uint64(entries[i-1][len(entries[i-1])-8:]),
  149. binary.BigEndian.Uint64(entries[i][len(entries[i])-8:]))
  150. }
  151. }
  152. }
  153. if err := buildSecondaryFromEntries(table.handle, m, entries); err != nil {
  154. return err
  155. }
  156. table.indexMeta.saveMeta(m)
  157. table.indexMeta.InvalidateRowCache()
  158. return nil
  159. }
  160. func (table *Table) DropIndex(idx string) error {
  161. defer table.db.lockOp()()
  162. // DROP INDEX 必须把整棵索引树还给空闲链。
  163. // 旧实现只把元数据打上软删除标记 (Flag|=0x80) 并从 metas 摘掉: 根指针一丢, 整棵树
  164. // (内部节点 + 叶子) 就既不可达、又不在空闲链上 —— 变成永久孤儿页, 文件只涨不缩。
  165. // 实测 20 万行的一棵二级索引约 197 页; bench_10m 里 9.9M 行的 Dept 索引是数万页,
  166. // 正是那批孤儿页的主因。
  167. // 注意: indexMeta.DropIndex 会用 append 挪动 metas 元素, 之后 &metas[i] 就失效了,
  168. // 所以必须在摘除之前把 IndexType / EntryPage 快照出来。
  169. var itype page.IndexType
  170. var rootPID uint32
  171. if m := table.indexMeta.findIndexMetaByName(idx); m != nil {
  172. itype, rootPID = m.IndexType, m.EntryPage
  173. }
  174. if err := table.indexMeta.DropIndex(idx); err != nil {
  175. return err
  176. }
  177. // 旧结构此刻已不可达 (元数据已摘), 归还它的全部页。
  178. releaseIndexPages(table, itype, rootPID)
  179. return nil
  180. }
  181. // BuildIndexKey 按索引定义, 把各分量值编码成索引键 (与写入侧 extractIndexKey 同一编码)。
  182. //
  183. // 复合索引的键是各分量定长拼接, 外部无法凭空构造 —— 因此点查/删除/更新复合索引
  184. // (SearchByIndex / DeleteRow / UpdateRow) 都要先经这里把值编成键。
  185. // values 的顺序与建索引时给的 fieldIDs 顺序一致, 个数必须相等。
  186. func (table *Table) BuildIndexKey(idxName string, values ...interface{}) ([]byte, error) {
  187. m := table.indexMeta.findIndexMetaByName(idxName)
  188. if m == nil {
  189. return nil, ErrIndexNotFound
  190. }
  191. n := int(m.FieldCnt)
  192. if n != len(values) {
  193. return nil, fmt.Errorf("索引 %q 需要 %d 个分量值, 实际给了 %d 个", idxName, n, len(values))
  194. }
  195. key := make([]byte, 0, n*8)
  196. for k := 0; k < n; k++ {
  197. fi := int(m.FieldIDs[k])
  198. if fi >= len(table.Field) {
  199. return nil, ErrFieldNotFound
  200. }
  201. ft := table.Field[fi].FieldType
  202. if table.Field[fi].IsDropped() {
  203. return nil, fmt.Errorf("索引 %q 的第 %d 个分量列已被删除", idxName, k+1)
  204. }
  205. if types.IsDynamic(ft) {
  206. return nil, fmt.Errorf("索引 %q 的第 %d 个分量是动态列, 不能作为索引键", idxName, k+1)
  207. }
  208. if types.IsBlob(ft) {
  209. return nil, fmt.Errorf("索引 %q 的第 %d 个分量是 blob 列, 不能作为索引键", idxName, k+1)
  210. }
  211. enc, err := encodeIndexKeySafeWithParam(values[k], ft, table.fieldParam(fi))
  212. if err != nil {
  213. return nil, fmt.Errorf("索引 %q 的第 %d 个分量: %w", idxName, k+1, err)
  214. }
  215. if want := int(table.Field[fi].Width()); len(enc) != want {
  216. return nil, fmt.Errorf("索引 %q 的第 %d 个分量: 编码 %d 字节, 列宽 %d 字节",
  217. idxName, k+1, len(enc), want)
  218. }
  219. key = append(key, enc...)
  220. }
  221. return key, nil
  222. }
  223. // SearchByIndex 通过索引搜索一行
  224. func (table *Table) SearchByIndex(idxName string, key []byte) ([]types.FieldValue, error) {
  225. defer table.db.lockOp()()
  226. // 元信息只查一次, 命中后复用给 searchAt, 避免重复扫描 metas
  227. meta := table.indexMeta.findIndexMetaByName(idxName)
  228. if meta == nil {
  229. return nil, ErrFieldNotFound
  230. }
  231. storeRow, found := table.indexMeta.searchAt(meta, key)
  232. // storeRow = [pkID(8B BE) + rowData]
  233. if !found || len(storeRow) < 8 {
  234. return nil, page.ErrNotFound
  235. }
  236. return table.deserializeRow(storeRow[8:]), nil
  237. }
  238. // DeleteRow 通过索引删除一行
  239. func (table *Table) DeleteRow(idxName string, key []byte) error {
  240. defer table.db.lockOp()()
  241. // 一次定位: 既用于释放该行的堆页引用, 也下传给索引层删除 —— 避免同一行被查两次树
  242. storeRow, found := table.indexMeta.SearchByIndex(idxName, key)
  243. if !found || len(storeRow) < 8 {
  244. return page.ErrNotFound
  245. }
  246. if err := table.indexMeta.DeleteByKey(idxName, key, storeRow, table.Field, table.activeVersion, table.versions); err != nil {
  247. return err
  248. }
  249. // 行已删除 → 释放其动态字段占用的堆块; 计数归零的堆页会被摘链回收
  250. table.heapReleaseRow(storeRow[8:])
  251. table.info.TotalRows--
  252. table.saveTableInfo()
  253. if table.onWrite != nil {
  254. table.onWrite()
  255. }
  256. return nil
  257. }
  258. // checkUniqueUpdate 更新前的唯一性预检 —— 必须早于 serializeRow。
  259. //
  260. // 用 serializeRowForCheck 试算行字节 (全程不碰堆), 再交给索引层比对。冲突时旧行分毫
  261. // 未动, 动态堆块也没有被覆写或释放。代价是更新路径多一次序列化, 但更新本来就要回表、
  262. // 定位、重写行, 这笔开销可接受; 换来的是"要么全改, 要么全不动"。
  263. //
  264. // 为什么不能只靠 UpdateByKey 里那次预检: 那时 serializeRow 已经跑过了, 旧行的动态
  265. // 列早就被改成新值, 而固定列与索引条目还是旧的 —— 行被改了一半。
  266. func (table *Table) checkUniqueUpdate(values []types.FieldValue, oldRowData []byte, pkID uint64) error {
  267. // 没有唯一索引的表占绝大多数: 预检唯一的目的就是拦唯一冲突, 此时整行试序列化
  268. // (serializeRowForCheck) 纯属浪费 —— 后续真正的 serializeRow 仍会校验值宽度,
  269. // 非法值照样在写入前被拦下, 只是少了这次纯为唯一性服务的试算。
  270. if !table.indexMeta.HasUniqueIndex() {
  271. return nil
  272. }
  273. scratch, err := table.serializeRowForCheck(values, oldRowData)
  274. if err != nil {
  275. return err
  276. }
  277. return table.indexMeta.CheckUniqueForKey(scratch, pkID, table.Field, table.activeVersion, table.versions)
  278. }
  279. // UpdateRow 通过索引更新一行
  280. func (table *Table) UpdateRow(idxName string, key []byte, values []types.FieldValue) error {
  281. defer table.db.lockOp()()
  282. // 先取旧行: 动态字段可复用旧行已占用的堆块, 避免每次更新都产生堆垃圾。
  283. // 该行同时下传给 UpdateByKey, 免去索引层再次回表。
  284. storeRow, found := table.indexMeta.SearchByIndex(idxName, key)
  285. if !found {
  286. return ErrFieldNotFound
  287. }
  288. if len(storeRow) < 8 {
  289. return ErrFieldNotFound
  290. }
  291. pkID := binary.BigEndian.Uint64(storeRow[:8])
  292. // 唯一性预检放在序列化之前: 见 checkUniqueUpdate。
  293. if err := table.checkUniqueUpdate(values, storeRow[8:], pkID); err != nil {
  294. return err
  295. }
  296. newRowData, err := table.serializeRow(values, storeRow[8:])
  297. if err != nil {
  298. return err
  299. }
  300. if err := table.indexMeta.UpdateByKey(key, newRowData, pkID, storeRow, table.Field, table.activeVersion, table.versions); err != nil {
  301. return err
  302. }
  303. if table.onWrite != nil {
  304. table.onWrite()
  305. }
  306. return nil
  307. }
  308. // SearchByIndexWithPK 同 SearchByIndex, 但额外把该行的真实行号一并返回。
  309. //
  310. // 行号是行的 8 字节前缀, 不在字段值里 —— 上层要把它带回对象, 之后才能按行号
  311. // 精确 Update/Delete (见 ORM 的 modelCore.rowID)。
  312. func (table *Table) SearchByIndexWithPK(idxName string, key []byte) (uint64, []types.FieldValue, error) {
  313. defer table.db.lockOp()()
  314. meta := table.indexMeta.findIndexMetaByName(idxName)
  315. if meta == nil {
  316. return 0, nil, ErrFieldNotFound
  317. }
  318. storeRow, found := table.indexMeta.searchAt(meta, key)
  319. // storeRow = [pkID(8B BE) + rowData]
  320. if !found || len(storeRow) < 8 {
  321. return 0, nil, page.ErrNotFound
  322. }
  323. return binary.BigEndian.Uint64(storeRow[:8]), table.deserializeRow(storeRow[8:]), nil
  324. }