Conversation
…rge) Snapshot of the logical, pre-execution physical, and executed stage plans for all 22 TPC-H queries, captured from a single run against Ballista 54.0.0 with SF1000 Parquet data. Checked in as a discussion surface for a draft PR — not intended to merge as reference material. Plans come from the scheduler event log added in apache#2264. See docs/source/contributors-guide/plans/tpch-sf1000/README.md for setup, config, and wall-clock summary.
|
I'm sorry for being backseat driver, adding spark plans would help to narrow does ballista have planning logic or execution issue |
|
note, maybe we could emit final plan to event log when AQE used |
|
AI slop good enough it's worth reposting verbatim: |
I'll try and get those tomorrow |
|
I think bumping |
|
LLM-assisted reply (Claude Code), reviewed by me before posting. Good call — filed as #2422 with a design sketch. Short version of what I found while writing it up: the post-AQE plan is already in the log, just unlabelled. So the proposal is mostly about making that explicit ( Open questions are in the issue; question 2 in particular is one for you if you have a view: |
|
q8/q9 time per query is highly variable |
Digging into q8: the 6B-row lineitem shuffle should not existFollowing up on Theme 1 with a closer look at where q8's 164s goes and why. Summing max task duration per stage gets to ~170s, so the DAG is effectively serialized stage by stage. One stage is 80% of the query:
So q8 is really a single-stage problem. Why we shuffle 6B rows to join against 1.3MThe build side of join Two independent gates block it, both in Gate 1: the pre-execution estimate is wrong by 30x. There are no runtime stats when the initial plan is built, so Gate 2: the 1M row ceiling is a hard AND, evaluated before the byte estimate. In if num_rows == 0 || num_rows >= threshold_num_rows {
return false;
}
estimate_output_byte_size(...).is_some_and(|est| est < threshold_byte_size)Even with a perfect 1.33M row estimate, the row check rejects the broadcast before the byte estimate runs. @avantgardnerio your instinct about bumping One more thing worth knowing: once Possible fixes, cheapest first1. Config-only experiment to confirm the diagnosis. Set 2. Make the byte estimate authoritative when it is available. Reorder 3. Asymmetric exchange: resolve the cheap side first. This is the structural fix. Today The distinction worth encoding is that "estimated large but inexact" is not the same as "known large". Right now both take the same branch. 4. Cross-stage dynamic filters (#1375). Biggest lever for the whole suite rather than just q8. Two secondary findingsStage 5 runs the whole join in a single task. The metrics are unambiguous: Stage 1's time tail looks like contention rather than shuffle cost. Compare against q9 s1, which is the identical operator over the identical 6B rows with six projected columns instead of five:
q8 moves less data and takes 3.3x longer per task, on more balanced input. Together with the repeat run above showing q8 at 57s and 142s across two identical iterations, that reads as variance rather than a deterministic cost. Candidates: 34 concurrent tasks each buffering up to 256 MiB in the sort shuffle writer (up to ~8.7 GiB per executor before spill), or S3 read bandwidth. Separately, 34 tasks for the most expensive stage in the query seems low for 32-core executors, so the scan's file-group count may be worth a look on its own. |
What this is
A snapshot of the logical, pre-execution physical, and executed stage
plans for all 22 TPC-H queries, captured from a single run against
Ballista 54.0.0 with a SF1000 Parquet/Iceberg-partitioned dataset. Plans
were pulled from the scheduler event log added in #2264.
Each
qNN.mdhas:JobStart)JobStart)stage_plan+ input/outputrows, elapsed compute, and task duration / task input percentiles
(from
JobEnd)Env, config, and wall-clock summary are in the
README.
Wall-clock (single run, one job at a time, no warm-up)
q1 9.1s · q2 39.5s · q3 25.5s · q4 9.0s · q5 35.5s · q6 5.2s · q7 39.6s
· q8 164.2s · q9 91.7s · q10 39.6s · q11 15.2s · q12 11.3s ·
q13 12.2s · q14 5.1s · q15 10.8s · q16 16.3s · q17 25.8s · q18 50.6s ·
q19 16.0s · q20 26.2s · q21 59.6s · q22 10.0s
Observations from a first pass over the plans
Two themes explain most of the wall-clock outliers. Would love the
community's read on both.
Theme 1 —
SortShuffleWriterExecover lineitem-scale stages shows a large time tail on balanced inputQ8 stage 1 is the worst case: 34 tasks, task duration
min 14s / median 62s / max 131s, but task-input spread is only 1.36×. A 9.4× time spreadon balanced data. Q9 s1 and Q3 s3 show the same shape (3–3.5× time
spread on ~1× input). Not data skew — looks like sort/spill/memory
pressure on the writer.
SortShuffleWriterExecthe intended writer for stages of this size?ballista.shuffle.sort_based.memory_limit_per_task_bytes(currently 256 MiB) help, or is this the wrong strategy for
lineitem-scale scans?
Theme 2 — with
coalesce.enabled=false, most tasks for small partitioned dim tables read zero rowsIceberg-partitioned dim tables (
customerbyc_mktsegment,supplierby nation,
nation,region) produce a handful of useful tasks and 30+empty ones. Examples:
coalesce.enabled=falseintentional at SF1000, or should empty /tiny partitions be merged automatically?
Metric definitions I couldn't pin down from the event log
Both look like reporting issues rather than execution issues, but they
make the numbers hard to reason about.
output_rowsappears to sum across operators, not reportthe terminal operator. Q17 s2:
input_rows=5,999,981,877,output_rows=17,999,953,463(3× lineitem cardinality). Q1 s0:output_rows=17,746,748,965for a stage whose scan is ~6B. Thepattern matches
sum(op.output_rows for op in stage)where the intentlooks like it was the terminal op.
input_rowsis inconsistent. Same plan shape(
SortShuffleWriter ← PartialAgg ← … ← DataSource(lineitem)) reports401 in Q1 s0 but 6B in Q17 s2. Something switches between "batch /
group count" and "row count" depending on plan shape.
Is either of these an intentional definition, or worth an issue?
Smaller items worth a look
correlated-subquery rewrite, no scan-sharing. Any planned work on
shared scans / CTE materialization in DataFusion / Ballista?
CollectLefthash join. Expected for broadcast, but that's a lot forone partition — is there a threshold at which the probe should
re-parallelize?
input_rows=3,793,363,900on lineitem in threedifferent stages with three different join keys and no explicit
lineitem date filter. Same exact count in all three suggests it's
post-filter cardinality of
l_receiptdate > l_commitdate(~63% of6B). Just wanted to confirm where in the pipeline that count is
measured.
DataSourceExecfile-groups sections are huge — every planinlines the full parquet file list per group. Any appetite for a
compact representation in the event log?
Positive:
AdaptiveDatafusionExecappears once at the root of everyplan, no fragmentation.
Not asking for
docs/.there's appetite for keeping a versioned plan baseline in-tree.