Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/sql-data-sources-jdbc.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ logging into the data sources.
<td>read</td>
</tr>

<tr>
<td><code>pushDownLimit</code></td>
<td><code>false</code></td>
<td>
The option to enable or disable LIMIT push-down into the JDBC data source. The default value is false, in which case Spark does not push down LIMIT to the JDBC data source. Otherwise, if value sets to true, LIMIT is pushed down to the JDBC data source. SPARK still applies LIMIT on the result from data source even if LIMIT is pushed down.
</td>
<td>read</td>
</tr>

<tr>
<td><code>keytab</code></td>
<td>(none)</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* An interface for building the {@link Scan}. Implementations can mixin SupportsPushDownXYZ
* interfaces to do operator pushdown, and keep the operator pushdown result in the returned
* {@link Scan}. When pushing down operators, Spark pushes down filters first, then pushes down
* aggregates or applies column pruning.
* aggregates or applies column pruning, lastly pushes down limit.
*
* @since 3.0.0
*/
Expand Down
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
Comment thread
huaxingao marked this conversation as resolved.
* 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.
Comment thread
cloud-fan marked this conversation as resolved.
*
* @since 3.3.0
*/
@Evolving
public interface SupportsPushDownLimit extends Scan {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all pushdown API should extends ScanBuilder?


/**
* Pushes down LIMIT to the data source.
*/
int pushLimit(int limit);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

boolean pushLimit(int limit); should be good enough.

}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ case class RowDataSourceScanExec(
filters: Set[Filter],
handledFilters: Set[Filter],
aggregation: Option[Aggregation],
limit: Option[Int],
rdd: RDD[InternalRow],
@transient relation: BaseRelation,
tableIdentifier: Option[TableIdentifier])
Expand Down Expand Up @@ -153,7 +154,8 @@ case class RowDataSourceScanExec(
"ReadSchema" -> requiredSchema.catalogString,
"PushedFilters" -> seqToString(markedFilters.toSeq),
"PushedAggregates" -> aggString,
"PushedGroupby" -> groupByString)
"PushedGroupby" -> groupByString) ++
limit.map(value => "PushedLimit" -> s"LIMIT $value")
}

