peter-toth commented on code in PR #58661:
URL: https://github.com/apache/spark/pull/58661#discussion_r3987281465
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PushdownPredicatesAndPruneColumnsForCTEDef.scala:
##########
@@ -77,8 +77,11 @@ object PushdownPredicatesAndPruneColumnsForCTEDef extends
Rule[LogicalPlan] with
val newPredicates = if (isTruePredicate(preds)) {
preds
} else {
- // Make sure we only push down predicates that do not contain
forward CTE references.
- val filteredPredicates = restoreCTEDefAttrs(predicates.filter(_.find
{
+ // Only push down deterministic predicates that do not contain
forward CTE references.
+ // The reference keeps its predicates, so a non-deterministic one
pushed into the
+ // definition as well would be evaluated twice.
+ val deterministicPredicates = predicates.filter(_.deterministic)
Review Comment:
**Finding 9.** This is a wrong-results fix that stands on its own, and the
shape it fixes needs no `MATERIALIZED`. Any definition that survives
`InlineCTE` gets its references' predicates OR-merged and pushed into the
shared definition while the references keep them. That is idempotent for a
deterministic predicate. For `rand() < 0.5` it filters twice. A
non-deterministic definition referenced more than once already survives
`InlineCTE` on master.
Measured on this head with `predicates.filter(_.deterministic)` put back to
`predicates`, `t` being a 3-row temp view:
```sql
with v as (select c1, rand(1) r from t)
select c1 from v where rand(2) < 0.5
union all
select c1 from v where rand(3) < 0.5
```
With the filter: two `rand` filters in the optimized plan, one per
reference, and 3 rows. Without it: four filters, because `(rand(2) < 0.5) OR
(rand(3) < 0.5)` sits in the definition as well, and 2 rows.
The comment change above names the cause correctly. This rule has matched
`PhysicalOperation` since
[SPARK-39764](https://github.com/apache/spark/commit/b02316ccf3e5af545f6e5444761761ddd73fb931)
in 3.4.0, and `PhysicalOperation` hands back a single filter even when it is
non-deterministic. The assert at
`sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/planning/patterns.scala:117`
only binds when it collected more than one.
So the maintenance branches likely carry this. Please give it its own JIRA,
and pin it with a test that does not use the option, e.g. the query above.
`CTEInlineSuite`'s "non-deterministic predicates are not pushed into a
MATERIALIZED CTE" reaches the code only through `MATERIALIZED`, so a backport
of the two lines would arrive untested.
##########
docs/sql-ref-syntax-qry-select-cte.md:
##########
@@ -40,6 +40,19 @@ expression_name [ ( column_name [ , ... ] ) ] [ AS ] ( query
)
Specifies a name for the common table expression.
+* **MATERIALIZED**, **NOT MATERIALIZED**
+
+ Optionally specifies how the common table expression is evaluated.
`MATERIALIZED` forces it
+ to be evaluated once and shared by all references. `NOT MATERIALIZED`
forces it to be inlined,
+ so that each reference is planned and evaluated independently, and
non-deterministic
+ expressions such as `rand()` may yield different values per reference. A
`MATERIALIZED`
+ common table expression cannot reference columns of an outer query.
`MATERIALIZED` is not
+ supported in a statement whose common table expressions are always
inlined, such as a
+ multi-insert statement, nor in a subquery whose query, after the WITH
clause, references
Review Comment:
**Finding 10.** The check rejects on
`SubExprUtils.hasOuterReferences(withCTE)`, which covers the whole `WithCTE`,
definitions included. So the error also fires when the query after the WITH
clause references nothing outer and another CTE of the same clause carries the
correlation. Measured on this head:
```sql
select * from t o where exists (
with c as (select c1 from t2 where t2.c1 = o.c1),
v as materialized (select c1 from t2)
select * from v join c on v.c1 = c.c1
)
```
`UNSUPPORTED_FEATURE.MATERIALIZED_CTE_IN_CORRELATED_SUBQUERY` naming `v`.
The rejection is right here, since `c` is inlined into the body and the
`WithCTE` ends up on the correlated path. It is the sentence that is narrower
than the code.
```suggestion
multi-insert statement, nor in a subquery whose WITH clause or query
references
```
One shape the wording change does not cover: with `c` unreferenced (`select
* from v` as the body) the error still fires, though `InlineCTE` drops `c` and
the statement would run. A plain CTE in that shape returns rows on this head.
That matches how SPARK-45752 treats errors in unreferenced CTEs, so I am not
asking for a behaviour change.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/MaterializedCTECheck.scala:
##########
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.catalyst.analysis
+
+import scala.collection.mutable
+
+import org.apache.spark.sql.catalyst.expressions.{OuterReference,
OuterScopeReference, SubExprUtils, SubqueryExpression}
+import org.apache.spark.sql.catalyst.plans.logical.{CTERelationDef,
CTERelationRef, LogicalPlan, SubqueryAlias, WithCTE}
+import org.apache.spark.sql.catalyst.trees.TreePattern.CTE
+import org.apache.spark.sql.errors.QueryCompilationErrors
+
+/**
+ * Checks that a MATERIALIZED CTE does not reference the query enclosing it,
as it is evaluated
+ * once on its own. The CTEs it references, transitively, are checked with it,
since they are
+ * inlined into it, unless they are MATERIALIZED themselves and form their own
boundary. Each
+ * definition is scanned at its own operator level and never inside its
subquery plans: a
+ * correlation a definition keeps inside its own subquery targets that
definition, not the
+ * enclosing query. A MATERIALIZED CTE is also rejected in a correlated
subquery when the query
+ * of its WITH clause references the outer query, as a `WithCTE` that is not
inlined cannot be
+ * decorrelated. The check covers the given plan and all its subqueries.
+ */
+object MaterializedCTECheck extends (LogicalPlan => Unit) {
+ override def apply(plan: LogicalPlan): Unit = {
+ if (plan.containsPattern(CTE)) {
+ // All CTE definitions, including those of nested subqueries, so that
references from a
+ // MATERIALIZED CTE can be followed across subquery boundaries.
+ val cteDefs = mutable.LinkedHashMap.empty[Long, CTERelationDef]
+ plan.foreachWithSubqueries {
+ case cteDef: CTERelationDef => cteDefs(cteDef.id) = cteDef
+ case _ =>
+ }
+ cteDefs.values.filter(_.materialized.contains(true)).foreach { cteDef =>
+ (cteDef +: collectReferencedDefs(cteDef,
cteDefs)).foreach(checkDefinition)
+ }
+ // Decorrelation stops at a subtree without outer references, so only a
`WithCTE` whose
+ // own subtree is correlated is on its path. A correlation above the
WITH clause, e.g. on a
+ // derived table holding it, is fine.
+ plan.subqueriesAll.foreach(_.foreach {
Review Comment:
**Finding 11.** This pass runs for every plan that contains a CTE, whether
or not any definition is `MATERIALIZED`. `subqueriesAll` builds the list of
every subquery plan recursively, and each one is then walked in full.
`CheckAnalysis` runs the check on every analyzed plan, and single-pass runs it
once per subquery plan on top (`ResolutionCheckRunner.runWithSubqueries`), so
the ungated pass is paid several times over on a subquery-heavy query.
The traversal above already collected the answer, so both passes can hang
off it:
```scala
val materializedDefs =
cteDefs.values.filter(_.materialized.contains(true)).toSeq
if (materializedDefs.nonEmpty) {
materializedDefs.foreach { cteDef =>
(cteDef +: collectReferencedDefs(cteDef,
cteDefs)).foreach(checkDefinition)
}
// Decorrelation stops at a subtree without outer references, so
only a `WithCTE` whose
// ...
plan.subqueriesAll.foreach(...)
}
```
##########
sql/core/src/test/resources/sql-tests/inputs/cte-command.sql:
##########
@@ -29,5 +29,20 @@ INSERT INTO cte_tbl2 SELECT col;
SELECT * FROM cte_tbl;
SELECT * FROM cte_tbl2;
+-- MATERIALIZED CTE in a Multi-INSERT, should fail
+WITH s AS MATERIALIZED (SELECT 46 AS col)
+FROM s
+INSERT INTO cte_tbl SELECT col
+INSERT INTO cte_tbl2 SELECT col;
+
+-- NOT MATERIALIZED CTE in a Multi-INSERT
+WITH s AS NOT MATERIALIZED (SELECT 46 AS col)
+FROM s
+INSERT INTO cte_tbl SELECT col
+INSERT INTO cte_tbl2 SELECT col;
Review Comment:
**Finding 12.** This file pins both rejections, and nothing pins the command
path that does keep the definition. A future widening of the `alwaysInline`
condition in `CTESubstitution` would turn a working statement into
`MATERIALIZED_CTE_ALWAYS_INLINED` with every test still green.
It works today. Measured on this head with a counting UDF over a 3-row
source and two references, the body produced 3 rows, so the definition is
evaluated once inside the command:
```sql
insert into a
with v as materialized (select counted(c1) c1 from t)
select c1 from v union all select c1 from v
```
One more block here covers it:
```sql
-- MATERIALIZED CTE in a single-INSERT statement
INSERT INTO cte_tbl WITH s AS MATERIALIZED (SELECT 47 AS col) SELECT col
FROM s UNION ALL SELECT col FROM s;
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]