|
|
@@ -6,6 +6,7 @@ import (
|
|
|
"TsDB2/DB/core/types"
|
|
|
"bytes"
|
|
|
"encoding/binary"
|
|
|
+ "fmt"
|
|
|
"sort"
|
|
|
)
|
|
|
|
|
|
@@ -82,6 +83,18 @@ func encodeIndexKey(value interface{}, ft types.FieldType) []byte {
|
|
|
return buf
|
|
|
}
|
|
|
|
|
|
+// encodeIndexKeySafe 在 encodeIndexKey 外面包一层 panic 恢复。
|
|
|
+// encodeIndexKey 直接对值做类型断言 (value.(uint64) 等), 类型不符会 panic;
|
|
|
+// 内部调用点的值来自条件对象 (受控), 而公开 API 传进来的值不可信, 必须转成 error。
|
|
|
+func encodeIndexKeySafe(value interface{}, ft types.FieldType) (enc []byte, err error) {
|
|
|
+ defer func() {
|
|
|
+ if r := recover(); r != nil {
|
|
|
+ enc, err = nil, fmt.Errorf("值 %v 与存储类型 %d 不匹配: %v", value, ft, r)
|
|
|
+ }
|
|
|
+ }()
|
|
|
+ return encodeIndexKey(value, ft), nil
|
|
|
+}
|
|
|
+
|
|
|
// ======================== 索引匹配 ========================
|
|
|
|
|
|
// fieldNameFromMeta 获取索引第一个字段的名称
|
|
|
@@ -93,85 +106,224 @@ func (q *Query) fieldNameFromMeta(meta *IndexMeta) string {
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
-// fieldTypeFromMeta 获取索引第一个字段的类型
|
|
|
-func (q *Query) fieldTypeFromMeta(meta *IndexMeta) types.FieldType {
|
|
|
- fieldIdx := int(meta.FieldIDs[0])
|
|
|
- if fieldIdx < len(q.table.Field) {
|
|
|
- return q.table.Field[fieldIdx].FieldType
|
|
|
+// ======================== 复合索引键规划 ========================
|
|
|
+//
|
|
|
+// 索引键 = 各分量字段定长拼接 (编码与 extractIndexKey 完全一致), 因此:
|
|
|
+// - 已知前 k 个分量等值 → 键上一段连续区间;
|
|
|
+// - 第 k+1 个分量上的范围条件 → 再收窄这段区间;
|
|
|
+// - 更靠后的分量索引帮不上忙, 原样留作行级后置过滤。
|
|
|
+// 下面的代码把 WHERE 按这个模型折叠成 (startKey, endKey, 残余条件)。
|
|
|
+//
|
|
|
+// 单列索引是 n == 1 的特例, 行为与旧实现一致。
|
|
|
+
|
|
|
+// indexKeyLayout 索引键各分量的偏移与长度 (定长拼接)
|
|
|
+type indexKeyLayout struct {
|
|
|
+ offsets []int
|
|
|
+ sizes []int
|
|
|
+ total int
|
|
|
+}
|
|
|
+
|
|
|
+// cmpKind 叶子条件能为键分量提供的边界方向
|
|
|
+type cmpKind int
|
|
|
+
|
|
|
+const (
|
|
|
+ cmpEq cmpKind = iota
|
|
|
+ cmpGe
|
|
|
+ cmpGt
|
|
|
+ cmpLe
|
|
|
+ cmpLt
|
|
|
+)
|
|
|
+
|
|
|
+// keyConstraint 某个键分量上的合并边界 (多个 AND 子条件取交集)
|
|
|
+type keyConstraint struct {
|
|
|
+ lo, hi []byte // 编码后的分量值; nil 表示该侧无约束
|
|
|
+ loInc, hiInc bool
|
|
|
+ count int // 落在该分量上的条件个数, 0 = 无约束
|
|
|
+}
|
|
|
+
|
|
|
+// add 把一个边界并入本分量; 返回 false 表示条件自相矛盾。
|
|
|
+func (kc *keyConstraint) add(kind cmpKind, enc []byte) bool {
|
|
|
+ switch kind {
|
|
|
+ case cmpEq:
|
|
|
+ if !kc.addLower(enc, true) || !kc.addUpper(enc, true) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ case cmpGe:
|
|
|
+ if !kc.addLower(enc, true) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ case cmpGt:
|
|
|
+ if !kc.addLower(enc, false) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ case cmpLe:
|
|
|
+ if !kc.addUpper(enc, true) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ case cmpLt:
|
|
|
+ if !kc.addUpper(enc, false) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
}
|
|
|
- return 0
|
|
|
+ kc.count++
|
|
|
+ return true
|
|
|
}
|
|
|
|
|
|
-// condToRange 尝试从条件中提取扫描范围, 返回 (startKey, endKey, startInc, endInc, ok)
|
|
|
-func (q *Query) condToRange(cond Condition, fieldName string, ft types.FieldType) ([]byte, []byte, bool, bool, Condition) {
|
|
|
- switch c := cond.(type) {
|
|
|
- case EqCondition:
|
|
|
- if c.Field == fieldName {
|
|
|
- key := encodeIndexKey(c.Value, ft)
|
|
|
- return key, key, true, true, nil
|
|
|
+func (kc *keyConstraint) addLower(enc []byte, inclusive bool) bool {
|
|
|
+ if kc.lo == nil {
|
|
|
+ kc.lo, kc.loInc = enc, inclusive
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ switch c := bytes.Compare(enc, kc.lo); {
|
|
|
+ case c > 0: // 取更高的下界
|
|
|
+ kc.lo, kc.loInc = enc, inclusive
|
|
|
+ case c == 0: // 同值: 两端都闭才算"含"
|
|
|
+ kc.loInc = kc.loInc && inclusive
|
|
|
+ }
|
|
|
+ return kc.notContradictory()
|
|
|
+}
|
|
|
+
|
|
|
+func (kc *keyConstraint) addUpper(enc []byte, inclusive bool) bool {
|
|
|
+ if kc.hi == nil {
|
|
|
+ kc.hi, kc.hiInc = enc, inclusive
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ switch c := bytes.Compare(enc, kc.hi); {
|
|
|
+ case c < 0: // 取更低的上界
|
|
|
+ kc.hi, kc.hiInc = enc, inclusive
|
|
|
+ case c == 0:
|
|
|
+ kc.hiInc = kc.hiInc && inclusive
|
|
|
+ }
|
|
|
+ return kc.notContradictory()
|
|
|
+}
|
|
|
+
|
|
|
+// notContradictory 判断 [lo, hi] 是否还可能有值。
|
|
|
+//
|
|
|
+// 注意: 这里按编码字节比较, 而定长整数用的是补码 —— 负数的字节序与数值序相反。
|
|
|
+// 所以它只在"字节序下已确定矛盾"时才返回 false, 此时真值也必然为空, 反向不成立。
|
|
|
+// 调用方因此只把它当作"放弃使用索引"的依据, 绝不据此断言区间非空。
|
|
|
+func (kc *keyConstraint) notContradictory() bool {
|
|
|
+ if kc.lo == nil || kc.hi == nil {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ switch c := bytes.Compare(kc.lo, kc.hi); {
|
|
|
+ case c > 0:
|
|
|
+ return false
|
|
|
+ case c == 0:
|
|
|
+ return kc.loInc && kc.hiInc
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+// isEquality 该分量已被钉死为单值 (lo == hi 且两端都闭)
|
|
|
+func (kc *keyConstraint) isEquality() bool {
|
|
|
+ return kc.count > 0 && kc.lo != nil && kc.hi != nil &&
|
|
|
+ kc.loInc && kc.hiInc && bytes.Equal(kc.lo, kc.hi)
|
|
|
+}
|
|
|
+
|
|
|
+// buildKeyLayout 依据索引元信息与表字段类型算出键布局。
|
|
|
+// 返回 false 表示该索引当前不可用于查询:
|
|
|
+// - 分量列不存在或已被 Drop (新行提不出该分量, 索引里根本没有这批键);
|
|
|
+// - 分量是动态列 (行内只存 6B 堆引用, 值 != 引用, 无法按键定位)。
|
|
|
+func (q *Query) buildKeyLayout(meta *IndexMeta) (*indexKeyLayout, bool) {
|
|
|
+ n := int(meta.FieldCnt)
|
|
|
+ if n <= 0 || n > len(meta.FieldIDs) || n > len(q.table.Field) {
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ lay := &indexKeyLayout{offsets: make([]int, n), sizes: make([]int, n)}
|
|
|
+ off := 0
|
|
|
+ for k := 0; k < n; k++ {
|
|
|
+ fi := int(meta.FieldIDs[k])
|
|
|
+ if fi >= len(q.table.Field) {
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ ft := q.table.Field[fi].FieldType
|
|
|
+ if ft&0x80 != 0 {
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ if types.IsDynamic(ft) {
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ size := int(getFieldSize(ft))
|
|
|
+ if size <= 0 {
|
|
|
+ return nil, false
|
|
|
+ }
|
|
|
+ lay.offsets[k] = off
|
|
|
+ lay.sizes[k] = size
|
|
|
+ off += size
|
|
|
+ }
|
|
|
+ lay.total = off
|
|
|
+ return lay, true
|
|
|
+}
|
|
|
+
|
|
|
+// locateComponent 返回字段 master 下标在索引键中的分量位置; -1 表示不属于该索引
|
|
|
+func (q *Query) locateComponent(fieldName string, meta *IndexMeta) int {
|
|
|
+ fi := q.table.findFieldIndex(fieldName)
|
|
|
+ if fi < 0 {
|
|
|
+ return -1
|
|
|
+ }
|
|
|
+ n := int(meta.FieldCnt)
|
|
|
+ if n > len(meta.FieldIDs) {
|
|
|
+ n = len(meta.FieldIDs)
|
|
|
+ }
|
|
|
+ for k := 0; k < n; k++ {
|
|
|
+ if int(meta.FieldIDs[k]) == fi {
|
|
|
+ return k
|
|
|
}
|
|
|
+ }
|
|
|
+ return -1
|
|
|
+}
|
|
|
+
|
|
|
+// classifyLeaf 判断一个叶子条件能否成为某个键分量的边界。
|
|
|
+// comp < 0 表示与该索引无关 (字段不在索引里, 或该条件类型给不出可比边界)。
|
|
|
+func (q *Query) classifyLeaf(c Condition, meta *IndexMeta) (comp int, value interface{}, kind cmpKind) {
|
|
|
+ switch t := c.(type) {
|
|
|
+ case EqCondition:
|
|
|
+ return q.locateComponent(t.Field, meta), t.Value, cmpEq
|
|
|
case GeCondition:
|
|
|
- if c.Field == fieldName {
|
|
|
- return encodeIndexKey(c.Value, ft), nil, true, false, nil
|
|
|
- }
|
|
|
+ return q.locateComponent(t.Field, meta), t.Value, cmpGe
|
|
|
case GtCondition:
|
|
|
- if c.Field == fieldName {
|
|
|
- return encodeIndexKey(c.Value, ft), nil, false, false, nil
|
|
|
- }
|
|
|
+ return q.locateComponent(t.Field, meta), t.Value, cmpGt
|
|
|
case LeCondition:
|
|
|
- if c.Field == fieldName {
|
|
|
- return nil, encodeIndexKey(c.Value, ft), false, true, nil
|
|
|
- }
|
|
|
+ return q.locateComponent(t.Field, meta), t.Value, cmpLe
|
|
|
case LtCondition:
|
|
|
- if c.Field == fieldName {
|
|
|
- return nil, encodeIndexKey(c.Value, ft), false, false, nil
|
|
|
- }
|
|
|
- case AndCondition:
|
|
|
- // 合并所有子条件的范围
|
|
|
- var sk, ek []byte
|
|
|
- var si, ei bool = true, true // 默认全包含
|
|
|
- var hasStart, hasEnd bool
|
|
|
- var remaining []Condition
|
|
|
-
|
|
|
- for _, sub := range c.Conditions {
|
|
|
- s, e, sinc, einc, rem2 := q.condToRange(sub, fieldName, ft)
|
|
|
- if s != nil || e != nil {
|
|
|
- // 该子条件能为索引提供边界: 合并到已有范围
|
|
|
- if s != nil {
|
|
|
- if !hasStart || (sinc && bytes.Compare(s, sk) > 0) || (!sinc && bytes.Compare(s, sk) >= 0) {
|
|
|
- sk = s
|
|
|
- si = sinc
|
|
|
- hasStart = true
|
|
|
- }
|
|
|
- }
|
|
|
- if e != nil {
|
|
|
- if !hasEnd || (einc && bytes.Compare(e, ek) < 0) || (!einc && bytes.Compare(e, ek) <= 0) {
|
|
|
- ek = e
|
|
|
- ei = einc
|
|
|
- hasEnd = true
|
|
|
- }
|
|
|
- }
|
|
|
- // 未被完全吸收的残余条件仍需在扫描结果上后置过滤
|
|
|
- if rem2 != nil {
|
|
|
- remaining = append(remaining, rem2)
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 此子条件不能用索引: 整体留作后置过滤
|
|
|
- remaining = append(remaining, sub)
|
|
|
- }
|
|
|
+ return q.locateComponent(t.Field, meta), t.Value, cmpLt
|
|
|
+ default:
|
|
|
+ return -1, nil, cmpEq
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// flattenAnd 把 AND 树展平成叶子条件。
|
|
|
+// OR / NOT / LIKE / IN 不是合取项, 整体当叶子返回 —— 它们给不出索引边界,
|
|
|
+// 但必须原样保留下来做行级过滤。
|
|
|
+func flattenAnd(cond Condition, out *[]Condition) {
|
|
|
+ if and, ok := cond.(AndCondition); ok {
|
|
|
+ for _, sub := range and.Conditions {
|
|
|
+ flattenAnd(sub, out)
|
|
|
}
|
|
|
+ return
|
|
|
+ }
|
|
|
+ *out = append(*out, cond)
|
|
|
+}
|
|
|
|
|
|
- if hasStart || hasEnd {
|
|
|
- var rem Condition
|
|
|
- if len(remaining) == 1 {
|
|
|
- rem = remaining[0]
|
|
|
- } else if len(remaining) > 1 {
|
|
|
- rem = And(remaining...)
|
|
|
- }
|
|
|
- return sk, ek, si, ei, rem
|
|
|
+// incBE 把 buf 当作大端无符号整数加一 (原地)。返回 false 表示溢出 ——
|
|
|
+// 此时 buf 之后不再存在任何更大的键。
|
|
|
+func incBE(buf []byte) bool {
|
|
|
+ for i := len(buf) - 1; i >= 0; i-- {
|
|
|
+ buf[i]++
|
|
|
+ if buf[i] != 0 {
|
|
|
+ return true
|
|
|
}
|
|
|
}
|
|
|
- return nil, nil, false, false, cond
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+// fillFF 把 buf[from:] 全置 0xFF (该分量及其后分量的最大值)。
|
|
|
+// from 可以是 0 —— 首分量就是范围分量时前缀为空, 此时整个键都无上界。
|
|
|
+func fillFF(buf []byte, from int) {
|
|
|
+ for i := from; i < len(buf); i++ {
|
|
|
+ buf[i] = 0xFF
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// buildScanPlan 分析 WHERE 条件, 选择最优索引
|
|
|
@@ -183,8 +335,11 @@ func (q *Query) buildScanPlan() *scanPlan {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
- var bestPlan *scanPlan
|
|
|
+ // 先展平成合取项: 索引区间是按"分量"折叠出来的, 只有 AND 语义才可加。
|
|
|
+ var leaves []Condition
|
|
|
+ flattenAnd(q.whereCond, &leaves)
|
|
|
|
|
|
+ var bestPlan *scanPlan
|
|
|
for i := range q.table.indexMeta.metas {
|
|
|
m := &q.table.indexMeta.metas[i]
|
|
|
if m.Flag&0x80 != 0 {
|
|
|
@@ -194,34 +349,141 @@ func (q *Query) buildScanPlan() *scanPlan {
|
|
|
if !ok {
|
|
|
continue
|
|
|
}
|
|
|
+ plan := q.planIndex(m, handler, leaves)
|
|
|
+ if plan == nil {
|
|
|
+ continue // 此索引不匹配
|
|
|
+ }
|
|
|
// 该索引是否承载行数据: 由处理器自报, 本层不认识索引类型
|
|
|
- rowStore := handler.IsRowStore()
|
|
|
- fieldName := q.fieldNameFromMeta(m)
|
|
|
- ft := q.fieldTypeFromMeta(m)
|
|
|
- if fieldName == "" {
|
|
|
- continue
|
|
|
+ plan.rowStore = handler.IsRowStore()
|
|
|
+ if plan.rowStore {
|
|
|
+ return plan // 行存储索引最优 (免回表), 不再找
|
|
|
}
|
|
|
-
|
|
|
- sk, ek, si, ei, rem := q.condToRange(q.whereCond, fieldName, ft)
|
|
|
- if sk == nil && ek == nil {
|
|
|
- continue // 此索引不匹配
|
|
|
+ if bestPlan == nil {
|
|
|
+ bestPlan = plan
|
|
|
}
|
|
|
+ }
|
|
|
+ return bestPlan
|
|
|
+}
|
|
|
+
|
|
|
+// planIndex 为一个索引推导扫描区间; 该索引用不上时返回 nil。
|
|
|
+func (q *Query) planIndex(m *IndexMeta, handler index.IndexHandler, leaves []Condition) *scanPlan {
|
|
|
+ lay, ok := q.buildKeyLayout(m)
|
|
|
+ if !ok {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ n := len(lay.sizes)
|
|
|
|
|
|
- plan := &scanPlan{
|
|
|
- meta: m, handler: handler,
|
|
|
- startKey: sk, endKey: ek,
|
|
|
- startInclusive: si, endInclusive: ei,
|
|
|
- rowStore: rowStore, remainingCond: rem,
|
|
|
+ cons := make([]keyConstraint, n) // 各分量合并后的边界
|
|
|
+ compCond := make([][]Condition, n) // 各分量上的原始条件 (用于决定哪些能丢)
|
|
|
+ var rest []Condition // 与索引无关、必须保留的条件
|
|
|
+
|
|
|
+ for _, c := range leaves {
|
|
|
+ k, value, kind := q.classifyLeaf(c, m)
|
|
|
+ if k < 0 {
|
|
|
+ rest = append(rest, c)
|
|
|
+ continue
|
|
|
}
|
|
|
- // 优先选承载行数据的索引 (避免回查)
|
|
|
- if rowStore || bestPlan == nil {
|
|
|
- bestPlan = plan
|
|
|
+ ft := q.table.Field[m.FieldIDs[k]].FieldType
|
|
|
+ // 用 Safe 版本: 条件的值类型与列存储类型不符时 encodeIndexKey 会 panic,
|
|
|
+ // 规划阶段不该把用户输入直接变成崩溃 —— 退回行级过滤由 Match 去处理。
|
|
|
+ enc, err := encodeIndexKeySafe(value, ft)
|
|
|
+ if err != nil || len(enc) != lay.sizes[k] {
|
|
|
+ // 编码失败, 或长度与分量长度不符 (拼进定长键会错位) → 交给行级过滤
|
|
|
+ rest = append(rest, c)
|
|
|
+ continue
|
|
|
}
|
|
|
- if rowStore {
|
|
|
- break // 行存储索引最优, 不再找
|
|
|
+ if !cons[k].add(kind, enc) {
|
|
|
+ // 该分量上的条件互相矛盾 → 结果必为空。
|
|
|
+ // 这里保守地"放弃索引"而不是返回空区间: 定长补码编码下负数字节序与
|
|
|
+ // 数值序相反, 用字节比较断定"空"对负数不成立, 退回全表扫描更稳。
|
|
|
+ return nil
|
|
|
}
|
|
|
+ compCond[k] = append(compCond[k], c)
|
|
|
+ }
|
|
|
+
|
|
|
+ if cons[0].count == 0 {
|
|
|
+ return nil // 首分量没有边界 → 索引给不出连续区间
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从左往右消费"等值分量"; 之后最多再消费一个范围分量, 更后面的分量帮不上忙。
|
|
|
+ eqLen := 0
|
|
|
+ for eqLen < n && cons[eqLen].isEquality() {
|
|
|
+ eqLen++
|
|
|
+ }
|
|
|
+ rangeIdx := -1
|
|
|
+ if eqLen < n && cons[eqLen].count > 0 {
|
|
|
+ rangeIdx = eqLen
|
|
|
+ }
|
|
|
+
|
|
|
+ startKey := make([]byte, lay.total)
|
|
|
+ endKey := make([]byte, lay.total)
|
|
|
+ prefix := 0
|
|
|
+ for j := 0; j < eqLen; j++ {
|
|
|
+ copy(startKey[prefix:], cons[j].lo)
|
|
|
+ copy(endKey[prefix:], cons[j].lo)
|
|
|
+ prefix += len(cons[j].lo)
|
|
|
+ }
|
|
|
+
|
|
|
+ plan := &scanPlan{
|
|
|
+ meta: m, handler: handler,
|
|
|
+ startKey: startKey, endKey: endKey,
|
|
|
+ startInclusive: true, endInclusive: true,
|
|
|
+ remainingCond: q.remainCond(rest, compCond, eqLen, rangeIdx),
|
|
|
+ }
|
|
|
+
|
|
|
+ // 全部等值 → 精确键 (此时 prefix == total, fillFF 无事可做);
|
|
|
+ // 只消费了等值前缀 → 该前缀下的全部键: [前缀||00.., 前缀||FF..]。
|
|
|
+ if eqLen == n || rangeIdx < 0 {
|
|
|
+ fillFF(endKey, prefix)
|
|
|
+ return plan
|
|
|
+ }
|
|
|
+
|
|
|
+ rc := &cons[rangeIdx]
|
|
|
+ size := lay.sizes[rangeIdx]
|
|
|
+
|
|
|
+ // 下界: [前缀 || 分量值 || 00..]
|
|
|
+ if rc.lo != nil {
|
|
|
+ copy(startKey[prefix:], rc.lo)
|
|
|
+ if !rc.loInc {
|
|
|
+ // 严格下界: 要跳过分量 == lo 的全部键, 即把 (前缀||lo) 整体加一
|
|
|
+ if !incBE(startKey[:prefix+size]) {
|
|
|
+ return nil // 已经是最大键, 该条件不可能有匹配行
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上界: [前缀 || 分量值 || FF..]
|
|
|
+ if rc.hi == nil {
|
|
|
+ fillFF(endKey, prefix)
|
|
|
+ } else if rc.hiInc {
|
|
|
+ copy(endKey[prefix:], rc.hi)
|
|
|
+ fillFF(endKey, prefix+size)
|
|
|
+ } else {
|
|
|
+ // 严格上界: 分量 == hi 的键全部排除
|
|
|
+ copy(endKey[prefix:], rc.hi)
|
|
|
+ plan.endInclusive = false
|
|
|
+ }
|
|
|
+ return plan
|
|
|
+}
|
|
|
+
|
|
|
+// remainCond 汇总索引区间没能表达、仍需逐行过滤的条件。
|
|
|
+// 已被区间精确覆盖的分量 (等值前缀 + 那个范围分量) 上的条件可以安全丢掉。
|
|
|
+func (q *Query) remainCond(rest []Condition, compCond [][]Condition, eqLen, rangeIdx int) Condition {
|
|
|
+ remaining := rest
|
|
|
+ for j := range compCond {
|
|
|
+ if j < eqLen || j == rangeIdx {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ remaining = append(remaining, compCond[j]...)
|
|
|
+ }
|
|
|
+ switch len(remaining) {
|
|
|
+ case 0:
|
|
|
+ return nil
|
|
|
+ case 1:
|
|
|
+ return remaining[0]
|
|
|
+ default:
|
|
|
+ return And(remaining...)
|
|
|
}
|
|
|
- return bestPlan
|
|
|
}
|
|
|
|
|
|
// ======================== 核心查询 ========================
|