// Don't care about `rdd` and `tableIdentifier` when canonicalizing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ object DataSourceStrategy
Set.empty,
Set.empty,
None,
None,
toCatalystRDD(l, baseRelation.buildScan()),
baseRelation,
None) :: Nil
Expand Down Expand Up @@ -410,6 +411,7 @@ object DataSourceStrategy
pushedFilters.toSet,
handledFilters,
None,
None,
scanBuilder(requestedColumns, candidatePredicates, pushedFilters),
relation.relation,
relation.catalogTable.map(_.identifier))
Expand All @@ -433,6 +435,7 @@ object DataSourceStrategy
pushedFilters.toSet,
handledFilters,
None,
None,
scanBuilder(requestedColumns, candidatePredicates, pushedFilters),
relation.relation,
relation.catalogTable.map(_.identifier))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ class JDBCOptions(
// An option to allow/disallow pushing down aggregate into JDBC data source
val pushDownAggregate = parameters.getOrElse(JDBC_PUSHDOWN_AGGREGATE, "false").toBoolean

// An option to allow/disallow pushing down LIMIT into JDBC data source
val pushDownLimit = parameters.getOrElse(JDBC_PUSHDOWN_LIMIT, "false").toBoolean

// The local path of user's keytab file, which is assumed to be pre-uploaded to all nodes either
// by --files option of spark-submit or manually
val keytab = {
Expand Down Expand Up @@ -266,6 +269,7 @@ object JDBCOptions {
val JDBC_SESSION_INIT_STATEMENT = newOption("sessionInitStatement")
val JDBC_PUSHDOWN_PREDICATE = newOption("pushDownPredicate")
val JDBC_PUSHDOWN_AGGREGATE = newOption("pushDownAggregate")
val JDBC_PUSHDOWN_LIMIT = newOption("pushDownLimit")
val JDBC_KEYTAB = newOption("keytab")
val JDBC_PRINCIPAL = newOption("principal")
val JDBC_TABLE_COMMENT = newOption("tableComment")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ object JDBCRDD extends Logging {
* @param options - JDBC options that contains url, table and other information.
* @param outputSchema - The schema of the columns or aggregate columns to SELECT.
* @param groupByColumns - The pushed down group by columns.
* @param limit - The pushed down limit. If the value is 0, it means no limit or limit
* is not pushed down.
*
* @return An RDD representing "SELECT requiredColumns FROM fqTable".
*/
Expand All @@ -190,7 +192,8 @@ object JDBCRDD extends Logging {
parts: Array[Partition],
options: JDBCOptions,
outputSchema: Option[StructType] = None,
groupByColumns: Option[Array[String]] = None): RDD[InternalRow] = {
groupByColumns: Option[Array[String]] = None,
limit: Int = 0): RDD[InternalRow] = {
val url = options.url
val dialect = JdbcDialects.get(url)
val quotedColumns = if (groupByColumns.isEmpty) {
Expand All @@ -208,7 +211,8 @@ object JDBCRDD extends Logging {
parts,
url,
options,
groupByColumns)
groupByColumns,
limit)
}
}

Expand All @@ -226,7 +230,8 @@ private[jdbc] class JDBCRDD(
partitions: Array[Partition],
url: String,
options: JDBCOptions,
groupByColumns: Option[Array[String]])
groupByColumns: Option[Array[String]],
limit: Int)
extends RDD[InternalRow](sc, Nil) {

/**
Expand Down Expand Up @@ -349,8 +354,10 @@ private[jdbc] class JDBCRDD(

val myWhereClause = getWhereClause(part)

Comment thread
cloud-fan marked this conversation as resolved.
val myLimitClause: String = dialect.getLimitClause(limit)
Comment thread
huaxingao marked this conversation as resolved.

val sqlText = s"SELECT $columnList FROM ${options.tableOrQuery} $myWhereClause" +
s" $getGroupByClause"
s" $getGroupByClause $myLimitClause"
stmt = conn.prepareStatement(sqlText,
ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)
stmt.setFetchSize(options.fetchSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,8 @@ private[sql] case class JDBCRelation(
requiredColumns: Array[String],
finalSchema: StructType,
filters: Array[Filter],
groupByColumns: Option[Array[String]]): RDD[Row] = {
groupByColumns: Option[Array[String]],
limit: Int): RDD[Row] = {
// Rely on a type erasure hack to pass RDD[InternalRow] back as RDD[Row]
JDBCRDD.scanTable(
sparkSession.sparkContext,
Expand All @@ -308,7 +309,8 @@ private[sql] case class JDBCRelation(
parts,
jdbcOptions,
Some(finalSchema),
groupByColumns).asInstanceOf[RDD[Row]]
groupByColumns,
limit).asInstanceOf[RDD[Row]]
}

override def insert(data: DataFrame, overwrite: Boolean): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,22 @@ class DataSourceV2Strategy(session: SparkSession) extends Strategy with Predicat

override def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {
case PhysicalOperation(project, filters,
DataSourceV2ScanRelation(_, V1ScanWrapper(scan, pushed, aggregate), output)) =>
DataSourceV2ScanRelation(_, V1ScanWrapper(scan, pushed, aggregate, limit), output)) =>
val v1Relation = scan.toV1TableScan[BaseRelation with TableScan](session.sqlContext)
if (v1Relation.schema != scan.readSchema()) {
throw QueryExecutionErrors.fallbackV1RelationReportsInconsistentSchemaError(
scan.readSchema(), v1Relation.schema)
}
val rdd = v1Relation.buildScan()
val unsafeRowRDD = DataSourceStrategy.toCatalystRDD(v1Relation, output, rdd)

val dsScan = RowDataSourceScanExec(
output,
output.toStructType,
Set.empty,
pushed.toSet,
aggregate,
limit,
unsafeRowRDD,
v1Relation,
tableIdentifier = None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import org.apache.spark.sql.catalyst.util.CharVarcharUtils
import org.apache.spark.sql.connector.expressions.FieldReference
import org.apache.spark.sql.connector.expressions.aggregate.Aggregation
import org.apache.spark.sql.connector.expressions.filter.{Filter => V2Filter}
import org.apache.spark.sql.connector.read.{Scan, ScanBuilder, SupportsPushDownAggregates, SupportsPushDownFilters, SupportsPushDownRequiredColumns, SupportsPushDownV2Filters}
import org.apache.spark.sql.connector.read.{Scan, ScanBuilder, SupportsPushDownAggregates, SupportsPushDownFilters, SupportsPushDownLimit, SupportsPushDownRequiredColumns, SupportsPushDownV2Filters}
import org.apache.spark.sql.execution.datasources.{DataSourceStrategy, PushableColumnWithoutNestedColumn}
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.sources
Expand Down Expand Up @@ -138,6 +138,23 @@ object PushDownUtils extends PredicateHelper {
}
}

/**
* Pushes down LIMIT to the data source Scan
*/
def pushLimit(scan: Scan, limit: Int): Int = {
scan match {
case s: SupportsPushDownLimit =>
s.pushLimit(limit)
case v1: V1ScanWrapper =>
v1.v1Scan match {
case s: SupportsPushDownLimit =>
s.pushLimit(limit)
case _ => 0
}
case _ => 0
}
}

/**
* Applies column pruning to the data source, w.r.t. the references of the given expressions.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we pushdown LocalLimit? pushdown LocalLimit can benefit this query:

select * from jdbc_table t1 left join t2 on t1.id = t2.id limit 2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
without pushdown with pushdown
image image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemsfetchsize=-2147483648 means enable streaming mode: https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

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

This is benchmark result:
image
close() took lot of time.
image

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,
Expand All @@ -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
}
}
Expand All @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ package org.apache.spark.sql.execution.datasources.v2.jdbc

import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.sql.connector.read.V1Scan
import org.apache.spark.sql.connector.read.{SupportsPushDownLimit, V1Scan}
import org.apache.spark.sql.execution.datasources.jdbc.JDBCRelation
import org.apache.spark.sql.jdbc.JdbcDialects
import org.apache.spark.sql.sources.{BaseRelation, Filter, TableScan}
import org.apache.spark.sql.types.StructType

Expand All @@ -28,10 +29,20 @@ case class JDBCScan(
prunedSchema: StructType,
pushedFilters: Array[Filter],
pushedAggregateColumn: Array[String] = Array(),
groupByColumns: Option[Array[String]]) extends V1Scan {
groupByColumns: Option[Array[String]]) extends V1Scan with SupportsPushDownLimit {

override def readSchema(): StructType = prunedSchema

private var pushedLimit = 0

override def pushLimit(limit: Int): Int = {
if (relation.jdbcOptions.pushDownLimit &&
JdbcDialects.get(relation.jdbcOptions.url).supportsLimit) {
pushedLimit = limit
}
pushedLimit
}

override def toV1TableScan[T <: BaseRelation with TableScan](context: SQLContext): T = {
new BaseRelation with TableScan {
override def sqlContext: SQLContext = context
Expand All @@ -43,7 +54,7 @@ case class JDBCScan(
} else {
pushedAggregateColumn
}
relation.buildScan(columnList, prunedSchema, pushedFilters, groupByColumns)
relation.buildScan(columnList, prunedSchema, pushedFilters, groupByColumns, pushedLimit)
}
}.asInstanceOf[T]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ private object DerbyDialect extends JdbcDialect {
override def getTableCommentQuery(table: String, comment: String): String = {
throw QueryExecutionErrors.commentOnTableUnsupportedError()
}

// ToDo: use fetch first n rows only for limit, e.g.
// select * from employee fetch first 10 rows only;
override def supportsLimit(): Boolean = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,18 @@ abstract class JdbcDialect extends Serializable with Logging{
def classifyException(message: String, e: Throwable): AnalysisException = {
new AnalysisException(message, cause = Some(e))
}

/**
* returns the LIMIT clause for the SELECT statement
*/
def getLimitClause(limit: Integer): String = {
if (limit > 0 ) s"LIMIT $limit" else ""
}

/**
* returns whether the dialect supports limit or not
*/
def supportsLimit(): Boolean = true
}

/**
Expand Down
Loading