mirror of
https://gitee.com/rulego/streamsql.git
synced 2026-08-05 10:51:21 +00:00
perf: WHERE 快速路径扩展到扁平 AND/OR 复合条件
对 `a OP lit (&&|||) b OP lit ...`(纯数值、无括号/字符串)在编译期拆分识别为 fastCompound,运行期逐段快速比较,任一段类型不匹配则整体回退 expr-lang。 rsql 已把 AND/OR 下译为 &&/||,故按 &&/|| 拆分。 基准: MultiFieldFilter (temp>20 && humid<80) 1286ns/6alloc -> 668ns/3alloc (1.9x)
This commit is contained in:
+105
-6
@@ -5,6 +5,7 @@ import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/expr-lang/expr/vm"
|
||||
@@ -17,11 +18,13 @@ type Condition interface {
|
||||
type ExprCondition struct {
|
||||
program *vm.Program
|
||||
// fast is a compiled fast-path for trivial `field OP literal` comparisons.
|
||||
// When non-nil and the runtime value matches its assumed type, Evaluate
|
||||
// skips the expr-lang VM (and its reflect-based map field fetch). Anything
|
||||
// the fast path cannot handle falls back to expr-lang, so semantics are
|
||||
// compound extends it to flat `a AND/OR b AND/OR ...` of such comparisons.
|
||||
// When set and the runtime values match the assumed types, Evaluate skips
|
||||
// the expr-lang VM (and its reflect-based map field fetch). Anything the
|
||||
// fast path cannot handle falls back to expr-lang, so semantics are
|
||||
// preserved exactly.
|
||||
fast *fastCompare
|
||||
fast *fastCompare
|
||||
compound *fastCompound
|
||||
}
|
||||
|
||||
func NewExprCondition(expression string) (Condition, error) {
|
||||
@@ -58,11 +61,21 @@ func NewExprCondition(expression string) (Condition, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ExprCondition{program: program, fast: tryFastCompare(expression)}, nil
|
||||
ec := &ExprCondition{program: program}
|
||||
if fc := tryFastCompound(expression); fc != nil {
|
||||
ec.compound = fc
|
||||
} else if fc := tryFastCompare(expression); fc != nil {
|
||||
ec.fast = fc
|
||||
}
|
||||
return ec, nil
|
||||
}
|
||||
|
||||
func (ec *ExprCondition) Evaluate(env interface{}) bool {
|
||||
if ec.fast != nil {
|
||||
if ec.compound != nil {
|
||||
if r, ok := ec.compound.eval(env); ok {
|
||||
return r
|
||||
}
|
||||
} else if ec.fast != nil {
|
||||
if r, ok := ec.fast.eval(env); ok {
|
||||
return r
|
||||
}
|
||||
@@ -131,6 +144,92 @@ func (fc *fastCompare) eval(env interface{}) (bool, bool) {
|
||||
return compareNum(f, fc.op, fc.numLit), true
|
||||
}
|
||||
|
||||
// fastCompound is a flat AND/OR chain of simple comparisons. It fires only when
|
||||
// every part fast-evaluates; if any part cannot (type mismatch/missing), the
|
||||
// whole condition falls back to expr-lang.
|
||||
type fastCompound struct {
|
||||
op string // "AND" or "OR"
|
||||
parts []*fastCompare
|
||||
}
|
||||
|
||||
func (fc *fastCompound) eval(env interface{}) (bool, bool) {
|
||||
data, ok := env.(map[string]interface{})
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
result := fc.op == "AND" // AND starts true, OR starts false
|
||||
for _, p := range fc.parts {
|
||||
r, ok := p.evalMap(data)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
if fc.op == "AND" {
|
||||
result = result && r
|
||||
} else {
|
||||
result = result || r
|
||||
}
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
// evalMap is like eval but assumes env is already a map (used by fastCompound).
|
||||
func (fc *fastCompare) evalMap(data map[string]interface{}) (bool, bool) {
|
||||
v, exists := data[fc.field]
|
||||
if !exists || v == nil {
|
||||
return false, false
|
||||
}
|
||||
if fc.isString {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
return compareStr(s, fc.op, fc.strLit), true
|
||||
}
|
||||
f, ok := toFloat64Fast(v)
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
return compareNum(f, fc.op, fc.numLit), true
|
||||
}
|
||||
|
||||
// fastAndOr splits a flat condition on its sole logical operator (all && or
|
||||
// all ||). The rsql parser lowers SQL AND/OR to &&/|| before reaching here, so
|
||||
// we split on the C-style operators.
|
||||
var fastAndOr = regexp.MustCompile(`\s*(&&|\|\|)\s*`)
|
||||
|
||||
func tryFastCompound(expression string) *fastCompound {
|
||||
// Conservative: only attempt when there is no grouping and no string
|
||||
// literals, so a naive split on &&/|| cannot break a quoted token.
|
||||
if strings.ContainsAny(expression, "()'\"") {
|
||||
return nil
|
||||
}
|
||||
hasAnd := strings.Contains(expression, "&&")
|
||||
hasOr := strings.Contains(expression, "||")
|
||||
var op string
|
||||
if hasAnd && hasOr {
|
||||
return nil // mixed logic — fall back
|
||||
} else if hasAnd {
|
||||
op = "AND"
|
||||
} else if hasOr {
|
||||
op = "OR"
|
||||
} else {
|
||||
return nil // single comparison handled by tryFastCompare
|
||||
}
|
||||
parts := fastAndOr.Split(expression, -1)
|
||||
if len(parts) < 2 {
|
||||
return nil
|
||||
}
|
||||
compares := make([]*fastCompare, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
fc := tryFastCompare(p)
|
||||
if fc == nil {
|
||||
return nil
|
||||
}
|
||||
compares = append(compares, fc)
|
||||
}
|
||||
return &fastCompound{op: op, parts: compares}
|
||||
}
|
||||
|
||||
func toFloat64Fast(v interface{}) (float64, bool) {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
|
||||
@@ -16,15 +16,20 @@ func TestFastCompareMatchesExprLang(t *testing.T) {
|
||||
"status == 'active'",
|
||||
"status != 'inactive'",
|
||||
"label > 'm'",
|
||||
// Flat compound (numeric only, no parens/quotes) -> fastCompound.
|
||||
// rsql lowers AND/OR to &&/||, which is the form reaching NewExprCondition.
|
||||
"temperature > 20 && humidity < 80",
|
||||
"temperature > 20 || pressure > 1000",
|
||||
"temperature > 20 && humidity < 80 && cnt >= 1",
|
||||
}
|
||||
rows := []map[string]interface{}{
|
||||
{"temperature": 25.5, "cnt": int64(10), "status": "active", "label": "zebra"},
|
||||
{"temperature": 20.0, "cnt": 9, "status": "inactive", "label": "apple"},
|
||||
{"temperature": 30, "cnt": int(11), "status": "active", "label": "m"},
|
||||
{"temperature": 19.9, "cnt": uint(10), "status": "", "label": "zzz"},
|
||||
{"temperature": 25.5, "cnt": int64(10), "status": "active", "label": "zebra", "humidity": 60.0, "pressure": 1100},
|
||||
{"temperature": 20.0, "cnt": 9, "status": "inactive", "label": "apple", "humidity": 90.0, "pressure": 900},
|
||||
{"temperature": 30, "cnt": int(11), "status": "active", "label": "m", "humidity": 50, "pressure": 1001},
|
||||
{"temperature": 19.9, "cnt": uint(10), "status": "", "label": "zzz", "humidity": 80, "pressure": 2000},
|
||||
{"temperature": "hot", "status": 1}, // wrong types -> must fall back
|
||||
{"humidity": 50}, // field missing -> fall back
|
||||
{"temperature": float32(21), "cnt": 10},
|
||||
{"temperature": float32(21), "cnt": 10, "humidity": 70},
|
||||
}
|
||||
|
||||
for _, condStr := range conditions {
|
||||
|
||||
Reference in New Issue
Block a user