mixermt opened a new issue, #4859:
URL: https://github.com/apache/datafusion-comet/issues/4859
### Describe the bug
Comet's native Parquet scan appears to ignore nested schema pruning for
complex columns. In a production Spark 3.5.6 comparison job, plain Spark and
Comet produced the same Spark physical-plan `ReadSchema`, same input row count,
same selected files, and same downstream shuffle output, but Comet
read/decompressed far more data during the scan.
Spark 3.5.6, Comet 0.17.0
The query reads Parquet from HDFS and writes Parquet back to HDFS. Column
names below are anonymized. The relevant scan reads four top-level fields:
```text
is_flagged
source_type
events
event_hour
```
The Spark UI shows the same pruned nested schema under `events` for both
plans:
```text
ReadSchema: struct<
is_flagged:boolean,
source_type:string,
events:array<struct<
is_available:boolean,
event_time_ms:bigint,
is_active:boolean,
items:array<struct<
group_id:bigint,
entity_id:bigint,
has_amount:boolean,
item_type:string,
metric_value:double,
actor_id:bigint,
is_skipped:boolean
>>
>>,
event_hour:timestamp
>
```
The physical input schema is much wider than the requested schema. A
representative anonymized shape is:
```text
InputSchema: struct<
is_flagged:boolean,
source_type:string,
event_hour:timestamp,
dimension_id:bigint,
region_code:string,
client_type:string,
ingestion_time:timestamp,
events:array<struct<
is_available:boolean,
event_time_ms:bigint,
is_active:boolean,
event_token:string,
source_name:string,
secondary_id:bigint,
debug_flags:array<string>,
latency_parts:struct<
queue_time_ms:bigint,
fetch_time_ms:bigint,
render_time_ms:bigint,
retry_count:int
>,
items:array<struct<
group_id:bigint,
entity_id:bigint,
has_amount:boolean,
item_type:string,
metric_value:double,
actor_id:bigint,
is_skipped:boolean,
auxiliary_id:bigint,
class_code:string,
mode:string,
raw_score:double,
normalized_score:double,
reason_codes:array<string>,
audit_blob:binary,
feature_map:map<string,double>,
diagnostics:struct<
module_id:string,
module_version:string,
trace_id:string,
extra_payload:binary
>
>>
>>,
raw_payload:binary,
metadata_json:string
>
```
The expected behavior is to read only the requested nested leaves, not all
child fields under `events` / `items`.
However, the observed scan metrics were very different:
| Metric | Plain Spark scan | Comet native scan |
| --- | ---: | ---: |
| Spark version | 3.5.6 | 3.5.6 |
| SQL duration | 120,642 ms | 1,688,780 ms |
| Scan tasks | 20,012 | 20,012 |
| Files selected | 10,300 | 10,300 |
| Input records | 310,897,758 | 310,897,758 |
| Stage input bytes | 30,945,027,031 | 1,351,791,550,588 |
| Shuffle records written | 157,270,190 | 157,270,190 |
| Shuffle bytes written | 3,216,674,929 | 3,216,674,929 |
| Comet `bytes_scanned` metric | n/a | 1259.0 GiB |
`size of files read = 2.2 TiB` appeared in both UIs, but that is the
selected file length metric, not physical bytes fetched. The physical read
evidence is Spark stage `inputBytes` and Comet's native `bytes_scanned` metric.
This strongly suggests the logical Spark-side pruning is present, but
Comet's native reader still reads a much wider nested Parquet schema for the
projected complex column.
### Steps to reproduce
1. Use Spark 3.5.x with Comet native Parquet scans enabled.
2. Read a Parquet dataset with a wide nested column, for example an
`array<struct<...>>` with many child fields.
3. Run a query that only needs a subset of nested fields from that complex
column.
4. Compare against plain Spark using the same query and selected files.
5. In Spark UI, verify both plans show the same pruned `ReadSchema`.
6. Compare stage input bytes and Comet native scan `Number of bytes scanned`.
Minimal shape of the reproducer:
```scala
val df = spark.read.schema(prunedSchema).parquet(path)
df
.filter(!$"is_flagged")
.selectExpr(
"event_hour",
"posexplode_outer(events) as (event_idx, event)"
)
.filter("event.is_available and (event.is_active is null or
event.is_active)")
.selectExpr(
"event_hour",
"posexplode_outer(event.items) as (item_idx, item)",
"event.event_time_ms"
)
.filter("item.entity_id is not null")
.groupBy(
$"event_hour",
$"item.actor_id",
$"item.group_id",
$"item.entity_id"
)
.count()
.write.mode("overwrite").parquet(outputPath)
```
The important part is that the query projects only selected child fields
from a nested Parquet column while the original physical Parquet files contain
many more children under that same top-level column.
### Expected behavior
Comet native Parquet scan should honor Spark's pruned nested `ReadSchema`
and avoid reading/decompressing unrequested nested child fields, matching
Spark's Parquet scan behavior as closely as possible.
For this workload, Comet should not read ~1.35 TB of stage input when plain
Spark reads ~30.9 GB for the same files, same rows, and same displayed
`ReadSchema`.
### Additional context
Local code inspection points to the native V1 Parquet scan path using full
`data_schema` plus a top-level projection vector, instead of giving DataFusion
a nested-pruned Parquet read schema.
Relevant code path:
- Spark writes the pruned `requiredSchema` into Parquet read config:
-
`sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFileFormat.scala`
- `ParquetReadSupport.SPARK_ROW_REQUESTED_SCHEMA`
- Spark clips nested Parquet schemas in:
-
`sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetReadSupport.scala`
- `clipParquetSchema`, `clipParquetType`, `clipParquetListType`,
`clipParquetMapType`
- Comet serializes both the pruned schema and the full data schema:
-
`spark/src/main/scala/org/apache/comet/serde/operator/CometNativeScan.scala`
- `requiredSchema = schema2Proto(scan.requiredSchema.fields)`
- `dataSchema = schema2Proto(scan.relation.dataSchema.fields)`
- `projectionVector` is built with
`scan.relation.dataSchema.fieldIndex(field.name)`, so it is top-level only.
- Native planner passes both schemas into the native scan:
- `native/core/src/execution/planner.rs`
- `init_datasource_exec(required_schema, Some(data_schema), ...,
Some(projection_vector), ...)`
- Native Parquet setup chooses the full `data_schema` as the base schema
when present:
- `native/core/src/parquet/parquet_exec.rs`
- `(Some(schema), Some(proj)) => (Arc::clone(schema), Some(proj.clone()))`
- then applies `with_projection_indices(Some(projection))`
That can prune top-level columns, but it does not appear to prune nested
leaves inside a projected complex column such as `events`.
Possible fixes / workarounds:
1. Workaround: set `spark.comet.scan.enabled=false` for this workload. That
keeps Spark's Parquet scan path and avoids the native scan behavior.
2. Optional workaround to test: combine `spark.comet.scan.enabled=false`
with `spark.comet.convert.parquet.enabled=true`, so Spark performs the Parquet
read and Comet can still convert the resulting batches after the scan.
3. Code fix direction: for native V1 Parquet scan, initialize DataFusion's
Parquet reader with the nested-pruned `required_schema` when Spark has already
produced the read schema, instead of using full `data_schema` with only a
top-level projection vector.
4. Add a regression test with `array<struct<...many fields...>>`, select
only a subset of nested fields, verify `CometNativeScanExec` is used, and
assert the native scan does not read unrequested nested leaves.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]