This is an automated email from the ASF dual-hosted git repository.
biyan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 193df7345 [spark] Integrate column statistics into spark scan (#2798)
193df7345 is described below
commit 193df7345aa520f8b45125cdd85588a91a3fc3a9
Author: Zouxxyy <[email protected]>
AuthorDate: Mon Jan 29 14:21:19 2024 +0800
[spark] Integrate column statistics into spark scan (#2798)
---
.../scala/org/apache/paimon/spark/PaimonScan.scala | 3 +-
.../spark/statistics/StatisticsHelper.scala} | 22 ++---
.../spark/statistics/StatisticsHelper.scala} | 22 ++---
.../spark/statistics/StatisticsHelper.scala} | 22 ++---
.../apache/paimon/spark/sql/AnalyzeTableTest.scala | 44 ++++++++-
.../apache/paimon/spark/sql/AnalyzeTableTest.scala | 43 ++++++++-
.../apache/paimon/spark/sql/AnalyzeTableTest.scala | 43 ++++++++-
.../org/apache/paimon/spark/PaimonBaseScan.scala | 21 ++++-
.../paimon/spark/PaimonBaseScanBuilder.scala | 14 ++-
.../org/apache/paimon/spark/PaimonImplicits.scala | 8 +-
.../scala/org/apache/paimon/spark/PaimonScan.scala | 3 +-
.../org/apache/paimon/spark/PaimonStatistics.scala | 78 +++++++++++++++-
.../StatisticsHelper.scala} | 22 ++---
.../spark/statistics/StatisticsHelperBase.scala | 104 +++++++++++++++++++++
.../paimon/spark/sql/AnalyzeTableTestBase.scala | 31 +++++-
15 files changed, 405 insertions(+), 75 deletions(-)
diff --git
a/paimon-spark/paimon-spark-3.1/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
b/paimon-spark/paimon-spark-3.1/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
index f9546c961..bc2c2bc8e 100644
---
a/paimon-spark/paimon-spark-3.1/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
+++
b/paimon-spark/paimon-spark-3.1/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
@@ -27,5 +27,6 @@ case class PaimonScan(
table: Table,
requiredSchema: StructType,
filters: Array[Predicate],
+ reservedFilters: Array[Filter],
pushDownLimit: Option[Int])
- extends PaimonBaseScan(table, requiredSchema, filters, pushDownLimit)
+ extends PaimonBaseScan(table, requiredSchema, filters, reservedFilters,
pushDownLimit)
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
b/paimon-spark/paimon-spark-3.1/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
similarity index 63%
copy from
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
copy to
paimon-spark/paimon-spark-3.1/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
index 208c5816c..66d1265b0 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
+++
b/paimon-spark/paimon-spark-3.1/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
@@ -15,21 +15,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.paimon.spark
+package org.apache.paimon.spark.statistics
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.catalyst.plans.logical
import org.apache.spark.sql.connector.read.Statistics
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
-import java.util.OptionalLong
-
-case class PaimonStatistics[T <: PaimonBaseScan](scan: T) extends Statistics {
-
- private lazy val rowCount: Long = scan.getSplits.map(_.rowCount).sum
-
- private lazy val scannedTotalSize: Long = rowCount *
scan.readSchema().defaultSize
-
- override def sizeInBytes(): OptionalLong = OptionalLong.of(scannedTotalSize)
-
- override def numRows(): OptionalLong = OptionalLong.of(rowCount)
-
- // TODO: extend columnStats for CBO
+trait StatisticsHelper extends StatisticsHelperBase {
+ protected def toV1Stats(v2Stats: Statistics, attrs: Seq[Attribute]):
logical.Statistics = {
+ DataSourceV2Relation.transformV2Stats(v2Stats, None,
conf.defaultSizeInBytes)
+ }
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
b/paimon-spark/paimon-spark-3.2/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
similarity index 63%
copy from
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
copy to
paimon-spark/paimon-spark-3.2/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
index 208c5816c..66d1265b0 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
+++
b/paimon-spark/paimon-spark-3.2/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
@@ -15,21 +15,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.paimon.spark
+package org.apache.paimon.spark.statistics
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.catalyst.plans.logical
import org.apache.spark.sql.connector.read.Statistics
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
-import java.util.OptionalLong
-
-case class PaimonStatistics[T <: PaimonBaseScan](scan: T) extends Statistics {
-
- private lazy val rowCount: Long = scan.getSplits.map(_.rowCount).sum
-
- private lazy val scannedTotalSize: Long = rowCount *
scan.readSchema().defaultSize
-
- override def sizeInBytes(): OptionalLong = OptionalLong.of(scannedTotalSize)
-
- override def numRows(): OptionalLong = OptionalLong.of(rowCount)
-
- // TODO: extend columnStats for CBO
+trait StatisticsHelper extends StatisticsHelperBase {
+ protected def toV1Stats(v2Stats: Statistics, attrs: Seq[Attribute]):
logical.Statistics = {
+ DataSourceV2Relation.transformV2Stats(v2Stats, None,
conf.defaultSizeInBytes)
+ }
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
b/paimon-spark/paimon-spark-3.3/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
similarity index 63%
copy from
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
copy to
paimon-spark/paimon-spark-3.3/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
index 208c5816c..66d1265b0 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
+++
b/paimon-spark/paimon-spark-3.3/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
@@ -15,21 +15,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.paimon.spark
+package org.apache.paimon.spark.statistics
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.catalyst.plans.logical
import org.apache.spark.sql.connector.read.Statistics
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
-import java.util.OptionalLong
-
-case class PaimonStatistics[T <: PaimonBaseScan](scan: T) extends Statistics {
-
- private lazy val rowCount: Long = scan.getSplits.map(_.rowCount).sum
-
- private lazy val scannedTotalSize: Long = rowCount *
scan.readSchema().defaultSize
-
- override def sizeInBytes(): OptionalLong = OptionalLong.of(scannedTotalSize)
-
- override def numRows(): OptionalLong = OptionalLong.of(rowCount)
-
- // TODO: extend columnStats for CBO
+trait StatisticsHelper extends StatisticsHelperBase {
+ protected def toV1Stats(v2Stats: Statistics, attrs: Seq[Attribute]):
logical.Statistics = {
+ DataSourceV2Relation.transformV2Stats(v2Stats, None,
conf.defaultSizeInBytes)
+ }
}
diff --git
a/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
index c6f788eab..ff65336a1 100644
---
a/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
+++
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
@@ -17,4 +17,46 @@
*/
package org.apache.paimon.spark.sql
-class AnalyzeTableTest extends AnalyzeTableTestBase {}
+import org.junit.jupiter.api.Assertions
+
+class AnalyzeTableTest extends AnalyzeTableTestBase {
+
+ test("Paimon analyze: spark use col stats") {
+ spark.sql(s"""
+ |CREATE TABLE T (id STRING, name STRING, i INT, l LONG)
+ |USING PAIMON
+ |TBLPROPERTIES ('primary-key'='id')
+ |""".stripMargin)
+
+ spark.sql(s"INSERT INTO T VALUES ('1', 'a', 1, 1)")
+ spark.sql(s"INSERT INTO T VALUES ('2', 'aaa', 1, 2)")
+ spark.sql(s"ANALYZE TABLE T COMPUTE STATISTICS FOR ALL COLUMNS")
+
+ val stats = getScanStatistic("SELECT * FROM T")
+ Assertions.assertEquals(2L, stats.rowCount.get.longValue())
+ // spark 33' v2 table not support col stats
+ Assertions.assertEquals(0, stats.attributeStats.size)
+ }
+
+ test("Paimon analyze: partition filter push down hit") {
+ spark.sql(s"""
+ |CREATE TABLE T (id INT, name STRING, pt INT)
+ |TBLPROPERTIES ('primary-key'='id, pt', 'bucket'='2')
+ |PARTITIONED BY (pt)
+ |""".stripMargin)
+
+ spark.sql("INSERT INTO T VALUES (1, 'a', 1), (2, 'b', 1), (3, 'c', 2), (4,
'd', 3)")
+ spark.sql(s"ANALYZE TABLE T COMPUTE STATISTICS FOR ALL COLUMNS")
+
+ // partition push down hit
+ var sql = "SELECT * FROM T WHERE pt < 1"
+ // spark 33' v2 table not support col stats
+ Assertions.assertEquals(4L, getScanStatistic(sql).rowCount.get.longValue())
+ checkAnswer(spark.sql(sql), Nil)
+
+ // partition push down not hit
+ sql = "SELECT * FROM T WHERE id < 1"
+ Assertions.assertEquals(4L, getScanStatistic(sql).rowCount.get.longValue())
+ checkAnswer(spark.sql(sql), Nil)
+ }
+}
diff --git
a/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
b/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
index c6f788eab..723b8082b 100644
---
a/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
+++
b/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
@@ -17,4 +17,45 @@
*/
package org.apache.paimon.spark.sql
-class AnalyzeTableTest extends AnalyzeTableTestBase {}
+import org.junit.jupiter.api.Assertions
+
+class AnalyzeTableTest extends AnalyzeTableTestBase {
+
+ test("Paimon analyze: spark use col stats") {
+ spark.sql(s"""
+ |CREATE TABLE T (id STRING, name STRING, i INT, l LONG)
+ |USING PAIMON
+ |TBLPROPERTIES ('primary-key'='id')
+ |""".stripMargin)
+
+ spark.sql(s"INSERT INTO T VALUES ('1', 'a', 1, 1)")
+ spark.sql(s"INSERT INTO T VALUES ('2', 'aaa', 1, 2)")
+ spark.sql(s"ANALYZE TABLE T COMPUTE STATISTICS FOR ALL COLUMNS")
+
+ val stats = getScanStatistic("SELECT * FROM T")
+ Assertions.assertEquals(2L, stats.rowCount.get.longValue())
+ Assertions.assertEquals(4, stats.attributeStats.size)
+ }
+
+ test("Paimon analyze: partition filter push down hit") {
+ spark.sql(s"""
+ |CREATE TABLE T (id INT, name STRING, pt INT)
+ |TBLPROPERTIES ('primary-key'='id, pt', 'bucket'='2')
+ |PARTITIONED BY (pt)
+ |""".stripMargin)
+
+ spark.sql("INSERT INTO T VALUES (1, 'a', 1), (2, 'b', 1), (3, 'c', 2), (4,
'd', 3)")
+ spark.sql(s"ANALYZE TABLE T COMPUTE STATISTICS FOR ALL COLUMNS")
+
+ // paimon will reserve partition filter and not return it to spark, we
need to ensure stats are filtered correctly.
+ // partition push down hit
+ var sql = "SELECT * FROM T WHERE pt < 1"
+ Assertions.assertEquals(0L, getScanStatistic(sql).rowCount.get.longValue())
+ checkAnswer(spark.sql(sql), Nil)
+
+ // partition push down not hit
+ sql = "SELECT * FROM T WHERE id < 1"
+ Assertions.assertEquals(4L, getScanStatistic(sql).rowCount.get.longValue())
+ checkAnswer(spark.sql(sql), Nil)
+ }
+}
diff --git
a/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
b/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
index c6f788eab..723b8082b 100644
---
a/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
+++
b/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTest.scala
@@ -17,4 +17,45 @@
*/
package org.apache.paimon.spark.sql
-class AnalyzeTableTest extends AnalyzeTableTestBase {}
+import org.junit.jupiter.api.Assertions
+
+class AnalyzeTableTest extends AnalyzeTableTestBase {
+
+ test("Paimon analyze: spark use col stats") {
+ spark.sql(s"""
+ |CREATE TABLE T (id STRING, name STRING, i INT, l LONG)
+ |USING PAIMON
+ |TBLPROPERTIES ('primary-key'='id')
+ |""".stripMargin)
+
+ spark.sql(s"INSERT INTO T VALUES ('1', 'a', 1, 1)")
+ spark.sql(s"INSERT INTO T VALUES ('2', 'aaa', 1, 2)")
+ spark.sql(s"ANALYZE TABLE T COMPUTE STATISTICS FOR ALL COLUMNS")
+
+ val stats = getScanStatistic("SELECT * FROM T")
+ Assertions.assertEquals(2L, stats.rowCount.get.longValue())
+ Assertions.assertEquals(4, stats.attributeStats.size)
+ }
+
+ test("Paimon analyze: partition filter push down hit") {
+ spark.sql(s"""
+ |CREATE TABLE T (id INT, name STRING, pt INT)
+ |TBLPROPERTIES ('primary-key'='id, pt', 'bucket'='2')
+ |PARTITIONED BY (pt)
+ |""".stripMargin)
+
+ spark.sql("INSERT INTO T VALUES (1, 'a', 1), (2, 'b', 1), (3, 'c', 2), (4,
'd', 3)")
+ spark.sql(s"ANALYZE TABLE T COMPUTE STATISTICS FOR ALL COLUMNS")
+
+ // paimon will reserve partition filter and not return it to spark, we
need to ensure stats are filtered correctly.
+ // partition push down hit
+ var sql = "SELECT * FROM T WHERE pt < 1"
+ Assertions.assertEquals(0L, getScanStatistic(sql).rowCount.get.longValue())
+ checkAnswer(spark.sql(sql), Nil)
+
+ // partition push down not hit
+ sql = "SELECT * FROM T WHERE id < 1"
+ Assertions.assertEquals(4L, getScanStatistic(sql).rowCount.get.longValue())
+ checkAnswer(spark.sql(sql), Nil)
+ }
+}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
index 5ede8883d..d234092a5 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScan.scala
@@ -17,11 +17,13 @@
*/
package org.apache.paimon.spark
-import org.apache.paimon.CoreOptions
+import org.apache.paimon.{stats, CoreOptions}
import org.apache.paimon.predicate.{Predicate, PredicateBuilder}
import org.apache.paimon.spark.sources.PaimonMicroBatchStream
+import org.apache.paimon.spark.statistics.StatisticsHelper
import org.apache.paimon.table.{DataTable, FileStoreTable, Table}
import org.apache.paimon.table.source.{ReadBuilder, Split}
+import org.apache.paimon.types.RowType
import org.apache.spark.sql.connector.metric.CustomMetric
import org.apache.spark.sql.connector.read.{Batch, Scan, Statistics,
SupportsReportStatistics}
@@ -29,18 +31,22 @@ import
org.apache.spark.sql.connector.read.streaming.MicroBatchStream
import org.apache.spark.sql.sources.Filter
import org.apache.spark.sql.types.StructType
+import java.util.Optional
+
import scala.collection.JavaConverters._
abstract class PaimonBaseScan(
table: Table,
requiredSchema: StructType,
filters: Array[Predicate],
+ reservedFilters: Array[Filter],
pushDownLimit: Option[Int])
extends Scan
with SupportsReportStatistics
- with ScanHelper {
+ with ScanHelper
+ with StatisticsHelper {
- private val tableRowType = table.rowType
+ val tableRowType: RowType = table.rowType
private lazy val tableSchema = SparkTypeUtils.fromPaimonRowType(tableRowType)
@@ -50,6 +56,8 @@ abstract class PaimonBaseScan(
override val coreOptions: CoreOptions = CoreOptions.fromMap(table.options())
+ lazy val statistics: Optional[stats.Statistics] = table.statistics()
+
lazy val readBuilder: ReadBuilder = {
val _readBuilder = table.newReadBuilder()
@@ -89,7 +97,12 @@ abstract class PaimonBaseScan(
}
override def estimateStatistics(): Statistics = {
- PaimonStatistics(this)
+ val stats = PaimonStatistics(this)
+ if (reservedFilters.nonEmpty) {
+ filterStatistics(stats, reservedFilters)
+ } else {
+ stats
+ }
}
override def supportedCustomMetrics: Array[CustomMetric] = {
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScanBuilder.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScanBuilder.scala
index e6ec87707..420f39637 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScanBuilder.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonBaseScanBuilder.scala
@@ -37,10 +37,12 @@ abstract class PaimonBaseScanBuilder(table: Table)
protected var pushed: Array[(Filter, Predicate)] = Array.empty
+ protected var reservedFilters: Array[Filter] = Array.empty
+
protected var pushDownLimit: Option[Int] = None
override def build(): Scan = {
- PaimonScan(table, requiredSchema, pushed.map(_._2), pushDownLimit)
+ PaimonScan(table, requiredSchema, pushed.map(_._2), reservedFilters,
pushDownLimit)
}
/**
@@ -51,6 +53,7 @@ abstract class PaimonBaseScanBuilder(table: Table)
override def pushFilters(filters: Array[Filter]): Array[Filter] = {
val pushable = mutable.ArrayBuffer.empty[(Filter, Predicate)]
val postScan = mutable.ArrayBuffer.empty[Filter]
+ val reserved = mutable.ArrayBuffer.empty[Filter]
val converter = new SparkFilterConverter(table.rowType)
val visitor = new PartitionPredicateVisitor(table.partitionKeys())
@@ -59,7 +62,11 @@ abstract class PaimonBaseScanBuilder(table: Table)
try {
val predicate = converter.convert(filter)
pushable.append((filter, predicate))
- if (!predicate.visit(visitor)) postScan.append(filter)
+ if (!predicate.visit(visitor)) {
+ postScan.append(filter)
+ } else {
+ reserved.append(filter)
+ }
} catch {
case e: UnsupportedOperationException =>
logWarning(e.getMessage)
@@ -70,6 +77,9 @@ abstract class PaimonBaseScanBuilder(table: Table)
if (pushable.nonEmpty) {
this.pushed = pushable.toArray
}
+ if (reserved.nonEmpty) {
+ this.reservedFilters = reserved.toArray
+ }
postScan.toArray
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonImplicits.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonImplicits.scala
index 8b7942596..2aa4431ca 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonImplicits.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonImplicits.scala
@@ -24,10 +24,16 @@ import scala.language.implicitConversions
object PaimonImplicits {
implicit def toScalaOption[T](o: Optional[T]): Option[T] = {
if (o.isPresent) {
- Option.apply(o.get())
+ Some(o.get)
} else {
None
}
}
+ implicit def toJavaOptional[T, U](o: Option[T]): Optional[U] = {
+ o match {
+ case Some(t) => Optional.ofNullable(t.asInstanceOf[U])
+ case _ => Optional.empty[U]
+ }
+ }
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
index 2bb3ff0c9..d4bda95ec 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonScan.scala
@@ -32,8 +32,9 @@ case class PaimonScan(
table: Table,
requiredSchema: StructType,
filters: Array[Predicate],
+ reservedFilters: Array[Filter],
pushDownLimit: Option[Int])
- extends PaimonBaseScan(table, requiredSchema, filters, pushDownLimit)
+ extends PaimonBaseScan(table, requiredSchema, filters, reservedFilters,
pushDownLimit)
with SupportsRuntimeFiltering {
override def filterAttributes(): Array[NamedReference] = {
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
index 208c5816c..e5129ae3c 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
@@ -17,9 +17,19 @@
*/
package org.apache.paimon.spark
+import org.apache.paimon.stats
+import org.apache.paimon.stats.ColStats
+import org.apache.paimon.types.DataType
+
+import org.apache.spark.sql.Utils
+import org.apache.spark.sql.catalyst.plans.logical.ColumnStat
+import org.apache.spark.sql.connector.expressions.NamedReference
import org.apache.spark.sql.connector.read.Statistics
+import org.apache.spark.sql.connector.read.colstats.ColumnStatistics
+
+import java.util.{Optional, OptionalLong}
-import java.util.OptionalLong
+import scala.collection.JavaConverters._
case class PaimonStatistics[T <: PaimonBaseScan](scan: T) extends Statistics {
@@ -27,9 +37,69 @@ case class PaimonStatistics[T <: PaimonBaseScan](scan: T)
extends Statistics {
private lazy val scannedTotalSize: Long = rowCount *
scan.readSchema().defaultSize
- override def sizeInBytes(): OptionalLong = OptionalLong.of(scannedTotalSize)
+ private lazy val paimonStats: Optional[stats.Statistics] = scan.statistics
+
+ override def sizeInBytes(): OptionalLong = if (paimonStats.isPresent)
+ paimonStats.get().mergedRecordSize()
+ else OptionalLong.of(scannedTotalSize)
+
+ override def numRows(): OptionalLong =
+ if (paimonStats.isPresent) paimonStats.get().mergedRecordCount() else
OptionalLong.of(rowCount)
+
+ override def columnStats(): java.util.Map[NamedReference, ColumnStatistics]
= {
+ val requiredFields = scan.readSchema().fieldNames.toList.asJava
+ val resultMap = new java.util.HashMap[NamedReference, ColumnStatistics]()
+ if (paimonStats.isPresent) {
+ val paimonColStats = paimonStats.get().colStats()
+ scan.tableRowType.getFields
+ .stream()
+ .filter(
+ field => requiredFields.contains(field.name) &&
paimonColStats.containsKey(field.name()))
+ .forEach(
+ f =>
+ resultMap.put(
+ Utils.fieldReference(f.name()),
+ PaimonColumnStats(f.`type`(), paimonColStats.get(f.name()))))
+ }
+ resultMap
+ }
+}
+
+case class PaimonColumnStats(
+ override val nullCount: OptionalLong,
+ override val min: Optional[Object],
+ override val max: Optional[Object],
+ override val distinctCount: OptionalLong,
+ override val avgLen: OptionalLong,
+ override val maxLen: OptionalLong)
+ extends ColumnStatistics
- override def numRows(): OptionalLong = OptionalLong.of(rowCount)
+object PaimonColumnStats {
+ def apply(dateType: DataType, paimonColStats: ColStats[_]):
PaimonColumnStats = {
+ PaimonColumnStats(
+ paimonColStats.nullCount,
+
Optional.ofNullable(SparkInternalRow.fromPaimon(paimonColStats.min().orElse(null),
dateType)),
+
Optional.ofNullable(SparkInternalRow.fromPaimon(paimonColStats.max().orElse(null),
dateType)),
+ paimonColStats.distinctCount,
+ paimonColStats.avgLen,
+ paimonColStats.maxLen
+ )
+ }
- // TODO: extend columnStats for CBO
+ def apply(v1ColStats: ColumnStat): PaimonColumnStats = {
+ import PaimonImplicits._
+ PaimonColumnStats(
+ if (v1ColStats.nullCount.isDefined)
OptionalLong.of(v1ColStats.nullCount.get.longValue())
+ else OptionalLong.empty(),
+ v1ColStats.min,
+ v1ColStats.max,
+ if (v1ColStats.distinctCount.isDefined)
+ OptionalLong.of(v1ColStats.distinctCount.get.longValue())
+ else OptionalLong.empty(),
+ if (v1ColStats.avgLen.isDefined)
OptionalLong.of(v1ColStats.avgLen.get.longValue())
+ else OptionalLong.empty(),
+ if (v1ColStats.maxLen.isDefined)
OptionalLong.of(v1ColStats.maxLen.get.longValue())
+ else OptionalLong.empty()
+ )
+ }
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
similarity index 63%
copy from
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
copy to
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
index 208c5816c..e0ff25077 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonStatistics.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelper.scala
@@ -15,21 +15,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.paimon.spark
+package org.apache.paimon.spark.statistics
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.catalyst.plans.logical
import org.apache.spark.sql.connector.read.Statistics
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
-import java.util.OptionalLong
-
-case class PaimonStatistics[T <: PaimonBaseScan](scan: T) extends Statistics {
-
- private lazy val rowCount: Long = scan.getSplits.map(_.rowCount).sum
-
- private lazy val scannedTotalSize: Long = rowCount *
scan.readSchema().defaultSize
-
- override def sizeInBytes(): OptionalLong = OptionalLong.of(scannedTotalSize)
-
- override def numRows(): OptionalLong = OptionalLong.of(rowCount)
-
- // TODO: extend columnStats for CBO
+trait StatisticsHelper extends StatisticsHelperBase {
+ protected def toV1Stats(v2Stats: Statistics, attrs: Seq[Attribute]):
logical.Statistics = {
+ DataSourceV2Relation.transformV2Stats(v2Stats, None,
conf.defaultSizeInBytes, attrs)
+ }
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelperBase.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelperBase.scala
new file mode 100644
index 000000000..26d442c64
--- /dev/null
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/statistics/StatisticsHelperBase.scala
@@ -0,0 +1,104 @@
+/*
+ * 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.paimon.spark.statistics
+
+import org.apache.paimon.spark.PaimonColumnStats
+
+import org.apache.spark.sql.Utils
+import org.apache.spark.sql.catalyst.{SQLConfHelper, StructFilters}
+import org.apache.spark.sql.catalyst.expressions.{Attribute,
AttributeReference, BoundReference, Expression}
+import org.apache.spark.sql.catalyst.plans.logical
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
+import
org.apache.spark.sql.catalyst.plans.logical.statsEstimation.FilterEstimation
+import org.apache.spark.sql.connector.expressions.NamedReference
+import org.apache.spark.sql.connector.read.Statistics
+import org.apache.spark.sql.connector.read.colstats.ColumnStatistics
+import org.apache.spark.sql.sources.{And, Filter}
+import org.apache.spark.sql.types.{StructField, StructType}
+
+import java.util.OptionalLong
+
+trait StatisticsHelperBase extends SQLConfHelper {
+
+ val requiredSchema: StructType
+
+ def filterStatistics(v2Stats: Statistics, filters: Array[Filter]):
Statistics = {
+ val attrs: Seq[AttributeReference] =
+ requiredSchema.map(f => AttributeReference(f.name, f.dataType,
f.nullable, f.metadata)())
+ val condition = filterToCondition(filters, attrs)
+
+ if (condition.isDefined && v2Stats.numRows().isPresent) {
+ val filteredStats = FilterEstimation(
+ logical.Filter(condition.get, FakePlanWithStats(toV1Stats(v2Stats,
attrs)))).estimate.get
+ toV2Stats(filteredStats)
+ } else {
+ v2Stats
+ }
+ }
+
+ private def filterToCondition(
+ filters: Array[Filter],
+ attrs: Seq[Attribute]): Option[Expression] = {
+ StructFilters.filterToExpression(filters.reduce(And), toRef).map {
+ expression =>
+ expression.transform {
+ case ref: BoundReference => attrs.find(_.name ==
requiredSchema(ref.ordinal).name).get
+ }
+ }
+ }
+
+ private def toRef(attr: String): Option[BoundReference] = {
+ val index = requiredSchema.fieldIndex(attr)
+ val field = requiredSchema(index)
+ Option.apply(BoundReference(index, field.dataType, field.nullable))
+ }
+
+ protected def toV1Stats(v2Stats: Statistics, attrs: Seq[Attribute]):
logical.Statistics
+
+ private def toV2Stats(v1Stats: logical.Statistics): Statistics = {
+ new Statistics() {
+ override def sizeInBytes(): OptionalLong = if (v1Stats.sizeInBytes !=
null)
+ OptionalLong.of(v1Stats.sizeInBytes.longValue())
+ else OptionalLong.empty()
+
+ override def numRows(): OptionalLong = if (v1Stats.rowCount.isDefined)
+ OptionalLong.of(v1Stats.rowCount.get.longValue())
+ else OptionalLong.empty()
+
+ override def columnStats(): java.util.Map[NamedReference,
ColumnStatistics] = {
+ val columnStatsMap = new java.util.HashMap[NamedReference,
ColumnStatistics]()
+ v1Stats.attributeStats.foreach {
+ case (attr, v1ColStats) =>
+ columnStatsMap.put(
+ Utils.fieldReference(attr.name),
+ PaimonColumnStats(v1ColStats)
+ )
+ }
+ columnStatsMap
+ }
+ }
+ }
+}
+
+case class FakePlanWithStats(v1Stats: logical.Statistics) extends LogicalPlan {
+ override def output: Seq[Attribute] = Seq.empty
+ override def children: Seq[LogicalPlan] = Seq.empty
+ override protected def withNewChildrenInternal(
+ newChildren: IndexedSeq[LogicalPlan]): LogicalPlan = throw new
UnsupportedOperationException
+ override def stats: logical.Statistics = v1Stats
+}
diff --git
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTestBase.scala
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTestBase.scala
index d8f121f6e..5656fa60e 100644
---
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTestBase.scala
+++
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/AnalyzeTableTestBase.scala
@@ -24,11 +24,11 @@ import org.apache.paimon.stats.ColStats
import org.apache.paimon.utils.DateTimeUtils
import org.apache.spark.sql.Row
+import org.apache.spark.sql.catalyst.plans.logical.Statistics
+import org.apache.spark.sql.execution.datasources.v2.DataSourceV2ScanRelation
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Assertions
-import java.util.concurrent.TimeUnit
-
abstract class AnalyzeTableTestBase extends PaimonSparkTestBase {
test("Paimon analyze: analyze table only") {
@@ -283,7 +283,32 @@ abstract class AnalyzeTableTestBase extends
PaimonSparkTestBase {
Assertions.assertEquals(1, statsFileCount(tableLocation, fileIO))
}
- private def statsFileCount(tableLocation: Path, fileIO: FileIO): Int = {
+ test("Paimon analyze: spark use table stats") {
+ spark.sql(s"""
+ |CREATE TABLE T (id STRING, name STRING, i INT, l LONG)
+ |USING PAIMON
+ |TBLPROPERTIES ('primary-key'='id')
+ |""".stripMargin)
+
+ spark.sql(s"INSERT INTO T VALUES ('1', 'a', 1, 1)")
+ spark.sql(s"INSERT INTO T VALUES ('2', 'aaa', 1, 2)")
+ spark.sql(s"ANALYZE TABLE T COMPUTE STATISTICS")
+
+ val stats = getScanStatistic("SELECT * FROM T")
+ Assertions.assertEquals(2L, stats.rowCount.get.longValue())
+ }
+
+ protected def statsFileCount(tableLocation: Path, fileIO: FileIO): Int = {
fileIO.listStatus(new Path(tableLocation, "statistics")).length
}
+
+ protected def getScanStatistic(sql: String): Statistics = {
+ val relation = spark
+ .sql(sql)
+ .queryExecution
+ .optimizedPlan
+ .collectFirst { case relation: DataSourceV2ScanRelation => relation }
+ .get
+ relation.computeStats()
+ }
}