Skip to content

[SPARK-58315][SQL] Refactor V2 scan statistics handling#57487

Open
yyanyy wants to merge 1 commit into
apache:masterfrom
yyanyy:yyanyy-dsv2-scan-statistics
Open

[SPARK-58315][SQL] Refactor V2 scan statistics handling#57487
yyanyy wants to merge 1 commit into
apache:masterfrom
yyanyy:yyanyy-dsv2-scan-statistics

Conversation

@yyanyy

@yyanyy yyanyy commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Refactor how a DataSource V2 (DSv2) scan reports statistics, aligning it with
the v1 (LogicalRelation / CatalogStatistics.toPlanStats) path.

  • computeStats gates on cboEnabled || planStatsEnabled: full statistics
    only when needed, otherwise a cheaper size-only estimate via the new
    SupportsReportStatistics.estimateSizeInBytes() default method.
  • New SupportsReportStatistics.reflectsFullyPushedDownFilters() default method
    (true). When a connector returns false, V2ScanRelationPushDown re-adds
    the fully pushed, deterministic filters above the scan so FilterEstimation
    adjusts the stats. The rewrite is row-equivalent, idempotent, and best-effort
    under column pruning (pruned-reference predicates are dropped).
  • transformV2Stats infers sizeInBytes from the row count when no size is
    reported (matching v1); the dead defaultRowCount parameter is removed.
  • V1ScanWrapper implements SupportsReportStatistics, delegating to the
    wrapped v1 scan so its stats are no longer dropped.
  • Shared logic extracted into V2StatisticsUtils (null-safe).

Both new methods have defaults that preserve existing behavior, so existing
connectors need no change.

Why are the changes needed?

To let DSv2 connectors produce statistics more cheaply and let Spark plan on
more accurate statistics, matching v1:

  • Cheaper when only size is needed: most queries (CBO/plan stats off) only need
    sizeInBytes, so a connector can skip the expensive row/column statistics.
  • Accurate after pushdown: whole-table stats are adjusted by FilterEstimation
    instead of planning on too-large estimates, improving plan choices.
  • Fuller use of reported stats: v1 scan stats and a row-count-derived size are
    now used instead of the default size, at no extra cost to the connector.

Does this PR introduce any user-facing change?

New API introduced but no behavior change by default except for the following cases:

  • when a source reports a row count but no size, the size is inferred from the row count (previously the
    default size was used)
  • a v1 scan implementing SupportsReportStatistics now has its reported statistics surfaced (previously dropped).

Both changes make the estimates more accurate and align with the v1 path.

How was this patch tested?

New unit and end-to-end tests in DataSourceV2RelationSuite and
DataSourceV2Suite cover: the CBO / plan-stats gating, size inference
(including the zero-row case), null/empty statistics handling, V1ScanWrapper
delegation, and the post-pushdown filter re-adding (including the recursion
through stacked residual filters).

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code

Refactor how a DSv2 scan reports statistics so the work matches what the
optimizer needs, and so source-reported statistics stay accurate after filter
pushdown. Aligns the DSv2 path with the v1 (LogicalRelation /
CatalogStatistics.toPlanStats) path.

Benefits:
- Stats are computed on demand: full stats only when CBO or plan stats are
  enabled, otherwise a cheaper size-only estimate via the new
  estimateSizeInBytes(). Previously the scan always fetched full stats.
- Source stats stay accurate across pushdown: a connector whose stats do not
  reflect the fully pushed filters can set reflectsFullyPushedDownFilters() to
  false, and Spark re-adds those filters above the scan so FilterEstimation
  adjusts the stats.
- V1 scans that report statistics now surface them through V1ScanWrapper
  (previously dropped).

Changes:
- computeStats gates on `cboEnabled || planStatsEnabled` (mirrors v1).
- Add estimateSizeInBytes() and reflectsFullyPushedDownFilters() default
  methods to SupportsReportStatistics.
- transformV2Stats infers sizeInBytes from numRows when no size is reported
  (matching v1 toPlanStats); drop the dead defaultRowCount parameter.
- V1ScanWrapper implements SupportsReportStatistics.
- Extract the shared statistics logic into V2StatisticsUtils (null-safe).

Tests: CBO / plan-stats gating, size inference (including the zero-row case),
null/empty statistics, V1ScanWrapper delegation, and post-pushdown filter
re-adding (including recursion through stacked residual filters).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant