Skip to content

Commit 4a4b3f7

Browse files
fangbofangbo.0511
andauthored
feat: add param rows_per_range for range-based btree index built (#439)
# Background Currently the partition(range) number is configured by spark parameter like: `spark.sql.adaptive.coalescePartitions.initialPartitionNum` or `spark.sql.shuffle.partitions` when building btree index using range-mode. The current approach cannot dynamically adjust the number of ranges based on changes in the total row count of the Dataset. This becomes quite inconvenient when the total row count of the Dataset continues to grow. # Design So, we add a new parameter `rows_per_range` for range-mode. This param specifies the row number for each range. The spark partition number is calculated by `Dataset.total_rows/rows_per_range`. This method can dynamically adjust the number of Ranges based on the Dataset's row count. @hamersaw @puchengy Could you please take a look and see if this makes sense? Thank you. Co-authored-by: fangbo.0511 <fangbo.0511@bytedance.com>
1 parent e2600de commit 4a4b3f7

3 files changed

Lines changed: 48 additions & 10 deletions

File tree

‎docs/src/operations/ddl/create-index.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ The `CREATE INDEX` command supports options via the `WITH` clause to control ind
3535

3636
For the `btree` method, the following options are supported:
3737

38-
| Option | Type | Description |
39-
|-------------|--------|----------------------------------------------|
40-
| `zone_size` | Long | The number of rows per zone in the B-tree index. |
41-
| `build_mode`| String | Index building mode: 'fragment' builds indexes in parallel by fragment; 'range' sorts data by indexed columns first, then partitions and builds indexes in parallel by partition. Default is 'fragment'.|
38+
| Option | Type | Description |
39+
|------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
40+
| `zone_size` | Long | The number of rows per zone in the B-tree index. |
41+
| `build_mode` | String | Index building mode: 'fragment' builds indexes in parallel by fragment; 'range' sorts data by indexed columns first, then partitions and builds indexes in parallel by partition. Default is 'fragment'. |
42+
| `rows_per_range` | Long | The number of rows per range when built using range mode. Default is 1000000. |
43+
4244

4345
### FTS Options
4446

‎lance-spark-base_2.12/src/main/scala/org/apache/spark/sql/execution/datasources/v2/AddIndexExec.scala‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,11 @@ case class AddIndexExec(
8585
val uuid = UUID.randomUUID()
8686
val indexType = IndexUtils.buildIndexType(method)
8787

88+
val dataset = Utils.openDatasetBuilder(readOptions).build()
89+
8890
val indexBuildResult =
89-
createIndexJob(lanceDataset, readOptions, uuid.toString, fragmentIds).run()
91+
createIndexJob(dataset, lanceDataset, readOptions, uuid.toString, fragmentIds).run()
9092

91-
val dataset = Utils.openDatasetBuilder(readOptions).build()
9293
try {
9394
// Merge index metadata after all fragments are indexed
9495
dataset.mergeIndexMetadata(uuid.toString, indexType, Optional.empty())
@@ -148,6 +149,7 @@ case class AddIndexExec(
148149
}
149150

150151
private def createIndexJob(
152+
dataset: Dataset,
151153
lanceDataset: LanceDataset,
152154
readOptions: LanceSparkReadOptions,
153155
uuid: String,
@@ -179,7 +181,8 @@ case class AddIndexExec(
179181
nsImpl,
180182
nsProps,
181183
tableId,
182-
initialStorageOpts)
184+
initialStorageOpts,
185+
dataset.getVersion.getManifestSummary.getTotalRows)
183186

184187
case Some("fragment") | None =>
185188
new FragmentBasedIndexJob(
@@ -362,6 +365,7 @@ case class FragmentIndexTask(
362365
* @param nsProps Optional namespace properties for credential vending
363366
* @param tableId Optional table identifier for credential vending
364367
* @param initialStorageOpts Optional initial storage options for the dataset
368+
* @param totalRows Total number of rows in the dataset
365369
*/
366370
class RangeBasedBTreeIndexJob(
367371
addIndexExec: AddIndexExec,
@@ -370,9 +374,11 @@ class RangeBasedBTreeIndexJob(
370374
nsImpl: Option[String],
371375
nsProps: Option[Map[String, String]],
372376
tableId: Option[List[String]],
373-
initialStorageOpts: Option[Map[String, String]]) extends IndexJob {
377+
initialStorageOpts: Option[Map[String, String]],
378+
totalRows: Long) extends IndexJob {
374379

375380
private val VALUE_COLUMN_NAME = "value"
381+
private val DEFAULT_ROWS_PER_RANGE = 1000000L
376382

377383
override def run(): IndexBuildResult = {
378384
if (addIndexExec.columns.size != 1) {
@@ -402,11 +408,15 @@ class RangeBasedBTreeIndexJob(
402408
df.select(df.col(columns.head).as(VALUE_COLUMN_NAME), df.col(LanceDataset.ROW_ID_COLUMN.name))
403409

404410
// Repartition the data to numRanges and sort by indexed column
411+
val rowsPerRange = addIndexExec.args.find(_.name == "rows_per_range").map(
412+
_.value.asInstanceOf[Long]).getOrElse(DEFAULT_ROWS_PER_RANGE)
413+
val numRange = Math.max(1L, totalRows / rowsPerRange.longValue())
414+
405415
val rangeDf = selectDf
406416
.repartitionByRange(
407-
session.sessionState.conf.numShufflePartitions,
417+
numRange.intValue(),
408418
selectDf.col(VALUE_COLUMN_NAME).asc)
409-
.sortWithinPartitions(VALUE_COLUMN_NAME)
419+
.sortWithinPartitions(selectDf.col(VALUE_COLUMN_NAME).asc)
410420

411421
val indexBuilder = RangeBTreeIndexBuilder(
412422
encode(readOptions),

‎lance-spark-base_2.12/src/test/java/org/lance/spark/update/BaseAddIndexTest.java‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,32 @@ public void testCreateBTreeIndexWithRangeMode() {
229229
Assertions.assertEquals("text_15", r.getString(1));
230230
}
231231

232+
@Test
233+
public void testCreateBTreeIndexWithRowsPerRange() {
234+
prepareDataset();
235+
Dataset<Row> result =
236+
spark.sql(
237+
String.format(
238+
"alter table %s create index test_index_btree_param using btree (id) "
239+
+ "with (zone_size=2048, build_mode='range', rows_per_range=2)",
240+
fullTable));
241+
Assertions.assertEquals(
242+
"StructType(StructField(fragments_indexed,LongType,true),StructField(index_name,StringType,true))",
243+
result.schema().toString());
244+
Row row = result.collectAsList().get(0);
245+
long fragmentsIndexed = row.getLong(0);
246+
String indexName = row.getString(1);
247+
Assertions.assertTrue(fragmentsIndexed >= 2, "Expected at least 2 fragments to be indexed");
248+
Assertions.assertEquals("test_index_btree_param", indexName);
249+
checkIndex("test_index_btree_param");
250+
// Verify query using the indexed field with zone_size parameter
251+
Dataset<Row> query = spark.sql(String.format("select * from %s where id=15", fullTable));
252+
Assertions.assertEquals(1L, query.count());
253+
Row r = query.collectAsList().get(0);
254+
Assertions.assertEquals(15, r.getInt(0));
255+
Assertions.assertEquals("text_15", r.getString(1));
256+
}
257+
232258
@Test
233259
public void testCreateBTreeIndexWithFragmentMode() {
234260
prepareDataset();

0 commit comments

Comments
 (0)