query_plan.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. package core
  2. import (
  3. "bytes"
  4. "fmt"
  5. "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  8. )
  9. // ======================== 索引键编码 ========================
  10. // encodeIndexKey 将条件值编码为索引键字节。
  11. //
  12. // 编码与行内存储【同源】(都走 types.EncodeValue): 索引的 B+ 树直接按字节序比较键,
  13. // 因此"索引键字节 == 行内字节"是范围扫描正确的前提。这里不再按类型手写分支 ——
  14. // 以前每加一个类型都要在这里补一份, 漏补的后果是索引选出来的行与过滤结果不一致。
  15. //
  16. // param 是列级类型参数: decimal 是小数位数, 字符串家族是行内宽度码。少了它就没法把
  17. // 条件值编成与行内完全一致的字节 —— decimal 会因 scale 不同而长度/系数不一致,
  18. // 字符串则会按错误的宽度截断或越界, 两种都让"索引选出 5 行、过滤后剩 0 行"。
  19. //
  20. // 键长必须等于 FieldSize(ft, param): B+ 树按【定长拼键】比较, 键长与行内宽度不一致
  21. // 就等于拿两种编码去比大小。
  22. func encodeIndexKey(value interface{}, ft types.FieldType, param byte) []byte {
  23. if types.IsDynamic(ft) {
  24. // 动态列不能作为索引分量 (CreateIndex 已拒绝), 这里仅作兜底
  25. if b, ok := value.([]byte); ok {
  26. return b
  27. }
  28. return nil
  29. }
  30. size := types.FieldSize(ft, param)
  31. if size == 0 {
  32. // 未注册类型 / 非法列参数: 交给下面 EncodeValue 报出具体原因 (buf 长度为 0)
  33. panic(fmt.Errorf("%w: 类型 %s 的宽度不可计算 (列级参数 %d)",
  34. types.ErrValueType, types.TypeNameOf(ft), param))
  35. }
  36. buf := make([]byte, size)
  37. if err := types.EncodeValue(ft, param, value, buf); err != nil {
  38. // 沿用既有约定: panic 由 encodeIndexKeySafe* 转成 error。
  39. panic(err)
  40. }
  41. return buf
  42. }
  43. // encodeIndexKeySafe 在 encodeIndexKey 外面包一层 panic 恢复。
  44. // 值类型与列不符时 encodeIndexKey 会 panic, 而公开 API 传进来的值不可信,
  45. // 必须转成 error, 不能把用户输入直接变成崩溃。
  46. func encodeIndexKeySafe(value interface{}, ft types.FieldType) (enc []byte, err error) {
  47. return encodeIndexKeySafeWithParam(value, ft, 0)
  48. }
  49. // encodeIndexKeySafeWithParam 同 encodeIndexKeySafe, 但带上列级类型参数。
  50. // 查询侧拿得到字段元信息时必须走这个版本 (decimal 需要 param 才能编出正确的字节)。
  51. func encodeIndexKeySafeWithParam(value interface{}, ft types.FieldType, param byte) (enc []byte, err error) {
  52. defer func() {
  53. if r := recover(); r != nil {
  54. enc, err = nil, fmt.Errorf("值 %v 与存储类型 %s 不匹配: %v", value, types.TypeNameOf(ft), r)
  55. }
  56. }()
  57. return encodeIndexKey(value, ft, param), nil
  58. }
  59. // ======================== 索引匹配 ========================
  60. // fieldNameFromMeta 获取索引第一个字段的名称
  61. func (q *Query) fieldNameFromMeta(meta *IndexMeta) string {
  62. fieldIdx := int(meta.FieldIDs[0])
  63. if fieldIdx < len(q.table.Field) {
  64. return string(q.table.Field[fieldIdx].FieldName)
  65. }
  66. return ""
  67. }
  68. // ======================== 复合索引键规划 ========================
  69. //
  70. // 索引键 = 各分量字段定长拼接 (编码与 extractIndexKey 完全一致), 因此:
  71. // - 已知前 k 个分量等值 → 键上一段连续区间;
  72. // - 第 k+1 个分量上的范围条件 → 再收窄这段区间;
  73. // - 更靠后的分量索引帮不上忙, 原样留作行级后置过滤。
  74. // 下面的代码把 WHERE 按这个模型折叠成 (startKey, endKey, 残余条件)。
  75. //
  76. // 单列索引是 n == 1 的特例, 行为与旧实现一致。
  77. // indexKeyLayout 索引键各分量的偏移与长度 (定长拼接)
  78. type indexKeyLayout struct {
  79. offsets []int
  80. sizes []int
  81. total int
  82. }
  83. // cmpKind 叶子条件能为键分量提供的边界方向。
  84. // 取值与 CmpOp 一一对应 (cmpEq == cmpKind(OpEq), ...), classifyLeaf 因此可以
  85. // 直接 cmpKind(t.Op) 转换, 不必再写映射表。
  86. type cmpKind int
  87. const (
  88. cmpEq = cmpKind(OpEq)
  89. cmpGe = cmpKind(OpGe)
  90. cmpGt = cmpKind(OpGt)
  91. cmpLe = cmpKind(OpLe)
  92. cmpLt = cmpKind(OpLt)
  93. )
  94. // keyConstraint 某个键分量上的合并边界 (多个 AND 子条件取交集)
  95. type keyConstraint struct {
  96. lo, hi []byte // 编码后的分量值; nil 表示该侧无约束
  97. loInc, hiInc bool
  98. count int // 落在该分量上的条件个数, 0 = 无约束
  99. }
  100. // add 把一个边界并入本分量; 返回 false 表示条件自相矛盾。
  101. func (kc *keyConstraint) add(kind cmpKind, enc []byte) bool {
  102. switch kind {
  103. case cmpEq:
  104. if !kc.addLower(enc, true) || !kc.addUpper(enc, true) {
  105. return false
  106. }
  107. case cmpGe:
  108. if !kc.addLower(enc, true) {
  109. return false
  110. }
  111. case cmpGt:
  112. if !kc.addLower(enc, false) {
  113. return false
  114. }
  115. case cmpLe:
  116. if !kc.addUpper(enc, true) {
  117. return false
  118. }
  119. case cmpLt:
  120. if !kc.addUpper(enc, false) {
  121. return false
  122. }
  123. }
  124. kc.count++
  125. return true
  126. }
  127. func (kc *keyConstraint) addLower(enc []byte, inclusive bool) bool {
  128. if kc.lo == nil {
  129. kc.lo, kc.loInc = enc, inclusive
  130. return true
  131. }
  132. switch c := bytes.Compare(enc, kc.lo); {
  133. case c > 0: // 取更高的下界
  134. kc.lo, kc.loInc = enc, inclusive
  135. case c == 0: // 同值: 两端都闭才算"含"
  136. kc.loInc = kc.loInc && inclusive
  137. }
  138. return kc.notContradictory()
  139. }
  140. func (kc *keyConstraint) addUpper(enc []byte, inclusive bool) bool {
  141. if kc.hi == nil {
  142. kc.hi, kc.hiInc = enc, inclusive
  143. return true
  144. }
  145. switch c := bytes.Compare(enc, kc.hi); {
  146. case c < 0: // 取更低的上界
  147. kc.hi, kc.hiInc = enc, inclusive
  148. case c == 0:
  149. kc.hiInc = kc.hiInc && inclusive
  150. }
  151. return kc.notContradictory()
  152. }
  153. // notContradictory 判断 [lo, hi] 是否还可能有值。
  154. //
  155. // 注意: 这里按编码字节比较, 而定长整数用的是补码 —— 负数的字节序与数值序相反。
  156. // 所以它只在"字节序下已确定矛盾"时才返回 false, 此时真值也必然为空, 反向不成立。
  157. // 调用方因此只把它当作"放弃使用索引"的依据, 绝不据此断言区间非空。
  158. func (kc *keyConstraint) notContradictory() bool {
  159. if kc.lo == nil || kc.hi == nil {
  160. return true
  161. }
  162. switch c := bytes.Compare(kc.lo, kc.hi); {
  163. case c > 0:
  164. return false
  165. case c == 0:
  166. return kc.loInc && kc.hiInc
  167. }
  168. return true
  169. }
  170. // isEquality 该分量已被钉死为单值 (lo == hi 且两端都闭)
  171. func (kc *keyConstraint) isEquality() bool {
  172. return kc.count > 0 && kc.lo != nil && kc.hi != nil &&
  173. kc.loInc && kc.hiInc && bytes.Equal(kc.lo, kc.hi)
  174. }
  175. // buildKeyLayout 依据索引元信息与表字段类型算出键布局。
  176. // 返回 false 表示该索引当前不可用于查询:
  177. // - 分量列不存在或已被 Drop (新行提不出该分量, 索引里根本没有这批键);
  178. // - 分量是动态列 (行内只存 6B 堆引用, 值 != 引用, 无法按键定位)。
  179. func (q *Query) buildKeyLayout(meta *IndexMeta) (*indexKeyLayout, bool) {
  180. n := int(meta.FieldCnt)
  181. if n <= 0 || n > len(meta.FieldIDs) || n > len(q.table.Field) {
  182. return nil, false
  183. }
  184. lay := &indexKeyLayout{offsets: make([]int, n), sizes: make([]int, n)}
  185. off := 0
  186. for k := 0; k < n; k++ {
  187. fi := int(meta.FieldIDs[k])
  188. if fi >= len(q.table.Field) {
  189. return nil, false
  190. }
  191. if q.table.Field[fi].IsDropped() {
  192. return nil, false
  193. }
  194. ft := q.table.Field[fi].FieldType
  195. if types.IsDynamic(ft) {
  196. return nil, false
  197. }
  198. // 键分量宽度必须与行内宽度同源 (含字符串列的宽度码), 否则索引键的偏移切不出该列
  199. size := int(q.table.Field[fi].Width())
  200. if size <= 0 {
  201. return nil, false
  202. }
  203. lay.offsets[k] = off
  204. lay.sizes[k] = size
  205. off += size
  206. }
  207. lay.total = off
  208. return lay, true
  209. }
  210. // locateComponent 返回字段 master 下标在索引键中的分量位置; -1 表示不属于该索引
  211. func (q *Query) locateComponent(fieldName string, meta *IndexMeta) int {
  212. fi := q.table.findFieldIndex(fieldName)
  213. if fi < 0 {
  214. return -1
  215. }
  216. n := int(meta.FieldCnt)
  217. if n > len(meta.FieldIDs) {
  218. n = len(meta.FieldIDs)
  219. }
  220. for k := 0; k < n; k++ {
  221. if int(meta.FieldIDs[k]) == fi {
  222. return k
  223. }
  224. }
  225. return -1
  226. }
  227. // classifyLeaf 判断一个叶子条件能否成为某个键分量的边界。
  228. // comp < 0 表示与该索引无关 (字段不在索引里, 或该条件类型给不出可比边界)。
  229. func (q *Query) classifyLeaf(c Condition, meta *IndexMeta) (comp int, value interface{}, kind cmpKind) {
  230. switch t := c.(type) {
  231. case CmpCondition:
  232. // cmpKind 与 CmpOp 取值一一对应 (见 cmpKind 常量块), 直接转换。
  233. return q.locateComponent(t.Field, meta), t.Value, cmpKind(t.Op)
  234. default:
  235. return -1, nil, cmpEq
  236. }
  237. }
  238. // flattenAnd 把 AND 树展平成叶子条件。
  239. // OR / NOT / LIKE / IN 不是合取项, 整体当叶子返回 —— 它们给不出索引边界,
  240. // 但必须原样保留下来做行级过滤。
  241. func flattenAnd(cond Condition, out *[]Condition) {
  242. if and, ok := cond.(AndCondition); ok {
  243. for _, sub := range and.Conditions {
  244. flattenAnd(sub, out)
  245. }
  246. return
  247. }
  248. *out = append(*out, cond)
  249. }
  250. // incBE 把 buf 当作大端无符号整数加一 (原地)。返回 false 表示溢出 ——
  251. // 此时 buf 之后不再存在任何更大的键。
  252. func incBE(buf []byte) bool {
  253. for i := len(buf) - 1; i >= 0; i-- {
  254. buf[i]++
  255. if buf[i] != 0 {
  256. return true
  257. }
  258. }
  259. return false
  260. }
  261. // fillFF 把 buf[from:] 全置 0xFF (该分量及其后分量的最大值)。
  262. // from 可以是 0 —— 首分量就是范围分量时前缀为空, 此时整个键都无上界。
  263. func fillFF(buf []byte, from int) {
  264. for i := from; i < len(buf); i++ {
  265. buf[i] = 0xFF
  266. }
  267. }
  268. // buildScanPlan 分析 WHERE 条件, 选择最优索引
  269. func (q *Query) buildScanPlan() *scanPlan {
  270. if q.whereCond == nil {
  271. return nil
  272. }
  273. if q.table.indexMeta == nil {
  274. return nil
  275. }
  276. // 先展平成合取项: 索引区间是按"分量"折叠出来的, 只有 AND 语义才可加。
  277. var leaves []Condition
  278. flattenAnd(q.whereCond, &leaves)
  279. var bestPlan *scanPlan
  280. for i := range q.table.indexMeta.metas {
  281. m := &q.table.indexMeta.metas[i]
  282. if m.Flag&0x80 != 0 {
  283. continue
  284. }
  285. // 主键索引直接承载全部行, 但它的 B+ 树叶键是 pkID, 而不是"建在哪一列上"。
  286. // planIndex 会用 meta.FieldIDs 编码出的【字段值】当扫描区间 (见 buildKeyLayout),
  287. // 拿它去比对 pkID 叶键, 语义对不上 —— 只有当该列的值恒等于 pkID 时才偶然正确,
  288. // 否则会静默地只命中/漏掉若干行 (例如 Delete/Update 的条件扫描)。
  289. // 因此主键索引不参与列条件规划, 这类查询退回全表扫描 + 行级过滤 (fullTableScan
  290. // 会施加 whereCond), 结果正确; 按 pkID 的点查/回表仍走 SearchByIndex/ResolveByPK,
  291. // 不经过这里, 不受影响。
  292. if m.IndexType == page.IndexTypePrimary {
  293. continue
  294. }
  295. handler, ok := index.Compose.GetHandler(m.IndexType)
  296. if !ok {
  297. continue
  298. }
  299. plan := q.planIndex(m, handler, leaves)
  300. if plan == nil {
  301. continue // 此索引不匹配
  302. }
  303. // 该索引是否承载行数据: 实现 RowStoreHandler 即承载, 本层不认识索引类型
  304. _, plan.rowStore = handler.(index.RowStoreHandler)
  305. if plan.rowStore {
  306. return plan // 行存储索引最优 (免回表), 不再找
  307. }
  308. if bestPlan == nil {
  309. bestPlan = plan
  310. }
  311. }
  312. return bestPlan
  313. }
  314. // planIndex 为一个索引推导扫描区间; 该索引用不上时返回 nil。
  315. func (q *Query) planIndex(m *IndexMeta, handler index.IndexHandler, leaves []Condition) *scanPlan {
  316. lay, ok := q.buildKeyLayout(m)
  317. if !ok {
  318. return nil
  319. }
  320. n := len(lay.sizes)
  321. cons := make([]keyConstraint, n) // 各分量合并后的边界
  322. compCond := make([][]Condition, n) // 各分量上的原始条件 (用于决定哪些能丢)
  323. var rest []Condition // 与索引无关、必须保留的条件
  324. for _, c := range leaves {
  325. k, value, kind := q.classifyLeaf(c, m)
  326. if k < 0 {
  327. rest = append(rest, c)
  328. continue
  329. }
  330. ft := q.table.Field[m.FieldIDs[k]].FieldType
  331. // 用 Safe 版本: 条件的值类型与列存储类型不符时 encodeIndexKey 会 panic,
  332. // 规划阶段不该把用户输入直接变成崩溃 —— 退回行级过滤由 Match 去处理。
  333. // 带上列级 param (decimal 的 scale / 字符串的宽度码): 索引键必须与行内字节完全一致。
  334. enc, err := encodeIndexKeySafeWithParam(value, ft, q.table.fieldParam(int(m.FieldIDs[k])))
  335. if err != nil || len(enc) != lay.sizes[k] {
  336. // 编码失败, 或长度与分量长度不符 (拼进定长键会错位) → 交给行级过滤
  337. rest = append(rest, c)
  338. continue
  339. }
  340. if !cons[k].add(kind, enc) {
  341. // 该分量上的条件互相矛盾 → 结果必为空。
  342. // 这里保守地"放弃索引"而不是返回空区间: 定长补码编码下负数字节序与
  343. // 数值序相反, 用字节比较断定"空"对负数不成立, 退回全表扫描更稳。
  344. return nil
  345. }
  346. compCond[k] = append(compCond[k], c)
  347. }
  348. // 从左往右消费"等值分量"; 之后最多再消费一个范围分量, 更后面的分量帮不上忙。
  349. eqLen := 0
  350. for eqLen < n && cons[eqLen].isEquality() {
  351. eqLen++
  352. }
  353. rangeIdx := -1
  354. if eqLen < n && cons[eqLen].count > 0 {
  355. rangeIdx = eqLen
  356. }
  357. // IN 展开: 只看"等值前缀的下一个分量", 且其上必须还没有任何边界条件
  358. // (有范围/等值条件时让位给常规区间规划, IN 由行级过滤兜底)。
  359. // 每个 IN 值变成该分量取值的精确区间 [前缀||v||00.., 前缀||v||FF..]。
  360. // 多个 IN 只消费第一个适用的, 其余留在行级过滤 (正确性不受影响)。
  361. // 注意本段在 cons[0].count 判定【之前】: 只有 IN 条件时 cons 全空,
  362. // 但它照样能给出一组精确区间。
  363. for _, c := range leaves {
  364. in, ok := c.(InCondition)
  365. if !ok {
  366. continue
  367. }
  368. if eqLen >= n || cons[eqLen].count != 0 {
  369. continue
  370. }
  371. if q.locateComponent(in.Field, m) != eqLen {
  372. continue
  373. }
  374. if plan := q.planInRanges(m, handler, lay, cons, eqLen, rest, compCond, in); plan != nil {
  375. return plan
  376. }
  377. // 返回了 nil = 有值编不出键: 本索引放弃 IN, 让行级过滤兜住正确性,
  378. // 继续走下面的常规区间规划 (等值前缀等条件仍可用)。
  379. break
  380. }
  381. if cons[0].count == 0 {
  382. return nil // 首分量没有边界 → 索引给不出连续区间
  383. }
  384. startKey := make([]byte, lay.total)
  385. endKey := make([]byte, lay.total)
  386. prefix := 0
  387. for j := 0; j < eqLen; j++ {
  388. copy(startKey[prefix:], cons[j].lo)
  389. copy(endKey[prefix:], cons[j].lo)
  390. prefix += len(cons[j].lo)
  391. }
  392. plan := &scanPlan{
  393. meta: m, handler: handler,
  394. ranges: []keyRange{{startKey: startKey, endKey: endKey,
  395. startInclusive: true, endInclusive: true}},
  396. remainingCond: q.remainCond(rest, compCond, eqLen, rangeIdx),
  397. }
  398. // 全部等值 → 精确键 (此时 prefix == total, fillFF 无事可做);
  399. // 只消费了等值前缀 → 该前缀下的全部键: [前缀||00.., 前缀||FF..]。
  400. if eqLen == n || rangeIdx < 0 {
  401. fillFF(endKey, prefix)
  402. return plan
  403. }
  404. rc := &cons[rangeIdx]
  405. size := lay.sizes[rangeIdx]
  406. // 下界: [前缀 || 分量值 || 00..]
  407. if rc.lo != nil {
  408. copy(startKey[prefix:], rc.lo)
  409. if !rc.loInc {
  410. // 严格下界: 要跳过分量 == lo 的全部键, 即把 (前缀||lo) 整体加一
  411. if !incBE(startKey[:prefix+size]) {
  412. return nil // 已经是最大键, 该条件不可能有匹配行
  413. }
  414. }
  415. }
  416. // 上界: [前缀 || 分量值 || FF..]
  417. kr := &plan.ranges[0]
  418. if rc.hi == nil {
  419. fillFF(endKey, prefix)
  420. } else if rc.hiInc {
  421. copy(endKey[prefix:], rc.hi)
  422. fillFF(endKey, prefix+size)
  423. } else {
  424. // 严格上界: 分量 == hi 的键全部排除
  425. copy(endKey[prefix:], rc.hi)
  426. kr.endInclusive = false
  427. }
  428. return plan
  429. }
  430. // planInRanges 把 IN 条件展开成多区间扫描计划。
  431. //
  432. // 返回 nil 表示 IN 无法索引化 (有值编不出键) —— 调用方退回常规规划/全表扫描,
  433. // IN 本身仍在 whereCond 里, 行级过滤保证正确性。
  434. // IN 空集合 → 返回 ranges 为空的计划: 任何区间都不扫, 结果必空 (IN 恒假),
  435. // 与 InCondition.Match 的空集合语义一致。
  436. func (q *Query) planInRanges(m *IndexMeta, handler index.IndexHandler, lay *indexKeyLayout,
  437. cons []keyConstraint, eqLen int, rest []Condition, compCond [][]Condition, in InCondition) *scanPlan {
  438. k := eqLen
  439. size := lay.sizes[k]
  440. fi := int(m.FieldIDs[k])
  441. ft := q.table.Field[fi].FieldType
  442. param := q.table.fieldParam(fi)
  443. // 等值前缀字节 (与常规规划同一编码: cons[j].lo, isEquality 保证两端闭且相等)
  444. prefix := make([]byte, lay.offsets[k])
  445. for j := 0; j < eqLen; j++ {
  446. copy(prefix[lay.offsets[j]:], cons[j].lo)
  447. }
  448. if len(in.Values) == 0 {
  449. // 恒空: 不扫任何区间。remainingCond 保留全部条件 (无行可过滤, 仅为语义完整)。
  450. return &scanPlan{meta: m, handler: handler,
  451. ranges: nil,
  452. remainingCond: q.remainCond(rest, compCond, 0, -1)}
  453. }
  454. ranges := make([]keyRange, 0, len(in.Values))
  455. seen := make(map[string]struct{}, len(in.Values)) // 编码后同键的值只留一个区间
  456. for _, v := range in.Values {
  457. enc, err := encodeIndexKeySafeWithParam(v, ft, param)
  458. if err != nil || len(enc) != size {
  459. return nil // 任一值编不出正确宽度的键 → 整个 IN 退回行级过滤
  460. }
  461. // 该分量取值 v 的全部键: [前缀||v||00.., 前缀||v||FF..] (单列索引即 [v, v])
  462. start := make([]byte, lay.total)
  463. end := make([]byte, lay.total)
  464. copy(start, prefix)
  465. copy(end, prefix)
  466. copy(start[lay.offsets[k]:], enc)
  467. copy(end[lay.offsets[k]:], enc)
  468. fillFF(end, lay.offsets[k]+size)
  469. // 重复值/编码碰撞值去重: 不去重的话, raw 聚合路径 (Count/Sum 的
  470. // scanRaw) 会对同一行重复计数 —— 行交付路径有去重, 聚合没有。
  471. if _, dup := seen[string(start)]; dup {
  472. continue
  473. }
  474. seen[string(start)] = struct{}{}
  475. ranges = append(ranges, keyRange{startKey: start, endKey: end,
  476. startInclusive: true, endInclusive: true})
  477. }
  478. return &scanPlan{meta: m, handler: handler, ranges: ranges,
  479. remainingCond: q.remainCond(rest, compCond, eqLen, -1)}
  480. }
  481. // remainCond 汇总索引区间没能表达、仍需逐行过滤的条件。
  482. // 已被区间精确覆盖的分量 (等值前缀 + 那个范围分量) 上的条件可以安全丢掉。
  483. func (q *Query) remainCond(rest []Condition, compCond [][]Condition, eqLen, rangeIdx int) Condition {
  484. remaining := rest
  485. for j := range compCond {
  486. if j < eqLen || j == rangeIdx {
  487. continue
  488. }
  489. remaining = append(remaining, compCond[j]...)
  490. }
  491. switch len(remaining) {
  492. case 0:
  493. return nil
  494. case 1:
  495. return remaining[0]
  496. default:
  497. return And(remaining...)
  498. }
  499. }