-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-37020][SQL] DS V2 LIMIT push down #34291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
37714db
16a617b
788f893
e6b419a
c3e5097
979e6d6
0c169c4
e82b846
c5967c4
5b7d16b
4a48624
499dff1
836746e
496878c
08f57cd
86c020d
008aadb
a36007e
e9978a8
7a3415e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /* | ||
| * 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.connector.read; | ||
|
|
||
| import org.apache.spark.annotation.Evolving; | ||
|
|
||
| /** | ||
| * A mix-in interface for {@link Scan}. Data sources can implement this interface to | ||
| * push down LIMIT. Please note that the combination of LIMIT with other operations | ||
| * such as AGGREGATE, GROUP BY, SORT BY, CLUSTER BY, DISTRIBUTE BY, etc. is NOT pushed down. | ||
|
cloud-fan marked this conversation as resolved.
|
||
| * | ||
| * @since 3.3.0 | ||
| */ | ||
| @Evolving | ||
| public interface SupportsPushDownLimit extends Scan { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all pushdown API should extends |
||
|
|
||
| /** | ||
| * Pushes down LIMIT to the data source. | ||
| */ | ||
| int pushLimit(int limit); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel this is a bit over-designed. The source should just tell Spark if the limit can be pushed or not, instead of returning the actual limit, as it's very rare that the pushed limit is different from the limit from Spark.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -19,11 +19,11 @@ package org.apache.spark.sql.execution.datasources.v2 | |||||
|
|
||||||
| import scala.collection.mutable | ||||||
|
|
||||||
| import org.apache.spark.sql.catalyst.expressions.{And, Attribute, AttributeReference, Expression, NamedExpression, PredicateHelper, ProjectionOverSchema, SubqueryExpression} | ||||||
| import org.apache.spark.sql.catalyst.expressions.{And, Attribute, AttributeReference, Expression, IntegerLiteral, NamedExpression, PredicateHelper, ProjectionOverSchema, SubqueryExpression} | ||||||
| import org.apache.spark.sql.catalyst.expressions.aggregate | ||||||
| import org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression | ||||||
| import org.apache.spark.sql.catalyst.planning.ScanOperation | ||||||
| import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Filter, LeafNode, LogicalPlan, Project} | ||||||
| import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Filter, GlobalLimit, LeafNode, LocalLimit, LogicalPlan, Project} | ||||||
| import org.apache.spark.sql.catalyst.rules.Rule | ||||||
| import org.apache.spark.sql.connector.expressions.aggregate.Aggregation | ||||||
| import org.apache.spark.sql.connector.read.{Scan, ScanBuilder, SupportsPushDownAggregates, SupportsPushDownFilters, V1Scan} | ||||||
|
|
@@ -36,7 +36,7 @@ object V2ScanRelationPushDown extends Rule[LogicalPlan] with PredicateHelper { | |||||
| import DataSourceV2Implicits._ | ||||||
|
|
||||||
| def apply(plan: LogicalPlan): LogicalPlan = { | ||||||
| applyColumnPruning(pushDownAggregates(pushDownFilters(createScanBuilder(plan)))) | ||||||
| applyLimit(applyColumnPruning(pushDownAggregates(pushDownFilters(createScanBuilder(plan))))) | ||||||
| } | ||||||
|
|
||||||
| private def createScanBuilder(plan: LogicalPlan) = plan.transform { | ||||||
|
|
@@ -225,6 +225,37 @@ object V2ScanRelationPushDown extends Rule[LogicalPlan] with PredicateHelper { | |||||
| withProjection | ||||||
| } | ||||||
|
|
||||||
| def applyLimit(plan: LogicalPlan): LogicalPlan = plan.transform { | ||||||
| case globalLimit @ GlobalLimit(_, l @ LocalLimit(IntegerLiteral(limitValue), child)) => | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we pushdown select * from jdbc_table t1 left join t2 on t1.id = t2.id limit 2;
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is MySQL benchmark result: bin/spark-shell --queue default --conf spark.sql.catalog.mysql=org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog --conf spark.sql.catalog.mysql.driver=com.mysql.jdbc.Driver --conf "spark.sql.catalog.mysql.url=jdbc:mysql://ipToMySQL:3306/mydb?user=userName&password=mypasswd" --conf spark.sql.catalog.mysql.pushDownLimit=true
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think local limit pushdown is not beneficial if the JDBC connection is using the streaming (row-by-row) mode?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems bin/spark-shell --queue default --conf spark.sql.catalog.mysql=org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog --conf spark.sql.catalog.mysql.driver=com.mysql.jdbc.Driver --conf "spark.sql.catalog.mysql.url=jdbc:mysql://ipToMySQL:3306/mydb?user=userName&password=mypasswd" --conf spark.sql.catalog.mysql.pushDownLimit=false --conf spark.sql.catalog.mysql.fetchsize=-2147483648 |
||||||
| child match { | ||||||
| case relation @ DataSourceV2ScanRelation(_, scan, _) => | ||||||
| val limit = PushDownUtils.pushLimit(scan, limitValue) | ||||||
| if (limit > 0) { | ||||||
| scan match { | ||||||
| case v1: V1ScanWrapper => | ||||||
| globalLimit.copy( | ||||||
| child = l.copy(child = relation.copy(scan = v1.copy(pushedLimit = Some(limit))))) | ||||||
| case _ => globalLimit | ||||||
| } | ||||||
| } else { | ||||||
| globalLimit | ||||||
| } | ||||||
| case project @ Project(_, relation @ DataSourceV2ScanRelation(_, scan, _)) => | ||||||
| val limit = PushDownUtils.pushLimit(scan, limitValue) | ||||||
| if (limit > 0) { | ||||||
| scan match { | ||||||
| case v1: V1ScanWrapper => | ||||||
| globalLimit.copy(child = l.copy(child = project.copy(child = relation.copy( | ||||||
| scan = v1.copy(pushedLimit = Some(limit)))))) | ||||||
| case _ => globalLimit | ||||||
| } | ||||||
| } else { | ||||||
| globalLimit | ||||||
| } | ||||||
| case _ => globalLimit | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private def getWrappedScan( | ||||||
| scan: Scan, | ||||||
| sHolder: ScanBuilderHolder, | ||||||
|
|
@@ -236,7 +267,7 @@ object V2ScanRelationPushDown extends Rule[LogicalPlan] with PredicateHelper { | |||||
| f.pushedFilters() | ||||||
| case _ => Array.empty[sources.Filter] | ||||||
| } | ||||||
| V1ScanWrapper(v1, pushedFilters, aggregation) | ||||||
| V1ScanWrapper(v1, pushedFilters, aggregation, None) | ||||||
| case _ => scan | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -252,6 +283,7 @@ case class ScanBuilderHolder( | |||||
| case class V1ScanWrapper( | ||||||
| v1Scan: V1Scan, | ||||||
| handledFilters: Seq[sources.Filter], | ||||||
| pushedAggregate: Option[Aggregation]) extends Scan { | ||||||
| pushedAggregate: Option[Aggregation], | ||||||
| pushedLimit: Option[Int]) extends Scan { | ||||||
| override def readSchema(): StructType = v1Scan.readSchema() | ||||||
| } | ||||||




Uh oh!
There was an error while loading. Please reload this page.