diff --git a/pkg/sql/opt/memo/check_expr.go b/pkg/sql/opt/memo/check_expr.go index 88f0fda176ed..dbb1b79c8270 100644 --- a/pkg/sql/opt/memo/check_expr.go +++ b/pkg/sql/opt/memo/check_expr.go @@ -222,11 +222,8 @@ func (m *Memo) CheckExpr(e opt.Expr) { if t.Cols.SubsetOf(t.Input.Relational().OutputCols) { panic(errors.AssertionFailedf("lookup join with no lookup columns")) } - switch t.JoinType { - case opt.AntiJoinOp: - if len(t.RemoteLookupExpr) > 0 { - panic(errors.AssertionFailedf("anti join with a non-empty RemoteLookupExpr")) - } + if t.JoinType == opt.AntiJoinOp && len(t.RemoteLookupExpr) > 0 { + panic(errors.AssertionFailedf("anti join with a non-empty RemoteLookupExpr")) } var requiredCols opt.ColSet requiredCols.UnionWith(t.Relational().OutputCols) @@ -239,10 +236,17 @@ func (m *Memo) CheckExpr(e opt.Expr) { for i := range t.KeyCols { requiredCols.Add(t.Table.ColumnID(idx.Column(i).Ordinal())) } - if !t.Cols.SubsetOf(requiredCols) { + projectedCols := t.Cols + if t.JoinType == opt.SemiJoinOp || t.JoinType == opt.AntiJoinOp { + // Semi and anti joins have an implicit projection that removes the + // columns from the right side. Therefore, we're not strict about removing + // right-side columns from t.Cols. + projectedCols = t.Cols.Intersection(t.Input.Relational().OutputCols) + } + if !projectedCols.SubsetOf(requiredCols) { panic(errors.AssertionFailedf( "lookup join with columns %s that are not required; required: %s", - t.Cols, requiredCols, + projectedCols, requiredCols, )) } if t.IsFirstJoinInPairedJoiner {