diff --git a/pkg/sql/opt/norm/rules/with.opt b/pkg/sql/opt/norm/rules/with.opt index f1e055ef1622..fad421f4e289 100644 --- a/pkg/sql/opt/norm/rules/with.opt +++ b/pkg/sql/opt/norm/rules/with.opt @@ -64,3 +64,20 @@ ) $private ) + +# ApplyBindingRowCountToCTE makes sure to that stats changes are propagated on +# placeholder assignment. +[ApplyBindingRowCountToCTE, Normalize] +(RecursiveCTE + $binding:* + $initial:* & (RowCountDifferent $binding $initial) + $recursive:* + $private:* +) +=> +(RecursiveCTE + (ApplyBindingRowCount $binding $initial) + $initial + $recursive + $private +) diff --git a/pkg/sql/opt/norm/with_funcs.go b/pkg/sql/opt/norm/with_funcs.go index f04152efc550..2ea3f52206d1 100644 --- a/pkg/sql/opt/norm/with_funcs.go +++ b/pkg/sql/opt/norm/with_funcs.go @@ -297,3 +297,23 @@ func (c *CustomFuncs) CanAddRecursiveLimit( func (c *CustomFuncs) GetRecursiveWithID(private *memo.RecursiveCTEPrivate) opt.WithID { return private.WithID } + +func (c *CustomFuncs) ApplyBindingRowCount( + binding, initial memo.RelExpr, +) memo.RelExpr { + // The properties of the binding are tricky: the recursive expression is + // invoked repeatedly and these must hold each time. We can't use the initial + // expression's properties directly, as those only hold the first time the + // recursive query is executed. We don't really know the input row count, + // except for the first time we run the recursive query. We don't have + // anything better though. + initialRowCount := initial.Relational().Statistics().RowCount + binding.Relational().Statistics().RowCount = initialRowCount + return binding +} + +func (c *CustomFuncs) RowCountDifferent( + binding, initial memo.RelExpr, +) bool { + return initial.Relational().Statistics().RowCount != binding.Relational().Statistics().RowCount +} diff --git a/pkg/sql/opt/xform/testdata/rules/cte b/pkg/sql/opt/xform/testdata/rules/cte new file mode 100644 index 000000000000..f365d1e18fc3 --- /dev/null +++ b/pkg/sql/opt/xform/testdata/rules/cte @@ -0,0 +1,57 @@ +exec-ddl +CREATE TABLE xy (x INT, y INT, INDEX (x) STORING (y)); +---- + +exec-ddl +ALTER TABLE xy INJECT STATISTICS '[ + { + "columns": ["x"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 100000, + "distinct_count": 100000 + }, + { + "columns": ["y"], + "created_at": "2018-01-01 1:00:00.00000+00:00", + "row_count": 100000, + "distinct_count": 2 + } +]'; +---- + +# Regression test for #99389. +assign-placeholders-opt query-args=(1) format=show-stats +WITH RECURSIVE cte(a,b) AS ( + (SELECT * FROM xy WHERE x = $1) + UNION ALL + (SELECT * FROM cte WHERE False) +) +SELECT * FROM cte; +---- +project + ├── columns: a:10 b:11 + ├── stats: [rows=10] + ├── recursive-c-t-e + │ ├── columns: a:6 b:7 + │ ├── working table binding: &1 + │ ├── initial columns: x:1 y:2 + │ ├── recursive columns: a:8 b:9 + │ ├── stats: [rows=10] + │ ├── fake-rel + │ │ ├── columns: a:6 b:7 + │ │ ├── cardinality: [1 - ] + │ │ └── stats: [rows=1.00001] + │ ├── scan xy@xy_x_idx + │ │ ├── columns: x:1!null y:2 + │ │ ├── constraint: /1/3: [/1 - /1] + │ │ ├── stats: [rows=1.00001, distinct(1)=1, null(1)=0] + │ │ └── fd: ()-->(1) + │ └── values + │ ├── columns: a:8!null b:9!null + │ ├── cardinality: [0 - 0] + │ ├── stats: [rows=0] + │ ├── key: () + │ └── fd: ()-->(8,9) + └── projections + ├── a:6 [as=a:10, outer=(6)] + └── b:7 [as=b:11, outer=(7)]