This is an automated email from the ASF dual-hosted git repository.
sarutak pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 05972c5dde5a [SPARK-55876][SQL] Add query statistics summary bar to
SQL tab
05972c5dde5a is described below
commit 05972c5dde5aabd190e46fbc805bb4bb079a597b
Author: Adithya Ajith <[email protected]>
AuthorDate: Tue Jun 2 14:00:54 2026 +0900
[SPARK-55876][SQL] Add query statistics summary bar to SQL tab
### What changes were proposed in this pull request?
This PR adds a SQL workload summary table at the top of the SQL / DataFrame
page ([SPARK-55876](https://issues.apache.org/jira/browse/SPARK-55876)).
The summary is returned as part of the existing SQL table REST response and
rendered above the SQL executions table. The summary is a fixed one-row table
styled consistently with the existing SQL executions table. It is not
initialized as a DataTable because paging, searching, ordering, and info
controls are not needed for a fixed 1x5 summary.
The summary is computed from the same execution set represented by the
current SQL table mode:
- When sub-execution grouping is enabled, the summary counts root SQL
executions only, matching the rows shown in the SQL executions table.
- When sub-execution grouping is disabled, the summary counts all SQL
executions, matching the flat table view.
The summary contains the following fields:
- Total Queries: total number of SQL query rows represented by the current
table mode.
- Average Duration: average duration across those query rows. Completed and
failed executions use their final duration. Running executions use their
current elapsed duration, computed with a single timestamp per response and
consistent with how the SQL executions table displays duration.
- Running Queries: number of query rows whose current status is RUNNING.
- Failed Queries: number of query rows whose current status is FAILED.
- Failure Rate: failed queries divided by total queries, displayed as a
percentage. When there are no queries, this is displayed as 0.0%.
The SQL executions table remains unchanged functionally.
### Why are the changes needed?
The SQL tab currently requires users to inspect the full executions table
to understand the overall SQL workload state.
Adding a summary table provides an at-a-glance view of SQL workload health,
including total query volume, average duration, currently running queries, and
failure rate.
### Does this PR introduce _any_ user-facing change?
Yes.
The SQL / DataFrame page now shows a Summary table above the SQL Executions
table.
### How was this patch tested?
Ran:
- `build/sbt 'sql/testOnly
org.apache.spark.status.api.v1.sql.SqlResourceWithActualMetricsSuite'`
- `build/sbt 'sql/testOnly
org.apache.spark.sql.execution.ui.AllExecutionsPageWithInMemoryStoreSuite'`
- `git diff --check`
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: OpenAI Codex GPT-5.5
### Before
<img width="1512" height="468" alt="before"
src="https://github.com/user-attachments/assets/9bd28a2a-2af5-49a4-b6f0-9480cc6a8f01"
/>
### After
<img width="1512" height="671" alt="Screenshot 2026-05-28 at 11 05 42 PM"
src="https://github.com/user-attachments/assets/e7cb8666-29c0-4d9b-824e-3f7b1b18986f"
/>
Closes #56090 from XdithyX/SPARK-55876.
Authored-by: Adithya Ajith <[email protected]>
Signed-off-by: Kousuke Saruta <[email protected]>
(cherry picked from commit f7c103196e0583c9a0206189c08b1e93c0e4ccc7)
Signed-off-by: Kousuke Saruta <[email protected]>
---
.../sql/execution/ui/static/allexecutionspage.js | 31 ++++++++++++++++++++--
.../spark/sql/execution/ui/AllExecutionsPage.scala | 3 +++
.../spark/status/api/v1/sql/SqlResource.scala | 26 ++++++++++++++++++
.../sql/execution/ui/AllExecutionsPageSuite.scala | 1 +
.../v1/sql/SqlResourceWithActualMetricsSuite.scala | 13 +++++++++
5 files changed, 72 insertions(+), 2 deletions(-)
diff --git
a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
index ed4561c73224..5973ab896de1 100644
---
a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
+++
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
@@ -32,16 +32,40 @@ $(document).ready(function () {
var container = document.getElementById("sql-executions-table");
container.innerHTML =
+ '<h4 class="title-table">SQL Executions</h4>' +
+ '<div>' +
'<select id="status-filter" class="form-select form-select-sm ' +
'd-inline-block w-auto mb-2">' +
'<option value="">All Statuses</option>' +
'<option value="RUNNING">Running</option>' +
'<option value="COMPLETED">Completed</option>' +
'<option value="FAILED">Failed</option>' +
- '</select>' +
+ '</select></div>' +
'<table id="sql-table" class="table table-striped compact cell-border" '
+
'style="width:100%"></table>';
+ function renderSummary(summary) {
+ var summaryBar = document.getElementById("sql-summary-bar");
+ if (!summaryBar || !summary) return;
+ var failureRate = (summary.failureRate || 0) * 100;
+ summaryBar.innerHTML =
+ '<h4 class="title-table">Summary</h4>' +
+ '<table id="sql-summary-table" class="table table-striped compact ' +
+ 'cell-border dataTable" style="width:100%"><thead><tr>' +
+ '<th>Total Queries</th>' +
+ '<th>Average Duration</th>' +
+ '<th>Running Queries</th>' +
+ '<th>Failed Queries</th>' +
+ '<th>Failure Rate</th>' +
+ '</tr></thead><tbody><tr>' +
+ '<td>' + (summary.totalQueries || 0) + '</td>' +
+ '<td>' + formatDurationSql(summary.averageDuration || 0) + '</td>' +
+ '<td>' + (summary.runningQueries || 0) + '</td>' +
+ '<td>' + (summary.failedQueries || 0) + '</td>' +
+ '<td>' + failureRate.toFixed(1) + '%</td>' +
+ '</tr></tbody></table>';
+ }
+
var columns = getSqlTableColumns({ detail: false });
if (groupSubExecEnabled) {
// Trailing "Sub Executions" column matching the SPARK-41752 / 4.1
layout:
@@ -80,7 +104,10 @@ $(document).ready(function () {
}
d.groupSubExecution = groupSubExecEnabled ? "true" : "false";
},
- dataSrc: function (json) { return json.aaData; },
+ dataSrc: function (json) {
+ renderSummary(json.summary);
+ return json.aaData;
+ },
error: function () {
$("#sql-table_processing").css("display", "none");
}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
index 5801aba8c769..2f120731ca20 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
@@ -39,6 +39,9 @@ private[ui] class AllExecutionsPage(parent: SQLTab) extends
WebUIPage("") {
<span>
<div id="group-sub-exec-config" style="display:none"
data-value={groupSubExec.toString}></div>
+ <div id="sql-summary-bar" class="mb-3">
+ {spinner}
+ </div>
<div id="sql-executions-table">
{spinner}
</div>
diff --git
a/sql/core/src/main/scala/org/apache/spark/status/api/v1/sql/SqlResource.scala
b/sql/core/src/main/scala/org/apache/spark/status/api/v1/sql/SqlResource.scala
index 59a1264993e1..b2c734ec27ac 100644
---
a/sql/core/src/main/scala/org/apache/spark/status/api/v1/sql/SqlResource.scala
+++
b/sql/core/src/main/scala/org/apache/spark/status/api/v1/sql/SqlResource.scala
@@ -134,6 +134,7 @@ private[v1] class SqlResource extends BaseAppResource {
} else {
(filteredExecs, Map.empty[Long, Seq[SQLExecutionUIData]])
}
+ val summary = SqlResource.executionSummary(rootRows)
// Sort
val sortCol = Option(uriParams.getFirst("order[0][column]"))
@@ -182,6 +183,7 @@ private[v1] class SqlResource extends BaseAppResource {
ret.put("aaData", aaData)
ret.put("recordsTotal", java.lang.Long.valueOf(recordsTotal))
ret.put("recordsFiltered", java.lang.Long.valueOf(recordsFiltered))
+ ret.put("summary", summary)
ret
}
}
@@ -315,6 +317,30 @@ private[v1] class SqlResource extends BaseAppResource {
private[v1] object SqlResource {
+ def executionSummary(execs: Seq[SQLExecutionUIData]):
java.util.LinkedHashMap[String, Object] = {
+ val totalQueries = execs.size
+ val runningQueries = execs.count(_.executionStatus == "RUNNING")
+ val failedQueries = execs.count(_.executionStatus == "FAILED")
+ val now = System.currentTimeMillis()
+ val totalDuration = execs.foldLeft(0L) { case (sum, exec) =>
+ sum + (exec.completionTime.map(_.getTime).getOrElse(now) -
exec.submissionTime)
+ }
+ val averageDuration = if (totalQueries > 0) totalDuration / totalQueries
else 0L
+ val failureRate = if (totalQueries > 0) {
+ failedQueries.toDouble / totalQueries.toDouble
+ } else {
+ 0.0
+ }
+
+ val summary = new java.util.LinkedHashMap[String, Object]()
+ summary.put("totalQueries", java.lang.Long.valueOf(totalQueries))
+ summary.put("averageDuration", java.lang.Long.valueOf(averageDuration))
+ summary.put("runningQueries", java.lang.Long.valueOf(runningQueries))
+ summary.put("failedQueries", java.lang.Long.valueOf(failedQueries))
+ summary.put("failureRate", java.lang.Double.valueOf(failureRate))
+ summary
+ }
+
/**
* Split a set of executions into root rows and a sub-execution map. A root
row is
* either an execution whose id equals its rootExecutionId, or an orphan sub
whose
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
index 635968838406..6c3419f7b10a 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
@@ -65,6 +65,7 @@ abstract class AllExecutionsPageSuite extends
SharedSparkSession with BeforeAndA
val page = new AllExecutionsPage(tab)
val html = page.render(request).toString().toLowerCase(Locale.ROOT)
+ assert(html.contains("sql-summary-bar"))
assert(html.contains("sql-executions-table"))
assert(html.contains("sql-table-utils.js"))
assert(html.contains("allexecutionspage.js"))
diff --git
a/sql/core/src/test/scala/org/apache/spark/status/api/v1/sql/SqlResourceWithActualMetricsSuite.scala
b/sql/core/src/test/scala/org/apache/spark/status/api/v1/sql/SqlResourceWithActualMetricsSuite.scala
index f03aff39b532..e7072a1c0d28 100644
---
a/sql/core/src/test/scala/org/apache/spark/status/api/v1/sql/SqlResourceWithActualMetricsSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/status/api/v1/sql/SqlResourceWithActualMetricsSuite.scala
@@ -180,10 +180,16 @@ class SqlResourceWithActualMetricsSuite
val recordsTotal = (json \ "recordsTotal").extract[Long]
val recordsFiltered = (json \ "recordsFiltered").extract[Long]
val aaData = (json \ "aaData").children
+ val summary = json \ "summary"
assert(draw === 1, "draw should be echoed back")
assert(recordsTotal > 0, "should have some executions")
assert(recordsFiltered === recordsTotal, "no filter applied")
assert(aaData.size <= 5, "should respect page length")
+ assert((summary \ "totalQueries").extract[Long] === recordsTotal)
+ assert((summary \ "averageDuration").extract[Long] >= 0)
+ assert((summary \ "runningQueries").extract[Long] >= 0)
+ assert((summary \ "failedQueries").extract[Long] >= 0)
+ assert((summary \ "failureRate").extract[Double] >= 0.0)
// Verify row data fields
val firstRow = aaData.head
@@ -237,11 +243,14 @@ class SqlResourceWithActualMetricsSuite
val groupedJson = JsonMethods.parse(groupedOpt.get)
val groupedRecordsTotal = (groupedJson \ "recordsTotal").extract[Long]
val groupedRecordsFiltered = (groupedJson \
"recordsFiltered").extract[Long]
+ val groupedSummary = groupedJson \ "summary"
val groupedRows = (groupedJson \ "aaData").children
assert(groupedRecordsTotal === groupedRows.size,
"with no filter, recordsTotal should match returned root count")
assert(groupedRecordsFiltered === groupedRows.size,
"with no filter, recordsFiltered should match returned root count")
+ assert((groupedSummary \ "totalQueries").extract[Long] ===
groupedRecordsFiltered,
+ "grouped summary should count root rows only")
// Every row in grouped mode is either a true root (id ==
rootExecutionId)
// or an orphan sub whose real parent is absent from the result set.
val visibleIds = groupedRows.map(r => (r \ "id").extract[Long]).toSet
@@ -272,9 +281,13 @@ class SqlResourceWithActualMetricsSuite
val (flatCode, flatOpt, _) = getContentAndCode(flatUrl)
assert(flatCode === HttpServletResponse.SC_OK)
val flatJson = JsonMethods.parse(flatOpt.get)
+ val flatRecordsFiltered = (flatJson \ "recordsFiltered").extract[Long]
+ val flatSummary = flatJson \ "summary"
val flatRows = (flatJson \ "aaData").children
assert(flatRows.size > groupedRows.size,
"flat listing should contain at least one extra sub-execution row")
+ assert((flatSummary \ "totalQueries").extract[Long] ===
flatRecordsFiltered,
+ "flat summary should count all execution rows")
val embeddedSubs = groupedRows.map(r => (r \
"subExecutions").children.size).sum
assert(flatRows.size === groupedRows.size + embeddedSubs,
"flat size should equal grouped roots plus embedded sub rows")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]