diff --git a/docs/docs/primary-key-table/data-distribution.md b/docs/docs/primary-key-table/data-distribution.md
index 49846b2ea3b8..a7cf7aa16dc0 100644
--- a/docs/docs/primary-key-table/data-distribution.md
+++ b/docs/docs/primary-key-table/data-distribution.md
@@ -70,12 +70,27 @@ Postpone bucket mode is configured by `'bucket' = '-2'`.
This mode aims to solve the difficulty to determine a fixed number of buckets
and support different buckets for different partitions.
-When writing records into the table,
-all records will first be stored in the `bucket-postpone` directory of each partition
+By default, batch writes set `postpone.batch-write-fixed-bucket` to `true`
+and write records directly to real buckets.
+For a Spark batch write to a partition without real bucket files,
+Spark calculates the bucket number from the uncompressed Paimon `BinaryRow` serialized size.
+The target size is configured by `postpone.target-size-per-bucket` and defaults to `1 GB`.
+Existing postpone files do not record their uncompressed serialized size, so Spark estimates their
+size using their row count and the average serialized size of incoming rows in the same partition.
+You can instead configure `postpone.target-row-num-per-bucket` to calculate the bucket number
+from row counts; this option takes precedence over the target size.
+The calculated bucket number is at least `1` and is limited by
+`postpone.batch-write-fixed-bucket.max-parallelism`.
+The serialized size is measured before file encoding and compression, so it is not the exact
+ORC or Parquet size on storage.
+Inferring the data amount adds a statistics stage; Spark caches the batch so that inference and
+writing use the same input rows. Spark skips this stage for an unpartitioned table that already
+has real bucket files, or when `postpone.batch-write-fixed-bucket.max-parallelism` is `1`.
+
+When `postpone.batch-write-fixed-bucket` is `false`,
+records are first stored in the `bucket-postpone` directory of each partition
and are not available to readers.
-
-To move the records into the correct bucket and make them readable,
-you need to run a compaction job.
+To move these records into the correct bucket and make them readable, run a compaction job.
See `compact` [procedure](../flink/procedures).
The bucket number for the partitions compacted for the first time
is configured by the option `postpone.default-bucket-num`, whose default value is `1`.
diff --git a/docs/generated/core_configuration.html b/docs/generated/core_configuration.html
index 4d1d8f87f7b1..7bc238d4ff23 100644
--- a/docs/generated/core_configuration.html
+++ b/docs/generated/core_configuration.html
@@ -1294,7 +1294,13 @@
postpone.target-row-num-per-bucket |
(none) |
Long |
- Target row number per bucket for partitions compacted from postpone bucket files for the first time. |
+ Target row number per bucket when batch writing fixed buckets or compacting postpone bucket files for a partition without real bucket data. |
+
+
+ postpone.target-size-per-bucket |
+ 1 gb |
+ MemorySize |
+ Target uncompressed serialized data size per bucket when Spark batch writes fixed buckets for a partition without real bucket data. This option is ignored when 'postpone.target-row-num-per-bucket' is configured. |
primary-key |
diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
index 3040a981a4fc..ec95ab6ec537 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -2733,7 +2733,15 @@ public String toString() {
.longType()
.noDefaultValue()
.withDescription(
- "Target row number per bucket for partitions compacted from postpone bucket files for the first time.");
+ "Target row number per bucket when batch writing fixed buckets or compacting postpone bucket files for a partition without real bucket data.");
+
+ public static final ConfigOption POSTPONE_TARGET_SIZE_PER_BUCKET =
+ key("postpone.target-size-per-bucket")
+ .memoryType()
+ .defaultValue(MemorySize.parse("1 gb"))
+ .withDescription(
+ "Target uncompressed serialized data size per bucket when Spark batch writes fixed buckets for a partition without real bucket data. "
+ + "This option is ignored when 'postpone.target-row-num-per-bucket' is configured.");
public static final ConfigOption GLOBAL_INDEX_ROW_COUNT_PER_SHARD =
key("global-index.row-count-per-shard")
@@ -4406,6 +4414,10 @@ public Optional postponeTargetRowNumPerBucket() {
return options.getOptional(POSTPONE_TARGET_ROW_NUM_PER_BUCKET);
}
+ public long postponeTargetSizePerBucket() {
+ return options.get(POSTPONE_TARGET_SIZE_PER_BUCKET).getBytes();
+ }
+
public long globalIndexRowCountPerShard() {
return options.get(GLOBAL_INDEX_ROW_COUNT_PER_SHARD);
}
diff --git a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala
index 1518a77934f6..aaba0f1ca0bf 100644
--- a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala
+++ b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala
@@ -52,6 +52,7 @@ import java.io.IOException
import java.util.Collections.singletonMap
import scala.collection.JavaConverters._
+import scala.collection.mutable
case class PaimonSparkWriter(
table: FileStoreTable,
@@ -59,6 +60,8 @@ case class PaimonSparkWriter(
batchId: Option[Long] = None)
extends WriteHelper {
+ import PaimonSparkWriter._
+
private lazy val tableSchema = table.schema
private lazy val bucketMode = table.bucketMode
@@ -106,6 +109,12 @@ case class PaimonSparkWriter(
}
def write(data: DataFrame): Seq[CommitMessage] = {
+ write(data, overwriteExistingData = false)
+ }
+
+ private[commands] def write(
+ data: DataFrame,
+ overwriteExistingData: Boolean): Seq[CommitMessage] = {
val sparkSession = data.sparkSession
val uriReaderFactory = uriReaderFactoryForBlobDescriptor
import sparkSession.implicits._
@@ -118,20 +127,19 @@ case class PaimonSparkWriter(
.withColumn(BUCKET_COL, lit(-1))
case _ => data.withColumn(BUCKET_COL, lit(-1))
}
- val rowKindColIdx = SparkRowUtils.getFieldIndex(withInitBucketCol.schema, ROW_KIND_COL)
- val bucketColIdx = SparkRowUtils.getFieldIndex(withInitBucketCol.schema, BUCKET_COL)
- val encoderGroupWithBucketCol = EncoderSerDeGroup(withInitBucketCol.schema)
- val postponePartitionBucketComputer: Option[BinaryRow => Integer] =
+ val postponeBucketAssignment =
if (postponeBatchWriteFixedBucket) {
- val knownNumBuckets = PostponeUtils.getKnownNumBuckets(table)
- val defaultPostponeNumBuckets = Math.min(
- withInitBucketCol.rdd.getNumPartitions,
- table.coreOptions().postponeBatchWriteFixedBucketMaxParallelism)
- Some((p: BinaryRow) => knownNumBuckets.getOrDefault(p, defaultPostponeNumBuckets))
+ Some(preparePostponeBucketAssignment(withInitBucketCol, overwriteExistingData))
} else {
None
}
+ val rowKindColIdx = SparkRowUtils.getFieldIndex(withInitBucketCol.schema, ROW_KIND_COL)
+ val bucketColIdx = SparkRowUtils.getFieldIndex(withInitBucketCol.schema, BUCKET_COL)
+ val encoderGroupWithBucketCol = EncoderSerDeGroup(withInitBucketCol.schema)
+ val postponePartitionBucketComputer =
+ postponeBucketAssignment.map(_.partitionBucketComputer)
+
def newWrite() = PaimonDataWrite(
writeBuilder,
writeType,
@@ -349,7 +357,13 @@ case class PaimonSparkWriter(
throw new UnsupportedOperationException(s"Spark doesn't support $bucketMode mode.")
}
- WriteTaskResult.merge(written.collect())
+ try {
+ WriteTaskResult.merge(written.collect())
+ } finally {
+ if (postponeBucketAssignment.exists(_.dataPersisted)) {
+ withInitBucketCol.unpersist()
+ }
+ }
}
/**
@@ -536,6 +550,136 @@ case class PaimonSparkWriter(
.toSeq
}
+ private def preparePostponeBucketAssignment(
+ df: DataFrame,
+ overwriteExistingData: Boolean): PostponeBucketAssignment = {
+ val knownNumBuckets = PostponeUtils.getKnownNumBuckets(table)
+ val maxNumBuckets = coreOptions.postponeBatchWriteFixedBucketMaxParallelism()
+ val unpartitionedTableHasKnownNumBuckets =
+ tableSchema.partitionKeys().isEmpty &&
+ knownNumBuckets.containsKey(BinaryRow.EMPTY_ROW)
+ val inferBucketNumFromData =
+ maxNumBuckets != 1 && !unpartitionedTableHasKnownNumBuckets
+ if (inferBucketNumFromData) {
+ df.persist()
+ }
+
+ try {
+ val defaultNumBuckets = Math.min(df.rdd.getNumPartitions, maxNumBuckets)
+ val inferredNumBuckets: Map[BinaryRow, Int] =
+ if (inferBucketNumFromData) {
+ val targetRowNum = coreOptions.postponeTargetRowNumPerBucket()
+ val postponeRowCounts =
+ if (overwriteExistingData) {
+ java.util.Collections.emptyMap[BinaryRow, java.lang.Long]()
+ } else {
+ PostponeUtils.getPostponeRowCounts(table)
+ }
+ val dataStats =
+ collectDataStatsByPartition(df, collectSize = !targetRowNum.isPresent)
+ dataStats.map {
+ case (partition, stats) =>
+ val postponeRowCount = postponeRowCounts.getOrDefault(partition, 0L)
+ val numBuckets =
+ if (targetRowNum.isPresent) {
+ computeBucketNumByRowCount(
+ Math.addExact(stats.rowCount, postponeRowCount),
+ targetRowNum.get(),
+ maxNumBuckets)
+ } else {
+ computeBucketNumBySize(
+ stats,
+ postponeRowCount,
+ coreOptions.postponeTargetSizePerBucket(),
+ maxNumBuckets)
+ }
+ partition -> numBuckets
+ }
+ } else {
+ Map.empty
+ }
+ val partitionBucketComputer = (partition: BinaryRow) =>
+ knownNumBuckets.getOrDefault(
+ partition,
+ Integer.valueOf(inferredNumBuckets.getOrElse(partition, defaultNumBuckets)))
+ PostponeBucketAssignment(partitionBucketComputer, inferBucketNumFromData)
+ } catch {
+ case e: Throwable =>
+ if (inferBucketNumFromData) {
+ df.unpersist()
+ }
+ throw e
+ }
+ }
+
+ private def collectDataStatsByPartition(
+ df: DataFrame,
+ collectSize: Boolean): Map[BinaryRow, PartitionDataStats] = {
+ val schema = tableSchema
+ val rowType = writeType
+ val toPaimonRow = SparkRowUtils.toPaimonRow(
+ rowType,
+ SparkRowUtils.getFieldIndex(df.schema, ROW_KIND_COL),
+ uriReaderFactoryForBlobDescriptor)
+ df.rdd
+ .mapPartitions {
+ rows =>
+ val partitionKeyExtractor = new RowPartitionKeyExtractor(schema)
+ val rowSerializer = InternalSerializers.create(rowType)
+ val stats = mutable.HashMap.empty[SerializedPartition, PartitionDataStats]
+ rows.foreach {
+ row =>
+ val paimonRow = toPaimonRow(row)
+ val partition = SerializedPartition(
+ SerializationUtils.serializeBinaryRow(partitionKeyExtractor.partition(paimonRow)))
+ val rowSize =
+ if (collectSize) rowSerializer.toBinaryRow(paimonRow).getSizeInBytes.toLong else 0L
+ val previous = stats.getOrElse(partition, PartitionDataStats(0L, 0L))
+ stats.put(
+ partition,
+ PartitionDataStats(
+ Math.addExact(previous.rowCount, 1L),
+ Math.addExact(previous.serializedSize, rowSize)))
+ }
+ stats.iterator
+ }
+ .reduceByKey {
+ (left, right) =>
+ PartitionDataStats(
+ Math.addExact(left.rowCount, right.rowCount),
+ Math.addExact(left.serializedSize, right.serializedSize))
+ }
+ .collect()
+ .map {
+ case (partition, stats) =>
+ SerializationUtils.deserializeBinaryRow(partition.bytes) -> stats
+ }
+ .toMap
+ }
+
+ private def computeBucketNumBySize(
+ dataStats: PartitionDataStats,
+ postponeRowCount: Long,
+ targetSizePerBucket: Long,
+ maxNumBuckets: Int): Int = {
+ if (targetSizePerBucket <= 0) {
+ throw new IllegalArgumentException(
+ "Option 'postpone.target-size-per-bucket' must be greater than 0.")
+ }
+
+ // Previous postpone files do not record their uncompressed serialized size. Estimate it with
+ // the average serialized size of incoming rows from the same partition.
+ val estimatedTotalSizeNumerator =
+ BigInt(dataStats.serializedSize) *
+ (BigInt(dataStats.rowCount) + BigInt(postponeRowCount))
+ val rowCount = BigInt(dataStats.rowCount)
+ val estimatedTotalSize = (estimatedTotalSizeNumerator + rowCount - 1) / rowCount
+ val bucketNum =
+ if (estimatedTotalSize == 0) BigInt(1)
+ else (estimatedTotalSize - 1) / BigInt(targetSizePerBucket) + 1
+ bucketNum.min(BigInt(maxNumBuckets)).toInt
+ }
+
private def deserializeCommitMessage(
serializer: CommitMessageSerializer,
bytes: Array[Byte]): CommitMessage = {
@@ -551,10 +695,42 @@ case class PaimonSparkWriter(
override def numPartitions: Int = partitions
override def getPartition(key: Any): Int = Math.abs(key.asInstanceOf[Int] % numPartitions)
}
+
}
object PaimonSparkWriter {
+ private[spark] def computeBucketNumByRowCount(
+ rowCount: Long,
+ targetRowNumPerBucket: Long,
+ maxNumBuckets: Int): Int = {
+ if (targetRowNumPerBucket <= 0) {
+ throw new IllegalArgumentException(
+ "Option 'postpone.target-row-num-per-bucket' must be greater than 0.")
+ }
+
+ val bucketNum =
+ if (rowCount <= 0) 1L else (rowCount - 1) / targetRowNumPerBucket + 1
+ Math.min(bucketNum, maxNumBuckets.toLong).toInt
+ }
+
+ private case class PostponeBucketAssignment(
+ partitionBucketComputer: BinaryRow => Integer,
+ dataPersisted: Boolean)
+
+ private case class PartitionDataStats(rowCount: Long, serializedSize: Long)
+
+ private case class SerializedPartition(bytes: Array[Byte]) {
+ override def equals(other: Any): Boolean = {
+ other match {
+ case that: SerializedPartition => java.util.Arrays.equals(bytes, that.bytes)
+ case _ => false
+ }
+ }
+
+ override def hashCode(): Int = java.util.Arrays.hashCode(bytes)
+ }
+
def apply(table: FileStoreTable): PaimonSparkWriter = {
new PaimonSparkWriter(table)
}
diff --git a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/WriteIntoPaimonTable.scala b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/WriteIntoPaimonTable.scala
index f400409fdae1..346aa0fe3ae8 100644
--- a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/WriteIntoPaimonTable.scala
+++ b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/WriteIntoPaimonTable.scala
@@ -71,7 +71,10 @@ case class WriteIntoPaimonTable(
Snapshot.Operation.WRITE
}
}
- val commitMessages = writer.write(replacedData)
+ val commitMessages =
+ writer.write(
+ replacedData,
+ overwriteExistingData = overwritePartition != null || dynamicPartitionOverwriteMode)
writer.commit(commitMessages, operation)
Seq.empty
diff --git a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PostponeBucketTableTest.scala b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PostponeBucketTableTest.scala
index 805b747bb6c4..1e9910bf7e6e 100644
--- a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PostponeBucketTableTest.scala
+++ b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PostponeBucketTableTest.scala
@@ -22,6 +22,7 @@ import org.apache.paimon.catalog.{Catalog, CatalogLoader, DelegateCatalog, Ident
import org.apache.paimon.deletionvectors.DeletionVectorsIndexFile.DELETION_VECTORS_INDEX
import org.apache.paimon.fs.Path
import org.apache.paimon.spark.{PaimonScan, PaimonSparkTestBase}
+import org.apache.paimon.spark.commands.PaimonSparkWriter
import org.apache.paimon.spark.procedure.SparkPostponeCompactProcedure
import org.apache.paimon.table.{CatalogEnvironment, FileStoreTableFactory}
@@ -33,6 +34,14 @@ import scala.collection.JavaConverters._
class PostponeBucketTableTest extends PaimonSparkTestBase {
+ test("Postpone bucket table: cap inferred row-count buckets before Int conversion") {
+ assert(
+ PaimonSparkWriter.computeBucketNumByRowCount(
+ Integer.MAX_VALUE.toLong + 1L,
+ targetRowNumPerBucket = 1L,
+ maxNumBuckets = 2048) == 2048)
+ }
+
test("Postpone bucket table: write with different bucket number") {
withTable("t") {
sql("""
@@ -60,9 +69,11 @@ class PostponeBucketTableTest extends PaimonSparkTestBase {
checkAnswer(sql("SELECT sum(k) FROM t"), Seq(Row(499500)))
checkAnswer(
sql("SELECT distinct(bucket) FROM `t$buckets` ORDER BY bucket"),
- Seq(Row(0), Row(1), Row(2), Row(3))
+ Seq(Row(0))
)
+ sql("ALTER TABLE t SET TBLPROPERTIES ('postpone.target-size-per-bucket' = '8 kb')")
+
// Write to existing partition, the bucket number should not change
sql("""
|INSERT INTO t SELECT /*+ REPARTITION(6) */
@@ -73,7 +84,7 @@ class PostponeBucketTableTest extends PaimonSparkTestBase {
|""".stripMargin)
checkAnswer(
sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{3}' ORDER BY bucket"),
- Seq(Row(0), Row(1), Row(2), Row(3))
+ Seq(Row(0))
)
// Write to new partition, the bucket number should change
@@ -86,7 +97,201 @@ class PostponeBucketTableTest extends PaimonSparkTestBase {
|""".stripMargin)
checkAnswer(
sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{5}' ORDER BY bucket"),
- Seq(Row(0), Row(1), Row(2), Row(3), Row(4), Row(5))
+ Seq(Row(0), Row(1), Row(2))
+ )
+ }
+ }
+
+ test("Postpone bucket table: infer bucket number from incoming row count") {
+ withTable("t") {
+ sql("""
+ |CREATE TABLE t (
+ | k INT,
+ | v STRING,
+ | pt INT
+ |) PARTITIONED BY (pt)
+ |TBLPROPERTIES (
+ | 'primary-key' = 'k, pt',
+ | 'bucket' = '-2',
+ | 'postpone.batch-write-fixed-bucket' = 'true',
+ | 'postpone.target-row-num-per-bucket' = '200'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO t SELECT /*+ REPARTITION(20) */
+ |id AS k,
+ |CAST(id AS STRING) AS v,
+ |CASE WHEN id < 100 THEN 0 ELSE 1 END AS pt
+ |FROM range (0, 550)
+ |""".stripMargin)
+
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{0}' ORDER BY bucket"),
+ Seq(Row(0))
+ )
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{1}' ORDER BY bucket"),
+ Seq(Row(0), Row(1), Row(2))
+ )
+
+ // Existing partitions keep their bucket number even when the new data volume changes.
+ sql("""
+ |INSERT INTO t SELECT /*+ REPARTITION(20) */
+ |id AS k,
+ |CAST(id AS STRING) AS v,
+ |0 AS pt
+ |FROM range (1000, 2000)
+ |""".stripMargin)
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{0}' ORDER BY bucket"),
+ Seq(Row(0))
+ )
+
+ // Postpone rows are included when a partition gets real buckets for the first time.
+ withSparkSQLConf("spark.paimon.postpone.batch-write-fixed-bucket" -> "false") {
+ sql("""
+ |INSERT INTO t SELECT
+ |id AS k,
+ |CAST(id AS STRING) AS v,
+ |2 AS pt
+ |FROM range (2000, 2150)
+ |""".stripMargin)
+ }
+ sql("""
+ |INSERT INTO t SELECT /*+ REPARTITION(20) */
+ |id AS k,
+ |CAST(id AS STRING) AS v,
+ |2 AS pt
+ |FROM range (3000, 3100)
+ |""".stripMargin)
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{2}' ORDER BY bucket"),
+ Seq(Row(-2), Row(0), Row(1))
+ )
+ }
+ }
+
+ test("Postpone bucket table: overwrite excludes existing postpone rows from inference") {
+ Seq(
+ (
+ "static",
+ """
+ |INSERT OVERWRITE t SELECT
+ |id AS k,
+ |CAST(id AS STRING) AS v,
+ |0 AS pt
+ |FROM range (1000, 1100)
+ |""".stripMargin),
+ (
+ "static",
+ """
+ |INSERT OVERWRITE t PARTITION (pt = 0) SELECT
+ |id AS k,
+ |CAST(id AS STRING) AS v
+ |FROM range (1000, 1100)
+ |""".stripMargin),
+ (
+ "dynamic",
+ """
+ |INSERT OVERWRITE t SELECT
+ |id AS k,
+ |CAST(id AS STRING) AS v,
+ |0 AS pt
+ |FROM range (1000, 1100)
+ |""".stripMargin)
+ ).foreach {
+ case (partitionOverwriteMode, overwriteSql) =>
+ withTable("t") {
+ sql("""
+ |CREATE TABLE t (
+ | k INT,
+ | v STRING,
+ | pt INT
+ |) PARTITIONED BY (pt)
+ |TBLPROPERTIES (
+ | 'primary-key' = 'k, pt',
+ | 'bucket' = '-2',
+ | 'postpone.batch-write-fixed-bucket' = 'false',
+ | 'postpone.target-row-num-per-bucket' = '100'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO t SELECT
+ |id AS k,
+ |CAST(id AS STRING) AS v,
+ |0 AS pt
+ |FROM range (0, 1000)
+ |""".stripMargin)
+
+ withSparkSQLConf(
+ "spark.paimon.postpone.batch-write-fixed-bucket" -> "true",
+ "spark.sql.sources.partitionOverwriteMode" -> partitionOverwriteMode) {
+ sql(overwriteSql)
+ }
+
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{0}'"),
+ Seq(Row(0))
+ )
+ }
+ }
+ }
+
+ test("Postpone bucket table: infer bucket number from serialized data size") {
+ withTable("t") {
+ sql("""
+ |CREATE TABLE t (
+ | k INT,
+ | v STRING,
+ | pt INT
+ |) PARTITIONED BY (pt)
+ |TBLPROPERTIES (
+ | 'primary-key' = 'k, pt',
+ | 'bucket' = '-2',
+ | 'postpone.batch-write-fixed-bucket' = 'true',
+ | 'postpone.target-size-per-bucket' = '32 kb'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO t SELECT /*+ REPARTITION(20) */
+ |id AS k,
+ |CASE WHEN id < 100 THEN repeat('x', 100) ELSE repeat('x', 1000) END AS v,
+ |CASE WHEN id < 100 THEN 0 ELSE 1 END AS pt
+ |FROM range (0, 200)
+ |""".stripMargin)
+
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{0}' ORDER BY bucket"),
+ Seq(Row(0))
+ )
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{1}' ORDER BY bucket"),
+ Seq(Row(0), Row(1), Row(2), Row(3))
+ )
+
+ // Estimate existing postpone data with the average serialized size of incoming rows.
+ withSparkSQLConf("spark.paimon.postpone.batch-write-fixed-bucket" -> "false") {
+ sql("""
+ |INSERT INTO t SELECT
+ |id AS k,
+ |repeat('x', 1000) AS v,
+ |2 AS pt
+ |FROM range (1000, 1100)
+ |""".stripMargin)
+ }
+ sql("""
+ |INSERT INTO t SELECT /*+ REPARTITION(20) */
+ |id AS k,
+ |repeat('x', 1000) AS v,
+ |2 AS pt
+ |FROM range (2000, 2100)
+ |""".stripMargin)
+ checkAnswer(
+ sql("SELECT distinct(bucket) FROM `t$buckets` WHERE partition = '{2}' ORDER BY bucket"),
+ Seq(Row(-2), Row(0), Row(1), Row(2), Row(3), Row(4), Row(5), Row(6))
)
}
}
@@ -116,7 +321,7 @@ class PostponeBucketTableTest extends PaimonSparkTestBase {
checkAnswer(sql("SELECT sum(k) FROM t"), Seq(Row(499500)))
checkAnswer(
sql("SELECT distinct(bucket) FROM `t$buckets` ORDER BY bucket"),
- Seq(Row(0), Row(1), Row(2), Row(3))
+ Seq(Row(0))
)
// write postpone bucket
@@ -131,7 +336,7 @@ class PostponeBucketTableTest extends PaimonSparkTestBase {
checkAnswer(sql("SELECT sum(k) FROM t"), Seq(Row(499500)))
checkAnswer(
sql("SELECT distinct(bucket) FROM `t$buckets` ORDER BY bucket"),
- Seq(Row(-2), Row(0), Row(1), Row(2), Row(3))
+ Seq(Row(-2), Row(0))
)
}
}
@@ -605,7 +810,7 @@ class PostponeBucketTableTest extends PaimonSparkTestBase {
checkAnswer(sql("SELECT sum(k) FROM t"), Seq(Row(499500)))
checkAnswer(
sql("SELECT distinct(bucket) FROM `t$buckets` ORDER BY bucket"),
- Seq(Row(-2), Row(0), Row(1), Row(2), Row(3), Row(4), Row(5))
+ Seq(Row(-2), Row(0))
)
}
@@ -621,7 +826,7 @@ class PostponeBucketTableTest extends PaimonSparkTestBase {
checkAnswer(sql("SELECT sum(k) FROM t"), Seq(Row(124750)))
checkAnswer(
sql("SELECT distinct(bucket) FROM `t$buckets` ORDER BY bucket"),
- Seq(Row(0), Row(1), Row(2), Row(3), Row(4), Row(5))
+ Seq(Row(0))
)
}
}