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 classDecimal 类型(或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 中指定 DecimalBigDecimal 类型的精度和比例(见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 案例类 - 十进制类型编码器错误“无法从十进制向上转换”的主要内容,如果未能解决你的问题,请参考以下文章

Spark 错误:无法找到存储在数据集中的类型的编码器

为啥使用案例类在 DataFrame 上映射失败并显示“无法找到存储在数据集中的类型的编码器”?

Spark 数据集案例类编码器的小数精度

十进制数据类型无法在 spark 和 Hive 中正确存储值

如何从包含枚举的案例类创建 Spark 数据集或数据框

具有特征的 Spark 2.0 数据集编码器