This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 6a0e671b7672 [SPARK-57937][SQL] Improve decimal division performance
by dividing at the result scale
6a0e671b7672 is described below
commit 6a0e671b7672982dd4138c368ea71660ca70e604
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Sun Jul 5 12:19:19 2026 -0700
[SPARK-57937][SQL] Improve decimal division performance by dividing at the
result scale
### What changes were proposed in this pull request?
This PR changes decimal `Divide` to perform the division directly at the
result scale, instead of dividing at `DecimalType.MAX_SCALE + 1` and then
rounding down to the result scale.
- `Decimal` gets a new method `div(that: Decimal, scale: Int)` that divides
with `RoundingMode.HALF_UP` at the given scale.
- `DivModLike` codegen gains an overridable `decimalOperation` hook
(default: existing `decimalMethod` behavior), and `Divide` overrides it — both
the codegen and the interpreted eval now call `div(b, resultScale)` followed by
the existing overflow check. `IntegralDivide`, `Remainder`, and `Pmod` are
unchanged.
Previously, `Decimal./` always divided at scale `39` with `ROUND_DOWN` and
the subsequent `changePrecision` performed a second division to reach the
result scale. This is equivalent to a single `HALF_UP` division at the result
scale: truncating at 39 fractional digits cannot change a `HALF_UP` rounding
decision at any scale `<= 38`, because every half-way point at scale `s <= 38`
is exactly representable within 39 fractional digits.
### Why are the changes needed?
The fixed scale-39 intermediate makes decimal division needlessly
expensive, especially for small decimal types. For example, `decimal(7,2) /
decimal(7,2)` (result `decimal(17,10)`) produces a scale-39 intermediate whose
unscaled value exceeds the `Long` range, forcing two `BigInteger` (Knuth)
divisions per row — one inside `BigDecimal.divide` and another in
`CheckOverflow`'s rescale, visible in profiles as:
```
java.math.MutableBigInteger.divideMagnitude
java.math.BigDecimal.setScale
org.apache.spark.sql.types.Decimal.changePrecision
org.apache.spark.sql.types.Decimal.toPrecision
```
Dividing once at the result scale keeps the computation in the compact
`long` representation whenever the quotient fits.
### Does this PR introduce _any_ user-facing change?
- Query results are unchanged.
- The only difference is that the `NUMERIC_VALUE_OUT_OF_RANGE` error
message on division overflow now renders the quotient rounded at the result
scale instead of the internal scale-39 intermediate, e.g.
`1000000073899961059796.725866331521039185` instead of
`1000000073899961059796.725866331521039184725213147467478092333`. The error
class, parameters, and whether an error occurs are all unchanged.
### How was this patch tested?
Pass the CIs.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Fable 5
Closes #57006 from dongjoon-hyun/SPARK-57937.
Authored-by: Dongjoon Hyun <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
---
.../scala/org/apache/spark/sql/types/Decimal.scala | 9 ++++++++
.../sql/catalyst/expressions/arithmetic.scala | 23 ++++++++++++------
.../org/apache/spark/sql/types/DecimalSuite.scala | 27 ++++++++++++++++++++++
.../DecimalDivideBenchmark-jdk21-results.txt | 16 ++++++-------
.../DecimalDivideBenchmark-jdk25-results.txt | 20 ++++++++--------
.../benchmarks/DecimalDivideBenchmark-results.txt | 16 ++++++-------
.../results/decimalArithmeticOperations.sql.out | 12 +++++-----
7 files changed, 84 insertions(+), 39 deletions(-)
diff --git a/sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala
b/sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala
index bd94c386ab53..a41847f02c2a 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala
@@ -530,6 +530,15 @@ final class Decimal extends Ordered[Decimal] with
Serializable {
.divide(that.toJavaBigDecimal, DecimalType.MAX_SCALE + 1,
MATH_CONTEXT.getRoundingMode))
}
+ /**
+ * Divides `this` by `that` rounding the result to `scale` fractional digits
with HALF_UP.
+ * Returns the same result as `this / that` followed by rounding to `scale`,
but with a single
+ * division, which stays in the compact representation when the quotient
fits in a Long.
+ */
+ def div(that: Decimal, scale: Int): Decimal =
+ if (that.isZero) null
+ else Decimal(toJavaBigDecimal.divide(that.toJavaBigDecimal, scale,
RoundingMode.HALF_UP))
+
def %(that: Decimal): Decimal =
if (that.isZero) null
else Decimal(toJavaBigDecimal.remainder(that.toJavaBigDecimal,
MATH_CONTEXT))
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/arithmetic.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/arithmetic.scala
index 88b409c4e867..9c72f822143f 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/arithmetic.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/arithmetic.scala
@@ -692,6 +692,10 @@ trait DivModLike extends BinaryArithmetic {
def evalOperation(left: Any, right: Any): Any
+ /** Java code computing the decimal result that is fed into the overflow
check. */
+ protected def decimalOperation(eval1: String, eval2: String, scale: Int):
String =
+ s"$eval1.$decimalMethod($eval2)"
+
/**
* Special case handling due to division/remainder by 0 => null or
ArithmeticException.
*/
@@ -724,14 +728,14 @@ trait DivModLike extends BinaryArithmetic {
if (failOnError) {
s"""
|Decimal $decimalValue = $castUtils.changePrecisionExact(
- | ${eval1.value}.$decimalMethod(${eval2.value}), $precision,
$scale,
+ | ${decimalOperation(eval1.value, eval2.value, scale)},
$precision, $scale,
| $errorContextCode);
|${ev.value} = ${decimalToDataTypeCodeGen(s"$decimalValue")};
|""".stripMargin
} else {
s"""
|Decimal $decimalValue = $castUtils.changePrecisionOrNull(
- | ${eval1.value}.$decimalMethod(${eval2.value}), $precision,
$scale);
+ | ${decimalOperation(eval1.value, eval2.value, scale)},
$precision, $scale);
|if ($decimalValue != null) {
| ${ev.value} = ${decimalToDataTypeCodeGen(s"$decimalValue")};
|} else {
@@ -860,13 +864,18 @@ case class Divide(
}
}
+ // Dividing directly at the result scale returns the same result as dividing
at
+ // `DecimalType.MAX_SCALE + 1` and then rounding to the result scale, but
with a single
+ // division that stays in the compact representation when the quotient fits
in a Long.
+ override protected def decimalOperation(eval1: String, eval2: String, scale:
Int): String =
+ s"$eval1.div($eval2, $scale)"
+
private lazy val div: (Any, Any) => Any = dataType match {
- case d @ DecimalType.Fixed(precision, scale) =>
- val fractional = PhysicalDecimalType(precision, scale).fractional
+ case DecimalType.Fixed(precision, scale) =>
(l, r) => {
- val value = fractional.asInstanceOf[Fractional[Any]].div(l, r)
- checkDecimalOverflow(value.asInstanceOf[Decimal], precision, scale)
- }
+ val value = l.asInstanceOf[Decimal].div(r.asInstanceOf[Decimal], scale)
+ checkDecimalOverflow(value, precision, scale)
+ }
case ft: FractionalType => PhysicalFractionalType.fractional(ft).div
}
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/types/DecimalSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/types/DecimalSuite.scala
index 4af2c8367819..080c6693280a 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/types/DecimalSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/types/DecimalSuite.scala
@@ -258,6 +258,33 @@ class DecimalSuite extends SparkFunSuite with
PrivateMethodTester with SQLHelper
assert(Decimal(100).quot(Decimal(-33)) === Decimal(BigDecimal("-3")))
}
+ test("SPARK-57937: div with target scale") {
+ // `div(that, scale)` must return the same result as `/` (which divides at
+ // `DecimalType.MAX_SCALE + 1` with ROUND_DOWN) followed by rounding to
the same scale.
+ def checkDiv(a: Decimal, b: Decimal, scale: Int): Unit = {
+ val expected = (a / b).toPrecision(DecimalType.MAX_PRECISION, scale)
+ val actual = a.div(b, scale)
+ assert(actual.toJavaBigDecimal.compareTo(expected.toJavaBigDecimal) ===
0,
+ s"$a.div($b, $scale)")
+ }
+ assert(Decimal(100).div(Decimal(0), 10) === null)
+ // Half-way cases, both signs.
+ checkDiv(Decimal("1.00"), Decimal("8.00"), 2)
+ checkDiv(Decimal("-1.00"), Decimal("8.00"), 2)
+ checkDiv(Decimal("1.05"), Decimal("-2.00"), 3)
+ // Non-terminating expansions.
+ checkDiv(Decimal("1.00"), Decimal("3.00"), 10)
+ checkDiv(Decimal("-2.00"), Decimal("7.00"), 38)
+ val rnd = new scala.util.Random(42)
+ for (_ <- 1 to 1000) {
+ // decimal(7, 2) operands, rounded to the scale of the decimal(17, 10)
result type.
+ val a = Decimal(rnd.nextInt(19999999) - 9999999, 7, 2)
+ val b = Decimal(rnd.nextInt(9999998) + 1, 7, 2)
+ checkDiv(a, b, 10)
+ checkDiv(a, -b, 10)
+ }
+ }
+
test("negate & abs") {
assert(-Decimal(100) === Decimal(BigDecimal("-100")))
assert(-Decimal(-100) === Decimal(BigDecimal("100")))
diff --git a/sql/core/benchmarks/DecimalDivideBenchmark-jdk21-results.txt
b/sql/core/benchmarks/DecimalDivideBenchmark-jdk21-results.txt
index b449110256c4..c58728c42f81 100644
--- a/sql/core/benchmarks/DecimalDivideBenchmark-jdk21-results.txt
+++ b/sql/core/benchmarks/DecimalDivideBenchmark-jdk21-results.txt
@@ -6,18 +6,18 @@ OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux
6.17.0-1018-azure
AMD EPYC 7763 64-Core Processor
decimal(7,2) / decimal(7,2): Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------------------------------
-a / b (ansi=false) 7797 7858
37 1.3 779.7 1.0X
-a / b (ansi=true) 7886 7904
18 1.3 788.6 1.0X
-a / 2 (ansi=false) 5524 5553
18 1.8 552.4 1.4X
-a / 2 (ansi=true) 5476 5500
24 1.8 547.6 1.4X
+a / b (ansi=false) 4776 4837
90 2.1 477.6 1.0X
+a / b (ansi=true) 4813 4824
9 2.1 481.3 1.0X
+a / 2 (ansi=false) 2799 2828
21 3.6 279.9 1.7X
+a / 2 (ansi=true) 2739 2764
17 3.7 273.9 1.7X
OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
AMD EPYC 7763 64-Core Processor
decimal(38,18) / decimal(38,18): Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------------------------------
-a / b (ansi=false) 14130 14148
11 0.7 1413.0 1.0X
-a / b (ansi=true) 14138 14146
6 0.7 1413.8 1.0X
-a / 2 (ansi=false) 8607 8628
27 1.2 860.7 1.6X
-a / 2 (ansi=true) 8579 8623
26 1.2 857.9 1.6X
+a / b (ansi=false) 11056 11066
7 0.9 1105.6 1.0X
+a / b (ansi=true) 11067 11100
22 0.9 1106.7 1.0X
+a / 2 (ansi=false) 5969 5986
15 1.7 596.9 1.9X
+a / 2 (ansi=true) 5975 5984
13 1.7 597.5 1.9X
diff --git a/sql/core/benchmarks/DecimalDivideBenchmark-jdk25-results.txt
b/sql/core/benchmarks/DecimalDivideBenchmark-jdk25-results.txt
index 50913d8981d7..3281042b958a 100644
--- a/sql/core/benchmarks/DecimalDivideBenchmark-jdk25-results.txt
+++ b/sql/core/benchmarks/DecimalDivideBenchmark-jdk25-results.txt
@@ -3,21 +3,21 @@ Decimal division
================================================================================================
OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V45 96-Core Processor
+AMD EPYC 7763 64-Core Processor
decimal(7,2) / decimal(7,2): Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------------------------------
-a / b (ansi=false) 3110 3121
14 3.2 311.0 1.0X
-a / b (ansi=true) 3023 3070
36 3.3 302.3 1.0X
-a / 2 (ansi=false) 2088 2111
26 4.8 208.8 1.5X
-a / 2 (ansi=true) 2039 2065
30 4.9 203.9 1.5X
+a / b (ansi=false) 2792 2826
23 3.6 279.2 1.0X
+a / b (ansi=true) 2767 2781
15 3.6 276.7 1.0X
+a / 2 (ansi=false) 1773 1788
10 5.6 177.3 1.6X
+a / 2 (ansi=true) 1770 1778
7 5.7 177.0 1.6X
OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V45 96-Core Processor
+AMD EPYC 7763 64-Core Processor
decimal(38,18) / decimal(38,18): Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------------------------------
-a / b (ansi=false) 5557 5611
43 1.8 555.7 1.0X
-a / b (ansi=true) 5383 5437
43 1.9 538.3 1.0X
-a / 2 (ansi=false) 3275 3313
32 3.1 327.5 1.7X
-a / 2 (ansi=true) 3025 3082
35 3.3 302.5 1.8X
+a / b (ansi=false) 6595 6605
7 1.5 659.5 1.0X
+a / b (ansi=true) 6611 6629
11 1.5 661.1 1.0X
+a / 2 (ansi=false) 3373 3438
36 3.0 337.3 2.0X
+a / 2 (ansi=true) 3468 3499
19 2.9 346.8 1.9X
diff --git a/sql/core/benchmarks/DecimalDivideBenchmark-results.txt
b/sql/core/benchmarks/DecimalDivideBenchmark-results.txt
index ea91d089f6c4..e256bd58163f 100644
--- a/sql/core/benchmarks/DecimalDivideBenchmark-results.txt
+++ b/sql/core/benchmarks/DecimalDivideBenchmark-results.txt
@@ -6,18 +6,18 @@ OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux
6.17.0-1018-azure
AMD EPYC 7763 64-Core Processor
decimal(7,2) / decimal(7,2): Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------------------------------
-a / b (ansi=false) 9214 9273
35 1.1 921.4 1.0X
-a / b (ansi=true) 9287 9306
19 1.1 928.7 1.0X
-a / 2 (ansi=false) 6273 6296
21 1.6 627.3 1.5X
-a / 2 (ansi=true) 6257 6262
4 1.6 625.7 1.5X
+a / b (ansi=false) 6172 6204
32 1.6 617.2 1.0X
+a / b (ansi=true) 6088 6120
46 1.6 608.8 1.0X
+a / 2 (ansi=false) 3525 3541
17 2.8 352.5 1.8X
+a / 2 (ansi=true) 3574 3592
24 2.8 357.4 1.7X
OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
AMD EPYC 7763 64-Core Processor
decimal(38,18) / decimal(38,18): Best Time(ms) Avg Time(ms)
Stdev(ms) Rate(M/s) Per Row(ns) Relative
------------------------------------------------------------------------------------------------------------------------
-a / b (ansi=false) 15889 15898
9 0.6 1588.9 1.0X
-a / b (ansi=true) 15872 15896
18 0.6 1587.2 1.0X
-a / 2 (ansi=false) 9324 9341
13 1.1 932.4 1.7X
-a / 2 (ansi=true) 9307 9345
23 1.1 930.7 1.7X
+a / b (ansi=false) 12420 12435
17 0.8 1242.0 1.0X
+a / b (ansi=true) 12499 12578
90 0.8 1249.9 1.0X
+a / 2 (ansi=false) 6676 6691
9 1.5 667.6 1.9X
+a / 2 (ansi=true) 6470 6510
23 1.5 647.0 1.9X
diff --git
a/sql/core/src/test/resources/sql-tests/results/decimalArithmeticOperations.sql.out
b/sql/core/src/test/resources/sql-tests/results/decimalArithmeticOperations.sql.out
index 5110af2189cd..ddc7111dc6f6 100644
---
a/sql/core/src/test/resources/sql-tests/results/decimalArithmeticOperations.sql.out
+++
b/sql/core/src/test/resources/sql-tests/results/decimalArithmeticOperations.sql.out
@@ -248,7 +248,7 @@ org.apache.spark.SparkArithmeticException
"config" : "\"spark.sql.ansi.enabled\"",
"precision" : "38",
"scale" : "6",
- "value" :
"1000000000000000000000000000000000000.000000000000000000000000000000000000000"
+ "value" : "1000000000000000000000000000000000000.000000"
},
"queryContext" : [ {
"objectType" : "",
@@ -583,7 +583,7 @@ org.apache.spark.SparkArithmeticException
"config" : "\"spark.sql.ansi.enabled\"",
"precision" : "38",
"scale" : "3",
- "value" :
"1000000000000000000000000000000000000.000000000000000000000000000000000000000"
+ "value" : "1000000000000000000000000000000000000.000"
},
"queryContext" : [ {
"objectType" : "",
@@ -683,7 +683,7 @@ org.apache.spark.SparkArithmeticException
"config" : "\"spark.sql.ansi.enabled\"",
"precision" : "38",
"scale" : "18",
- "value" : "1000000073899961059796.725866331521039184725213147467478092333"
+ "value" : "1000000073899961059796.725866331521039185"
},
"queryContext" : [ {
"objectType" : "",
@@ -708,7 +708,7 @@ org.apache.spark.SparkArithmeticException
"config" : "\"spark.sql.ansi.enabled\"",
"precision" : "38",
"scale" : "2",
- "value" :
"10123456789012345678901234567890123456.000000000000000000000000000000000000000"
+ "value" : "10123456789012345678901234567890123456.00"
},
"queryContext" : [ {
"objectType" : "",
@@ -733,7 +733,7 @@ org.apache.spark.SparkArithmeticException
"config" : "\"spark.sql.ansi.enabled\"",
"precision" : "38",
"scale" : "3",
- "value" :
"101234567890123456789012345678901234.560000000000000000000000000000000000000"
+ "value" : "101234567890123456789012345678901234.560"
},
"queryContext" : [ {
"objectType" : "",
@@ -790,7 +790,7 @@ org.apache.spark.SparkArithmeticException
"config" : "\"spark.sql.ansi.enabled\"",
"precision" : "38",
"scale" : "6",
- "value" :
"101234567890123456789012345678901.234560000000000000000000000000000000000"
+ "value" : "101234567890123456789012345678901.234560"
},
"queryContext" : [ {
"objectType" : "",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]