Files

714 lines
19 KiB
Go

package metricsql
import (
"fmt"
"sort"
"strings"
)
// Optimize optimizes e in order to improve its performance.
//
// It performs the following optimizations:
//
// - Adds missing filters to `foo{filters1} op bar{filters2}`
// according to https://utcc.utoronto.ca/~cks/space/blog/sysadmin/PrometheusLabelNonOptimization
// I.e. such query is converted to `foo{filters1, filters2} op bar{filters1, filters2}`
func Optimize(e Expr) Expr {
if !canOptimize(e) {
return e
}
eCopy := Clone(e)
optimizeInplace(eCopy)
return eCopy
}
func canOptimize(e Expr) bool {
switch t := e.(type) {
case *RollupExpr:
return canOptimize(t.Expr) || canOptimize(t.At)
case *FuncExpr:
for _, arg := range t.Args {
if canOptimize(arg) {
return true
}
}
case *AggrFuncExpr:
for _, arg := range t.Args {
if canOptimize(arg) {
return true
}
}
case *BinaryOpExpr:
return true
}
return false
}
// Clone clones the given expression e and returns the cloned copy.
func Clone(e Expr) Expr {
s := e.AppendString(nil)
eCopy, err := Parse(string(s))
if err != nil {
panic(fmt.Errorf("BUG: cannot parse the expression %q: %w", s, err))
}
return eCopy
}
func optimizeInplace(e Expr) {
switch t := e.(type) {
case *RollupExpr:
optimizeInplace(t.Expr)
optimizeInplace(t.At)
case *FuncExpr:
optimizeArgsInplace(t.Args)
case *AggrFuncExpr:
optimizeArgsInplace(t.Args)
case *BinaryOpExpr:
optimizeInplace(t.Left)
optimizeInplace(t.Right)
lfs := getCommonLabelFilters(t)
pushdownBinaryOpFiltersInplace(lfs, t)
}
}
func optimizeArgsInplace(args []Expr) {
for _, arg := range args {
optimizeInplace(arg)
}
}
func getCommonLabelFilters(e Expr) []LabelFilter {
switch t := e.(type) {
case *MetricExpr:
return getCommonLabelFiltersWithoutMetricName(t.LabelFilterss)
case *RollupExpr:
return getCommonLabelFilters(t.Expr)
case *FuncExpr:
args := t.Args
switch strings.ToLower(t.Name) {
case "label_set":
return getCommonLabelFiltersForLabelSet(args)
case "label_replace", "label_join", "label_map", "label_match", "label_mismatch", "label_transform":
return getCommonLabelFiltersForLabelReplace(args)
case "label_copy", "label_move":
return getCommonLabelFiltersForLabelCopy(args)
case "label_del", "label_uppercase", "label_lowercase", "labels_equal":
return getCommonLabelFiltersForLabelDel(args)
case "label_keep":
return getCommonLabelFiltersForLabelKeep(args)
case "count_values_over_time":
return getCommonLabelFiltersForCountValuesOverTime(args)
case "range_normalize", "union", "":
return intersectLabelFiltersForAllArgs(args)
default:
arg := getFuncArgForOptimization(t.Name, args)
if arg == nil {
return nil
}
return getCommonLabelFilters(arg)
}
case *AggrFuncExpr:
args := t.Args
if strings.ToLower(t.Name) == "count_values" {
if len(args) != 2 {
return nil
}
lfs := getCommonLabelFilters(args[1])
lfs = dropLabelFiltersForLabelName(lfs, args[0])
return trimFiltersByAggrModifier(lfs, t)
}
if canAcceptMultipleArgsForAggrFunc(t.Name) {
lfs := intersectLabelFiltersForAllArgs(args)
return trimFiltersByAggrModifier(lfs, t)
}
arg := getFuncArgForOptimization(t.Name, args)
if arg == nil {
return nil
}
lfs := getCommonLabelFilters(arg)
return trimFiltersByAggrModifier(lfs, t)
case *BinaryOpExpr:
lfsLeft := getCommonLabelFilters(t.Left)
lfsRight := getCommonLabelFilters(t.Right)
var lfs []LabelFilter
switch strings.ToLower(t.Op) {
case "or":
// {fCommon, f1} or {fCommon, f2} -> {fCommon}
// {fCommon, f1} or on() {fCommon, f2} -> {}
// {fCommon, f1} or on(fCommon) {fCommon, f2} -> {fCommon}
// {fCommon, f1} or on(f1) {fCommon, f2} -> {}
// {fCommon, f1} or on(f2) {fCommon, f2} -> {}
// {fCommon, f1} or on(f3) {fCommon, f2} -> {}
lfs = intersectLabelFilters(lfsLeft, lfsRight)
return TrimFiltersByGroupModifier(lfs, t)
case "unless":
// {f1} unless {f2} -> {f1}
// {f1} unless on() {f2} -> {}
// {f1} unless on(f1) {f2} -> {f1}
// {f1} unless on(f2) {f2} -> {}
// {f1} unless on(f1, f2) {f2} -> {f1}
// {f1} unless on(f3) {f2} -> {}
return TrimFiltersByGroupModifier(lfsLeft, t)
case "ifnot":
// remove right from left, so filter in left can be pushed down to right.
// {f1} ifnot `any` -> {f1}
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8435
return TrimFiltersByGroupModifier(lfsLeft, t)
default:
switch strings.ToLower(t.JoinModifier.Op) {
case "group_left":
// {f1} * group_left() {f2} -> {f1, f2}
// {f1} * on() group_left() {f2} -> {f1}
// {f1} * on(f1) group_left() {f2} -> {f1}
// {f1} * on(f2) group_left() {f2} -> {f1, f2}
// {f1} * on(f1, f2) group_left() {f2} -> {f1, f2}
// {f1} * on(f3) group_left() {f2} -> {f1}
lfsRight = TrimFiltersByGroupModifier(lfsRight, t)
return unionLabelFilters(lfsLeft, lfsRight)
case "group_right":
// {f1} * group_right() {f2} -> {f1, f2}
// {f1} * on() group_right() {f2} -> {f2}
// {f1} * on(f1) group_right() {f2} -> {f1, f2}
// {f1} * on(f2) group_right() {f2} -> {f2}
// {f1} * on(f1, f2) group_right() {f2} -> {f1, f2}
// {f1} * on(f3) group_right() {f2} -> {f2}
lfsLeft = TrimFiltersByGroupModifier(lfsLeft, t)
return unionLabelFilters(lfsLeft, lfsRight)
default:
// {f1} * {f2} -> {f1, f2}
// {f1} * on() {f2} -> {}
// {f1} * on(f1) {f2} -> {f1}
// {f1} * on(f2) {f2} -> {f2}
// {f1} * on(f1, f2) {f2} -> {f2}
// {f1} * on(f3} {f2} -> {}
lfs = unionLabelFilters(lfsLeft, lfsRight)
return TrimFiltersByGroupModifier(lfs, t)
}
}
default:
return nil
}
}
func intersectLabelFiltersForAllArgs(args []Expr) []LabelFilter {
if len(args) == 0 {
return nil
}
lfs := getCommonLabelFilters(args[0])
for _, arg := range args[1:] {
lfsNext := getCommonLabelFilters(arg)
lfs = intersectLabelFilters(lfs, lfsNext)
}
return lfs
}
func getCommonLabelFiltersForCountValuesOverTime(args []Expr) []LabelFilter {
if len(args) != 2 {
return nil
}
lfs := getCommonLabelFilters(args[1])
return dropLabelFiltersForLabelName(lfs, args[0])
}
func getCommonLabelFiltersForLabelKeep(args []Expr) []LabelFilter {
if len(args) == 0 {
return nil
}
lfs := getCommonLabelFilters(args[0])
lfs = keepLabelFiltersForLabelNames(lfs, args[1:])
return lfs
}
func getCommonLabelFiltersForLabelDel(args []Expr) []LabelFilter {
if len(args) == 0 {
return nil
}
lfs := getCommonLabelFilters(args[0])
lfs = dropLabelFiltersForLabelNames(lfs, args[1:])
return lfs
}
func getCommonLabelFiltersForLabelCopy(args []Expr) []LabelFilter {
if len(args) == 0 {
return nil
}
lfs := getCommonLabelFilters(args[0])
args = args[1:]
var labelNames []Expr
for i := 0; i < len(args); i += 2 {
if i+1 >= len(args) {
return nil
}
labelNames = append(labelNames, args[i+1])
}
lfs = dropLabelFiltersForLabelNames(lfs, labelNames)
return lfs
}
func getCommonLabelFiltersForLabelReplace(args []Expr) []LabelFilter {
if len(args) < 2 {
return nil
}
lfs := getCommonLabelFilters(args[0])
return dropLabelFiltersForLabelName(lfs, args[1])
}
func getCommonLabelFiltersForLabelSet(args []Expr) []LabelFilter {
if len(args) == 0 {
return nil
}
lfs := getCommonLabelFilters(args[0])
args = args[1:]
for i := 0; i < len(args); i += 2 {
labelName := args[i]
if i+1 >= len(args) {
return nil
}
labelValue := args[i+1]
seLabelName, ok := labelName.(*StringExpr)
if !ok {
return nil
}
seLabelValue, ok := labelValue.(*StringExpr)
if !ok {
return nil
}
if seLabelName.S == "__name__" {
continue
}
lfs = dropLabelFiltersForLabelName(lfs, labelName)
lfs = append(lfs, LabelFilter{
Label: seLabelName.S,
Value: seLabelValue.S,
})
}
return lfs
}
func trimFiltersByAggrModifier(lfs []LabelFilter, afe *AggrFuncExpr) []LabelFilter {
switch strings.ToLower(afe.Modifier.Op) {
case "by":
return filterLabelFiltersOn(lfs, afe.Modifier.Args)
case "without":
return filterLabelFiltersIgnoring(lfs, afe.Modifier.Args)
default:
return nil
}
}
// TrimFiltersByGroupModifier trims lfs by the specified be.GroupModifier.Op (e.g. on() or ignoring()).
//
// The following cases are possible:
// - It returns lfs as is if be doesn't contain any group modifier
// - It returns only filters specified in on()
// - It drops filters specified inside ignoring()
func TrimFiltersByGroupModifier(lfs []LabelFilter, be *BinaryOpExpr) []LabelFilter {
switch strings.ToLower(be.GroupModifier.Op) {
case "on":
return filterLabelFiltersOn(lfs, be.GroupModifier.Args)
case "ignoring":
return filterLabelFiltersIgnoring(lfs, be.GroupModifier.Args)
default:
return lfs
}
}
func getCommonLabelFiltersWithoutMetricName(lfss [][]LabelFilter) []LabelFilter {
if len(lfss) == 0 {
return nil
}
lfsA := getLabelFiltersWithoutMetricName(lfss[0])
for _, lfs := range lfss[1:] {
if len(lfsA) == 0 {
return nil
}
lfsB := getLabelFiltersWithoutMetricName(lfs)
lfsA = intersectLabelFilters(lfsA, lfsB)
}
return lfsA
}
func getLabelFiltersWithoutMetricName(lfs []LabelFilter) []LabelFilter {
lfsNew := make([]LabelFilter, 0, len(lfs))
for _, lf := range lfs {
if lf.Label != "__name__" {
lfsNew = append(lfsNew, lf)
}
}
return lfsNew
}
// PushdownBinaryOpFilters pushes down the given commonFilters to e if possible.
//
// e must be a part of binary operation - either left or right.
//
// For example, if e contains `foo + sum(bar)` and commonFilters={x="y"},
// then the returned expression will contain `foo{x="y"} + sum(bar)`.
// The `{x="y"}` cannot be pusehd down to `sum(bar)`, since this may change binary operation results.
func PushdownBinaryOpFilters(e Expr, commonFilters []LabelFilter) Expr {
if len(commonFilters) == 0 {
// Fast path - nothing to push down.
return e
}
eCopy := Clone(e)
pushdownBinaryOpFiltersInplace(commonFilters, eCopy)
return eCopy
}
func pushdownBinaryOpFiltersInplace(lfs []LabelFilter, e Expr) {
if len(lfs) == 0 {
return
}
switch t := e.(type) {
case *MetricExpr:
for i, lfsLocal := range t.LabelFilterss {
lfsLocal = unionLabelFilters(lfsLocal, lfs)
sortLabelFilters(lfsLocal)
t.LabelFilterss[i] = lfsLocal
}
case *RollupExpr:
pushdownBinaryOpFiltersInplace(lfs, t.Expr)
case *FuncExpr:
args := t.Args
switch strings.ToLower(t.Name) {
case "label_set":
pushdownLabelFiltersForLabelSet(lfs, args)
case "label_replace", "label_join", "label_map", "label_match", "label_mismatch", "label_transform":
pushdownLabelFiltersForLabelReplace(lfs, args)
case "label_copy", "label_move":
pushdownLabelFiltersForLabelCopy(lfs, args)
case "label_del", "label_uppercase", "label_lowercase", "labels_equal":
pushdownLabelFiltersForLabelDel(lfs, args)
case "label_keep":
pushdownLabelFiltersForLabelKeep(lfs, args)
case "count_values_over_time":
pushdownLabelFiltersForCountValuesOverTime(lfs, args)
case "range_normalize", "union", "":
pushdownLabelFiltersForAllArgs(lfs, args)
default:
arg := getFuncArgForOptimization(t.Name, args)
if arg != nil {
pushdownBinaryOpFiltersInplace(lfs, arg)
}
}
case *AggrFuncExpr:
lfs = trimFiltersByAggrModifier(lfs, t)
args := t.Args
if strings.ToLower(t.Name) == "count_values" {
if len(args) == 2 {
lfs = dropLabelFiltersForLabelName(lfs, args[0])
pushdownBinaryOpFiltersInplace(lfs, args[1])
}
} else if canAcceptMultipleArgsForAggrFunc(t.Name) {
pushdownLabelFiltersForAllArgs(lfs, args)
} else {
arg := getFuncArgForOptimization(t.Name, args)
if arg != nil {
pushdownBinaryOpFiltersInplace(lfs, arg)
}
}
case *BinaryOpExpr:
lfs = TrimFiltersByGroupModifier(lfs, t)
pushdownBinaryOpFiltersInplace(lfs, t.Left)
pushdownBinaryOpFiltersInplace(lfs, t.Right)
}
}
func pushdownLabelFiltersForAllArgs(lfs []LabelFilter, args []Expr) {
for _, arg := range args {
pushdownBinaryOpFiltersInplace(lfs, arg)
}
}
func pushdownLabelFiltersForCountValuesOverTime(lfs []LabelFilter, args []Expr) {
if len(args) != 2 {
return
}
lfs = dropLabelFiltersForLabelName(lfs, args[0])
pushdownBinaryOpFiltersInplace(lfs, args[1])
}
func pushdownLabelFiltersForLabelKeep(lfs []LabelFilter, args []Expr) {
if len(args) == 0 {
return
}
lfs = keepLabelFiltersForLabelNames(lfs, args[1:])
pushdownBinaryOpFiltersInplace(lfs, args[0])
}
func pushdownLabelFiltersForLabelDel(lfs []LabelFilter, args []Expr) {
if len(args) == 0 {
return
}
lfs = dropLabelFiltersForLabelNames(lfs, args[1:])
pushdownBinaryOpFiltersInplace(lfs, args[0])
}
func pushdownLabelFiltersForLabelCopy(lfs []LabelFilter, args []Expr) {
if len(args) == 0 {
return
}
arg := args[0]
args = args[1:]
var labelNames []Expr
for i := 0; i < len(args); i += 2 {
if i+1 >= len(args) {
return
}
labelNames = append(labelNames, args[i+1])
}
lfs = dropLabelFiltersForLabelNames(lfs, labelNames)
pushdownBinaryOpFiltersInplace(lfs, arg)
}
func pushdownLabelFiltersForLabelReplace(lfs []LabelFilter, args []Expr) {
if len(args) < 2 {
return
}
lfs = dropLabelFiltersForLabelName(lfs, args[1])
pushdownBinaryOpFiltersInplace(lfs, args[0])
}
func pushdownLabelFiltersForLabelSet(lfs []LabelFilter, args []Expr) {
if len(args) == 0 {
return
}
arg := args[0]
args = args[1:]
var labelNames []Expr
for i := 0; i < len(args); i += 2 {
labelNames = append(labelNames, args[i])
}
lfs = dropLabelFiltersForLabelNames(lfs, labelNames)
pushdownBinaryOpFiltersInplace(lfs, arg)
}
func intersectLabelFilters(lfsA, lfsB []LabelFilter) []LabelFilter {
if len(lfsA) == 0 || len(lfsB) == 0 {
return nil
}
m := getLabelFiltersMap(lfsA)
var b []byte
var lfs []LabelFilter
for _, lf := range lfsB {
b = lf.AppendString(b[:0])
if _, ok := m[string(b)]; ok {
lfs = append(lfs, lf)
}
}
return lfs
}
func keepLabelFiltersForLabelNames(lfs []LabelFilter, labelNames []Expr) []LabelFilter {
m := make(map[string]struct{}, len(labelNames))
for _, labelName := range labelNames {
seLabelName, ok := labelName.(*StringExpr)
if !ok {
return nil
}
m[seLabelName.S] = struct{}{}
}
var lfsDst []LabelFilter
for _, lf := range lfs {
if _, ok := m[lf.Label]; ok {
lfsDst = append(lfsDst, lf)
}
}
return lfsDst
}
func dropLabelFiltersForLabelNames(lfs []LabelFilter, labelNames []Expr) []LabelFilter {
for _, labelName := range labelNames {
lfs = dropLabelFiltersForLabelName(lfs, labelName)
}
return lfs
}
func dropLabelFiltersForLabelName(lfs []LabelFilter, labelName Expr) []LabelFilter {
seLabelName, ok := labelName.(*StringExpr)
if !ok {
return nil
}
lfsDst := make([]LabelFilter, 0, len(lfs))
for _, lf := range lfs {
if lf.Label != seLabelName.S {
lfsDst = append(lfsDst, lf)
}
}
return lfsDst
}
func unionLabelFilters(lfsA, lfsB []LabelFilter) []LabelFilter {
if len(lfsA) == 0 {
return lfsB
}
if len(lfsB) == 0 {
return lfsA
}
m := getLabelFiltersMap(lfsA)
var b []byte
lfs := append([]LabelFilter{}, lfsA...)
for _, lf := range lfsB {
b = lf.AppendString(b[:0])
if _, ok := m[string(b)]; !ok {
lfs = append(lfs, lf)
}
}
return lfs
}
func getLabelFiltersMap(lfs []LabelFilter) map[string]struct{} {
m := make(map[string]struct{}, len(lfs))
var b []byte
for _, lf := range lfs {
b = lf.AppendString(b[:0])
m[string(b)] = struct{}{}
}
return m
}
func sortLabelFilters(lfs []LabelFilter) {
// Make sure the first label filter is __name__ (if any)
if len(lfs) > 0 && lfs[0].isMetricNameFilter() {
lfs = lfs[1:]
}
sort.Slice(lfs, func(i, j int) bool {
a, b := lfs[i], lfs[j]
if a.Label != b.Label {
return a.Label < b.Label
}
return a.Value < b.Value
})
}
func filterLabelFiltersOn(lfs []LabelFilter, args []string) []LabelFilter {
if len(args) == 0 {
return nil
}
m := make(map[string]struct{}, len(args))
for _, arg := range args {
m[arg] = struct{}{}
}
var lfsNew []LabelFilter
for _, lf := range lfs {
if _, ok := m[lf.Label]; ok {
lfsNew = append(lfsNew, lf)
}
}
return lfsNew
}
func filterLabelFiltersIgnoring(lfs []LabelFilter, args []string) []LabelFilter {
if len(args) == 0 {
return lfs
}
m := make(map[string]struct{}, len(args))
for _, arg := range args {
m[arg] = struct{}{}
}
var lfsNew []LabelFilter
for _, lf := range lfs {
if _, ok := m[lf.Label]; !ok {
lfsNew = append(lfsNew, lf)
}
}
return lfsNew
}
func getFuncArgForOptimization(funcName string, args []Expr) Expr {
idx := getFuncArgIdxForOptimization(funcName, args)
if idx < 0 || idx >= len(args) {
return nil
}
return args[idx]
}
func getFuncArgIdxForOptimization(funcName string, args []Expr) int {
funcName = strings.ToLower(funcName)
if IsRollupFunc(funcName) {
return getRollupArgIdxForOptimization(funcName, args)
}
if IsTransformFunc(funcName) {
return getTransformArgIdxForOptimization(funcName, args)
}
if IsAggrFunc(funcName) {
return getAggrArgIdxForOptimization(funcName, args)
}
return -1
}
func getAggrArgIdxForOptimization(funcName string, args []Expr) int {
switch strings.ToLower(funcName) {
case "bottomk", "bottomk_avg", "bottomk_max", "bottomk_median", "bottomk_last", "bottomk_min",
"limitk", "outliers_mad", "outliersk", "quantile",
"topk", "topk_avg", "topk_max", "topk_median", "topk_last", "topk_min":
return 1
case "quantiles":
return len(args) - 1
case "count_values":
panic(fmt.Errorf("BUG: count_values must be already handled"))
default:
if canAcceptMultipleArgsForAggrFunc(funcName) {
panic(fmt.Errorf("BUG: %s must be already handled", funcName))
}
return 0
}
}
func canAcceptMultipleArgsForAggrFunc(funcName string) bool {
switch strings.ToLower(funcName) {
case "any", "avg", "count", "distinct", "geomean", "group", "histogram", "mad", "max",
"median", "min", "mode", "share", "stddev", "stdvar", "sum", "sum2", "zscore":
return true
default:
return false
}
}
func getRollupArgIdxForOptimization(funcName string, args []Expr) int {
// This must be kept in sync with GetRollupArgIdx()
switch strings.ToLower(funcName) {
case "count_values_over_time":
panic(fmt.Errorf("BUG: count_values_over_time must be already handled"))
case "absent_over_time":
return -1
case "quantile_over_time", "aggr_over_time",
"hoeffding_bound_lower", "hoeffding_bound_upper":
return 1
case "quantiles_over_time":
return len(args) - 1
default:
return 0
}
}
func getTransformArgIdxForOptimization(funcName string, args []Expr) int {
switch strings.ToLower(funcName) {
case "label_copy", "label_del", "label_join", "label_keep", "label_lowercase", "label_map",
"label_match", "label_mismatch", "label_move", "label_replace", "label_set", "label_transform",
"label_uppercase", "labels_equal", "range_normalize", "", "union":
panic(fmt.Errorf("BUG: %s must be already handled", funcName))
case "drop_common_labels":
return -1
case "absent", "scalar":
return -1
case "end", "now", "pi", "ru", "start", "step", "time":
return -1
case "limit_offset", "histogram_fraction":
return 2
case "buckets_limit", "histogram_quantile", "histogram_share", "range_quantile",
"range_trim_outliers", "range_trim_spikes", "range_trim_zscore":
return 1
case "histogram_quantiles":
return len(args) - 1
default:
return 0
}
}