github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/opt/testutils/opttester/opt_steps.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package opttester
    12  
    13  import (
    14  	"github.com/cockroachdb/cockroach/pkg/sql/opt"
    15  	"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
    16  )
    17  
    18  // optSteps implements the stepping algorithm used by the OptTester's OptSteps
    19  // command. See the OptTester.OptSteps comment for more details on the command.
    20  //
    21  // The algorithm works as follows:
    22  //   1. The first time optSteps.next() is called, optSteps returns the starting
    23  //      expression tree, with no transformations applied to it.
    24  //
    25  //   2. Each optSteps.next() call after that will perform N+1 transformations,
    26  //      where N is the the number of steps performed during the previous call
    27  //      (starting at 0 with the first call).
    28  //
    29  //   3. Each optSteps.next() call will build the expression tree from scratch
    30  //      and re-run all transformations that were run in the previous call, plus
    31  //      one additional transformation (N+1). Therefore, the output expression
    32  //      tree from each call will differ from the previous call only by the last
    33  //      transformation's changes.
    34  //
    35  //   4. optSteps hooks the optimizer's MatchedRule event in order to limit the
    36  //      number of transformations that can be applied, as well as to record the
    37  //      name of the last rule that was applied, for later output.
    38  //
    39  //   5. While this works well for normalization rules, exploration rules are
    40  //      more difficult. This is because exploration rules are not guaranteed to
    41  //      produce a lower cost tree. Unless extra measures are taken, the returned
    42  //      Expr would not include the changed portion of the Memo, since Expr only
    43  //      shows the lowest cost path through the Memo.
    44  //
    45  //   6. To address this issue, optSteps hooks the optimizer's AppliedRule event
    46  //      and records the expression(s) that the last transformation has affected.
    47  //      It then re-runs the optimizer, but this time using a special Coster
    48  //      implementation that fools the optimizer into thinking that the new
    49  //      expression(s) have the lowest cost. The coster does this by assigning an
    50  //      infinite cost to all other expressions in the same group as the new
    51  //      expression(s), as well as in all ancestor groups.
    52  //
    53  type optSteps struct {
    54  	tester *OptTester
    55  
    56  	fo *forcingOptimizer
    57  
    58  	// steps is the maximum number of rules that can be applied by the optimizer
    59  	// during the current iteration.
    60  	steps int
    61  
    62  	// expr is the expression tree produced by the most recent optSteps iteration.
    63  	expr opt.Expr
    64  
    65  	// better is true if expr is lower cost than the expression tree produced by
    66  	// the previous iteration of optSteps.
    67  	better bool
    68  
    69  	// best is the textual representation of the most recent expression tree that
    70  	// was an improvement over the previous best tree.
    71  	best string
    72  }
    73  
    74  func newOptSteps(tester *OptTester) *optSteps {
    75  	return &optSteps{tester: tester}
    76  }
    77  
    78  // Root returns the node tree produced by the most recent optSteps iteration.
    79  func (os *optSteps) Root() opt.Expr {
    80  	return os.expr
    81  }
    82  
    83  // LastRuleName returns the name of the rule that was most recently matched by
    84  // the optimizer.
    85  func (os *optSteps) LastRuleName() opt.RuleName {
    86  	return os.fo.lastMatched
    87  }
    88  
    89  // IsBetter returns true if root is lower cost than the expression tree
    90  // produced by the previous iteration of optSteps.
    91  func (os *optSteps) IsBetter() bool {
    92  	return os.better
    93  }
    94  
    95  // Done returns true if there are no more rules to apply. Further calls to the
    96  // next method will result in a panic.
    97  func (os *optSteps) Done() bool {
    98  	// remaining starts out equal to steps, and is decremented each time a rule
    99  	// is applied. If it never reaches zero, then all possible rules were
   100  	// already applied, and optimization is complete.
   101  	return os.fo != nil && os.fo.remaining != 0
   102  }
   103  
   104  // Next triggers the next iteration of optSteps. If there is no error, then
   105  // results of the iteration can be accessed via the Root, LastRuleName, and
   106  // IsBetter methods.
   107  func (os *optSteps) Next() error {
   108  	if os.Done() {
   109  		panic("iteration already complete")
   110  	}
   111  
   112  	fo, err := newForcingOptimizer(os.tester, os.steps, false /* ignoreNormRules */)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	os.fo = fo
   118  	os.expr = fo.Optimize()
   119  	text := os.expr.String()
   120  
   121  	// If the expression text changes, then it must have gotten better.
   122  	os.better = text != os.best
   123  	if os.better {
   124  		os.best = text
   125  	} else if !os.Done() {
   126  		// The expression is not better, so suppress the lowest cost expressions
   127  		// so that the changed portions of the tree will be part of the output.
   128  		fo2, err := newForcingOptimizer(os.tester, os.steps, false /* ignoreNormRules */)
   129  		if err != nil {
   130  			return err
   131  		}
   132  
   133  		if fo.lastAppliedSource == nil {
   134  			// This was a normalization that created a new memo group.
   135  			fo2.RestrictToExpr(fo.LookupPath(fo.lastAppliedTarget))
   136  		} else if fo.lastAppliedTarget == nil {
   137  			// This was an exploration rule that didn't add any expressions to the
   138  			// group, so only ancestor groups need to be suppressed.
   139  			path := fo.LookupPath(fo.lastAppliedSource)
   140  			fo2.RestrictToExpr(path[:len(path)-1])
   141  		} else {
   142  			// This was an exploration rule that added one or more expressions to
   143  			// an existing group. Suppress all other members of the group.
   144  			member := fo.lastAppliedTarget.(memo.RelExpr)
   145  			for member != nil {
   146  				fo2.RestrictToExpr(fo.LookupPath(member))
   147  				member = member.NextExpr()
   148  			}
   149  		}
   150  		os.expr = fo2.Optimize()
   151  	}
   152  
   153  	os.steps++
   154  	return nil
   155  }