query_exec.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. package core
  2. import (
  3. "container/heap"
  4. "encoding/binary"
  5. "slices"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  7. )
  8. // executeScan 执行索引扫描或全表扫描, 将反序列化后的行传递给回调。
  9. // budget > 0 时最多交付 budget 行 (LIMIT+OFFSET 场景): 二级索引回表据此
  10. // 提前收工, 而不是把命中的上千行全部解出再丢掉 —— Where(hash=重).First()
  11. // 这类点查只允许解 1 行。
  12. func (q *Query) executeScan(callback func(pkID uint64, row []types.FieldValue) bool, plan *scanPlan, budget int) error {
  13. if plan != nil {
  14. return q.indexScan(plan, callback, budget)
  15. }
  16. return q.fullTableScan(callback)
  17. }
  18. // indexScan 使用索引扫描并按"先物化、后回表"两阶段执行。
  19. //
  20. // 为什么不能像旧实现那样在 RangeScan 回调里直接回表 + 触发用户 callback:
  21. // RangeScan 是沿叶子双向链 (PageHdr.NextPage) 推进的裸游标, 而用户的 Scan 回调
  22. // 常常会对【同一棵索引】做 Update/Delete —— 删旧条目 + 插新条目会触发叶分裂/合并,
  23. // 被合并掉的叶子页会被登记回收, 下一个 Commit/drain 时该页被清零并改写成空闲链节点
  24. // (空闲链同样用 PageHdr.NextPage 串接)。此时游标再读该页的 NextPage, 拿到的就是
  25. // 空闲链指针, 于是跳到一张 type=0 的空页上 (实测 panic: bad page type=0)。
  26. //
  27. // 物化把"遍历"与"修改"彻底分开: 遍历阶段树保持只读, 游标始终有效; 修改发生在
  28. // 遍历结束之后, 不再影响游标。二级索引只物化 pkID(8B/行), 内存开销与结果集成正比
  29. // 且远小于整行物化; rowStore 索引物化的是行的快照 (必须拷贝, 因为它是页内缓冲视图)。
  30. func (q *Query) indexScan(plan *scanPlan, callback func(pkID uint64, row []types.FieldValue) bool, budget int) error {
  31. // 判断 OrderBy 是否需要反向 (只对单区间有定义; 多区间已强制走排序管线)
  32. reverse := false
  33. if len(plan.ranges) == 1 && len(q.orderBys) == 1 {
  34. idxField := q.fieldNameFromMeta(plan.meta)
  35. if q.orderBys[0].Field == idxField && q.orderBys[0].Descending {
  36. reverse = true
  37. }
  38. }
  39. if plan.rowStore {
  40. // 承载行数据的索引: RangeScan 返回 storeRow = [pkID(8B) + rowData]。
  41. // storeRow 指向页缓冲, 遍历结束后页可能被回收改写, 必须拷贝一份快照。
  42. // 多区间 (IN 展开) 时不同值可能编码出重叠区间 → 按 pkID 去重。
  43. var rows [][]byte
  44. var seen map[uint64]struct{}
  45. if len(plan.ranges) > 1 {
  46. seen = make(map[uint64]struct{}, len(plan.ranges)*4)
  47. }
  48. for _, kr := range plan.ranges {
  49. if err := plan.handler.RangeScan(q.table.handle, plan.meta.EntryPage,
  50. kr.startKey, kr.endKey, kr.startInclusive, kr.endInclusive, reverse,
  51. func(row []byte) bool {
  52. if len(row) < 8 {
  53. return true
  54. }
  55. if seen != nil {
  56. pkID := binary.BigEndian.Uint64(row[:8])
  57. if _, dup := seen[pkID]; dup {
  58. return true
  59. }
  60. seen[pkID] = struct{}{}
  61. }
  62. snapshot := make([]byte, len(row))
  63. copy(snapshot, row)
  64. rows = append(rows, snapshot)
  65. return true
  66. }); err != nil {
  67. return err
  68. }
  69. }
  70. for _, row := range rows {
  71. rowData := row[8:]
  72. if plan.remainingCond != nil {
  73. if !plan.remainingCond.Match(rowData, q.table) {
  74. continue
  75. }
  76. }
  77. if !callback(binary.BigEndian.Uint64(row[:8]), q.deserialize(rowData)) {
  78. return nil
  79. }
  80. }
  81. return nil
  82. }
  83. // 二级索引: RangeScan 的输出序即索引键序 (B+ 叶序 / Hash 内部已按键排序),
  84. // 免排序 ORDER BY 路径依赖它 —— 单区间时原样保留; 只有 IN 多区间才排序去重
  85. // (此时顺序本来就交给排序管线, 升序输入也是批量回表游标的要求)。
  86. var pkIDs []uint64
  87. for _, kr := range plan.ranges {
  88. if err := plan.handler.RangeScan(q.table.handle, plan.meta.EntryPage,
  89. kr.startKey, kr.endKey, kr.startInclusive, kr.endInclusive, reverse,
  90. func(row []byte) bool {
  91. if len(row) < 8 {
  92. return true
  93. }
  94. pkIDs = append(pkIDs, binary.BigEndian.Uint64(row[:8]))
  95. return true
  96. }); err != nil {
  97. return err
  98. }
  99. }
  100. if len(plan.ranges) > 1 && len(pkIDs) > 1 {
  101. slices.Sort(pkIDs)
  102. // 多区间才可能重复 (同 pkID 命中多个重叠区间); 单区间去重徒增一次扫描
  103. w := 1
  104. for i := 1; i < len(pkIDs); i++ {
  105. if pkIDs[i] != pkIDs[w-1] {
  106. pkIDs[w] = pkIDs[i]
  107. w++
  108. }
  109. }
  110. pkIDs = pkIDs[:w]
  111. }
  112. q.table.indexMeta.BatchResolveByPK(pkIDs, budget, func(pkID uint64, storeRow []byte) bool {
  113. rowData := storeRow[8:]
  114. if plan.remainingCond != nil {
  115. if !plan.remainingCond.Match(rowData, q.table) {
  116. return true
  117. }
  118. }
  119. return callback(pkID, q.deserialize(rowData))
  120. })
  121. return nil
  122. }
  123. // fullTableScan 全表扫描 (流式回调模式)。与 fullTableScanRaw 同构,
  124. // 回调前多一步 deserialize 整行。
  125. func (q *Query) fullTableScan(callback func(pkID uint64, row []types.FieldValue) bool) error {
  126. return q.fullTableScanRaw(func(pkID uint64, rowData []byte) bool {
  127. return callback(pkID, q.deserialize(rowData))
  128. })
  129. }
  130. // ======================== 裸字节扫描 (聚合专用) ========================
  131. // scanRaw 裸字节版流式扫描: 回调拿 (pkID, 原始行字节), 全程不 deserializeRow。
  132. //
  133. // 背景: Count/Sum/Max/Min 每行只需要"过条件 + 最多一列", 而 scan 管线对每行都
  134. // 分配 []FieldValue 并解码全部活跃列 —— 100 万行的 SUM 里这部分占了绝对大头
  135. // (实测带条件全表扫描每行 ~0.66µs, 整行物化路径 ~1.4µs/行)。
  136. //
  137. // 约束: 只服务内部只读聚合。有 ORDER BY 时排序要比较解码后的列值, 这条路径
  138. // 无能为力 —— 调用方必须先退回整行路径。OFFSET/LIMIT 语义与 scanWithPKID
  139. // 的无排序分支逐行一致。
  140. func (q *Query) scanRaw(callback func(pkID uint64, rowData []byte) bool) error {
  141. plan := q.buildScanPlan()
  142. seen := 0
  143. return q.executeScanRaw(func(pkID uint64, rowData []byte) bool {
  144. if seen < q.offset {
  145. seen++
  146. return true // 跳过 OFFSET
  147. }
  148. seen++
  149. if !callback(pkID, rowData) {
  150. return false // 用户要求停止
  151. }
  152. if q.limit > 0 && seen-q.offset >= q.limit {
  153. return false // 已满足 LIMIT, 提前终止扫描
  154. }
  155. return true
  156. }, plan)
  157. }
  158. func (q *Query) executeScanRaw(callback func(pkID uint64, rowData []byte) bool, plan *scanPlan) error {
  159. if plan != nil {
  160. return q.indexScanRaw(plan, callback)
  161. }
  162. return q.fullTableScanRaw(callback)
  163. }
  164. // indexScanRaw 与 indexScan 走同一执行计划, 但回调拿裸行字节, 且遍历中同步回表。
  165. //
  166. // 为什么可以在 RangeScan 回调里直接回表 (indexScan 却必须"先物化、后回表"):
  167. // 物化两阶段是为"用户回调会在遍历中 Update/Delete 同一棵索引"准备的 —— 叶分裂/
  168. // 合并会让裸游标读到被改写的页。本路径的消费者全是只读聚合, 遍历中不存在任何
  169. // 写操作, 游标必然有效; 于是物化 (含 rowStore 的逐行快照拷贝) 一并省掉。
  170. func (q *Query) indexScanRaw(plan *scanPlan, callback func(pkID uint64, rowData []byte) bool) error {
  171. scanRange := func(kr keyRange) error {
  172. if plan.rowStore {
  173. // 承载行数据的索引: 条目即 storeRow = [pkID(8B) + rowData]
  174. return plan.handler.RangeScan(q.table.handle, plan.meta.EntryPage,
  175. kr.startKey, kr.endKey, kr.startInclusive, kr.endInclusive, false,
  176. func(row []byte) bool {
  177. if len(row) < 8 {
  178. return true
  179. }
  180. rowData := row[8:]
  181. if plan.remainingCond != nil && !plan.remainingCond.Match(rowData, q.table) {
  182. return true
  183. }
  184. return callback(binary.BigEndian.Uint64(row[:8]), rowData)
  185. })
  186. }
  187. // 二级索引: 条目 = pkID(8B BE), 遍历中同步回表 (只读, 不触碰被遍历的索引结构)
  188. return plan.handler.RangeScan(q.table.handle, plan.meta.EntryPage,
  189. kr.startKey, kr.endKey, kr.startInclusive, kr.endInclusive, false,
  190. func(row []byte) bool {
  191. if len(row) < 8 {
  192. return true
  193. }
  194. storeRow, found := q.table.indexMeta.ResolveByPK(binary.BigEndian.Uint64(row[:8]))
  195. if !found {
  196. return true // 行已被删除, 跳过
  197. }
  198. rowData := storeRow[8:]
  199. if plan.remainingCond != nil && !plan.remainingCond.Match(rowData, q.table) {
  200. return true
  201. }
  202. return callback(binary.BigEndian.Uint64(row[:8]), rowData)
  203. })
  204. }
  205. for _, kr := range plan.ranges {
  206. if err := scanRange(kr); err != nil {
  207. return err
  208. }
  209. }
  210. return nil
  211. }
  212. // fullTableScanRaw 与 fullTableScan 同构, 回调拿裸行字节。
  213. // ReadDataPageRows 返回的行本就是拷贝快照, rowData 跨回调安全。
  214. func (q *Query) fullTableScanRaw(callback func(pkID uint64, rowData []byte) bool) error {
  215. // 找承载行数据的索引 (实现 RowStoreHandler 者, 本层不认识索引类型)
  216. primMeta, primHandler, ok := q.table.indexMeta.rowStoreMeta()
  217. if !ok || primMeta == nil || primHandler == nil {
  218. return nil
  219. }
  220. dpIDs := primHandler.GetDataPageIDs(q.table.handle, primMeta.EntryPage)
  221. for _, dpID := range dpIDs {
  222. for _, storeRow := range primHandler.ReadDataPageRows(q.table.handle, dpID) {
  223. if len(storeRow) < 8 {
  224. continue
  225. }
  226. rowData := storeRow[8:]
  227. if q.whereCond != nil {
  228. if !q.whereCond.Match(rowData, q.table) {
  229. continue
  230. }
  231. }
  232. if !callback(binary.BigEndian.Uint64(storeRow[:8]), rowData) {
  233. return nil // 回调要求提前终止
  234. }
  235. }
  236. }
  237. return nil
  238. }
  239. // topKScan ORDER BY + LIMIT 的流式执行: 扫描全程只保留 limit+offset 行的小顶堆,
  240. // 内存 O(k)、免全量排序 (旧实现先物化全部结果再整体排序, 百万行翻页也要全排)。
  241. // 稳定性: 相等键按输入序号决胜, 输出与 slices.SortStableFunc 后切片逐字节一致。
  242. func (q *Query) topKScan(callback func(pkID uint64, row []types.FieldValue) bool,
  243. plan *scanPlan, specs []orderKeySpec) error {
  244. cap := q.limit + q.offset
  245. if cap < q.limit {
  246. cap = q.limit // 防 offset 溢出 (理论值, int 实际不会)
  247. }
  248. h := &topkHeap{specs: specs}
  249. idx := 0
  250. err := q.executeScan(func(pkID uint64, row []types.FieldValue) bool {
  251. keys := make([]interface{}, len(specs))
  252. for j, s := range specs {
  253. keys[j], _ = lookupFieldValue(row, []byte(s.field))
  254. }
  255. heap.Push(h, topkItem{pkID: pkID, row: row, keys: keys, idx: idx})
  256. idx++
  257. if h.Len() > cap {
  258. heap.Pop(h)
  259. }
  260. return true
  261. }, plan, 0)
  262. if err != nil {
  263. return err
  264. }
  265. // 全部弹出: 大顶堆弹出的是"输出序最后"的元素 → items 是输出序的逆序。
  266. items := make([]topkItem, 0, h.Len())
  267. for h.Len() > 0 {
  268. items = append(items, heap.Pop(h).(topkItem))
  269. }
  270. if q.offset > 0 {
  271. if q.offset >= len(items) {
  272. return nil
  273. }
  274. items = items[:len(items)-q.offset] // 弹出序的尾部 = 输出序的头部 → 丢弃
  275. }
  276. if q.limit > 0 && len(items) > q.limit {
  277. items = items[len(items)-q.limit:] // 保留弹出序尾部 = 输出序的前 limit 名
  278. }
  279. for i := len(items) - 1; i >= 0; i-- {
  280. if !callback(items[i].pkID, items[i].row) {
  281. return nil
  282. }
  283. }
  284. return nil
  285. }