Spark 案例类 - 十进制类型编码器错误“无法从十进制向上转换”
Posted
技术标签:
【中文标题】Spark 案例类 - 十进制类型编码器错误“无法从十进制向上转换”【英文标题】:Spark case class - decimal type encoder error "Cannot up cast from decimal" 【发布时间】:2016-12-03 20:59:58 【问题描述】:我正在从 mysql/MariaDB 中提取数据,在创建数据集期间,数据类型出现错误
线程“主”org.apache.spark.sql.AnalysisException 中的异常: 不能将
AMOUNT
从十进制(30,6)向上转换为十进制(38,18),因为它可能 truncate 目标对象的类型路径为: - 字段(类:“org.apache.spark.sql.types.Decimal”,名称:“AMOUNT”) - 根类:“com.misp.spark.Deal”您可以向输入数据添加显式转换或选择更高精度的字段类型 在目标对象中;
案例类是这样定义的
case class
(
AMOUNT: Decimal
)
有谁知道如何修复它而不去修改数据库?
【问题讨论】:
【参考方案1】:该错误表示 apache spark 无法自动将 BigDecimal(30,6) 从数据库转换为 Dataset 中想要的 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]
它应该可以解决阅读问题(但我猜不是写作问题)
【讨论】:
这个解决方法非常方便,谢谢。我在数据库端使用了一个视图,将所有十进制列转换为 (38,18)。是的,我打开了这个错误。【参考方案2】:如前所述,由于您的数据库使用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 个。
这样做的方法是在将任何内容转换为 Dataset 之前,在 DataFrame 上运行以下操作:
val downCastableData =
originalData.withColumn("amount", $"amount".cast(DecimalType(26,6)))
然后转换为Dataset
应该可以工作。
(实际上,您可以转换为 (20 left, 6 right)
或以下的任何内容,例如 (19 left, 5 right)
等...)。
【讨论】:
【参考方案3】:虽然我在这里没有解决方案,但我对正在发生的事情的理解:
默认情况下,spark 将推断case class
中Decimal
类型(或BigDecimal
)的架构为DecimalType(38, 18)
(参见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
类型时,可能会有一些值被截断。
不幸的是,从 scala case class
推断模式被 spark 开发人员认为是一种方便,他们选择不支持允许程序员在 case class
中指定 Decimal
或 BigDecimal
类型的精度和比例(见https://issues.apache.org/jira/browse/SPARK-18484)
【讨论】:
如此令人沮丧的火花开发人员只提供了十进制类型的错觉——如果它根本不存在就更好了。 :( 感谢张贴这张票 - 帮助我停止寻找一种方法来完成这项工作。【参考方案4】:基于@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]
【讨论】:
【参考方案5】:根据 pyspark,Decimal(38,18)
是默认值。
创建 DecimalType 时,默认精度和小数位数为 (10, 0)。从 decimal.Decimal 对象推断架构时,它将是 DecimalType(38, 18)。
【讨论】:
以上是关于Spark 案例类 - 十进制类型编码器错误“无法从十进制向上转换”的主要内容,如果未能解决你的问题,请参考以下文章
为啥使用案例类在 DataFrame 上映射失败并显示“无法找到存储在数据集中的类型的编码器”?