>我正在从MySQL/MariaDB中提取数据,在创建数据集期间,数据类型发生错误
线程"main"中的异常 org.apache.spark.sql.AnalysisException: 无法将
AMOUNT
从十进制 (30,6) 向上转换为十进制 (38,18) 截断 目标对象的类型路径为: - 字段(类:"org.apache.spark.sql.types.Decimal",名称:"AMOUNT") - 根类:"com.misp.spark.Deal" 您可以向输入数据添加显式强制转换,也可以选择更高精度的字段类型 在目标对象中;
案例类定义如下
case class
(
AMOUNT: Decimal
)
有人知道如何修复它而不接触数据库吗?
该错误表明 apache spark 无法自动将 BigDecimal(30,6) 从数据库转换为数据集中想要的 BigDecimal(38,18)(我不知道为什么它需要固定参数 38,18。更奇怪的是,spark不能自动将低精度的类型转换为高精度的类型)。
报告了一个错误:https://issues.apache.org/jira/browse/SPARK-20162(也许是你)。无论如何,我找到了很好的解决方法,可以通过将数据帧中的列转换为 BigDecimal(38,18),然后将数据帧转换为数据集来读取数据。
//first read data to dataframe with any way suitable for you
var df: DataFrame = ???
val dfSchema = df.schema
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types.DecimalType
dfSchema.foreach { field =>
field.dataType match {
case t: DecimalType if t != DecimalType(38, 18) =>
df = df.withColumn(field.name, col(field.name).cast(DecimalType(38,18)))
}
}
df.as[YourCaseClassWithBigDecimal]
它应该解决阅读问题(但我想不是写作问题)
如前所述,由于您的数据库使用DecimalType(30,6)
这意味着您总共有 30 个插槽和 6 个超过小数点的插槽,这为小数点前面的区域留下了30-6=24
。我喜欢称它为大十进制(24 left, 6 right)
。这当然不适合(20 left, 18 right)
(即DecimalType(38,18)
),因为后者在左侧没有足够的插槽(需要 20 与 24)。我们DecimalType(38,18)
只有 20 个左插槽,但我们需要 24 个左插槽来容纳您的DecimalType(30,6)
。
我们在这里能做的是将(24 left, 6 right)
向下投射到(20 left, 6 right)
(即DecimalType(26,6)
),以便在它被自动转换为(20 left, 18 right)
(即DecimalType(38,18)
)双方都将适合。您的DecimalType(26,6)
将有 20 个左插槽,使其可以放入DecimalType(38,18)
内,当然 6 个右侧插槽将适合 18 个。
执行此操作的方法是在将任何内容转换为数据集之前,对数据帧运行以下操作:
val downCastableData =
originalData.withColumn("amount", $"amount".cast(DecimalType(26,6)))
然后转换为Dataset
应该有效。
(实际上,您可以投射到任何(20 left, 6 right)
或更少的内容,例如(19 left, 5 right)
等等...
虽然我没有解决方案,但这是我对正在发生的事情的理解:
默认情况下,spark 将推断要DecimalType(38, 18)
case class
中Decimal
类型(或BigDecimal
)的模式(请参阅org.apache.spark.sql.types.DecimalType.SYSTEM_DEFAULT
)。38表示Decimal
总共可以容纳38位数字(小数点的左侧和右侧),而18表示这38位数字中的18位保留在小数点的右侧。这意味着Decimal(38, 18)
的小数点左侧可能有20位数字。您的 MySQL 架构是decimal(30, 6)
的,这意味着它可能包含小数点左侧有24位 (30 - 6) 和小数点右侧有6位数字的值。由于24位数字大于20位,因此从 MySQL 架构转换为该Decimal
类型的值可能会被截断。
不幸的是,从 scalacase class
推断模式被 Spark 开发人员认为是一种方便,他们选择不支持允许程序员为case class
中的Decimal
或BigDecimal
类型指定精度和小数位数(参见 https://issues.apache.org/jira/browse/SPARK-18484)
基于@user2737635的答案,您可以使用foldLeft
而不是foreach
来避免将数据集定义为var
并重新定义它:
//first read data to dataframe with any way suitable for you
val df: DataFrame = ???
val dfSchema = df.schema
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types.DecimalType
dfSchema.foldLeft(df){
(dataframe, field) => field.dataType match {
case t: DecimalType if t != DecimalType(38, 18) => dataframe.withColumn(field.name, col(field.name).cast(DecimalType(38, 18)))
case _ => dataframe
}
}.as[YourCaseClassWithBigDecimal]
我们正在通过定义我们在调用站点.as
中使用的自己的Encoder
来解决此问题。我们使用知道正确精度和小数位数的StructType
生成Encoder
(代码请参见下面的链接)。
https://issues.apache.org/jira/browse/SPARK-27339
根据 pyspark 的说法,Decimal(38,18)
是默认的。
创建 DecimalType 时,默认精度和小数位数为 (10, 0)。从十进制推断架构时。十进制对象,它将是 DecimalType(38, 18)。