Spark读取Oracle表遇Decimal精度超限异常,求助排查
问题描述
我需要读取一张包含3500万行、上百列的Oracle表,使用以下Spark JDBC代码:
extractDF = spark.read \ .format("jdbc") \ .option("dbTable", f""" select tt.* from ( select {ORIGIN_COLUMNS} from {ORIGIN_SCHEMA}.{ORIGIN_TABLE} t ) tt where tt.{ORIGIN_REF_COLUMN}<={int(origin_ref_column_value_max)} """) \ .option("driver", "oracle.jdbc.driver.OracleDriver" ) \ .option("url", ORACLE_URL) \ .option("user", ORACLE_USER) \ .option("password", ORACLE_PASSWORD) \ .option("fetchsize",f"{CONTROL_FETCH_SIZE}") \ .option("batchsize",f"{CONTROL_BATCH_SIZE}") \ .option("partitionColumn", ORIGIN_REF_COLUMN) \ .option("numPartitions", f"{numPartitions}") \ .option("lowerBound", f"{int(origin_ref_column_value_min)}") \ .option("upperBound", f"{int(origin_ref_column_value_max)+1}") \ .load() extractDF.collect() # --> Raise SparkArithmeticException: [DECIMAL_PRECISION_EXCEEDS_MAX_PRECISION] Decimal precision 31 exceeds max precision 11
表结构包含多个decimal类型列,示例如下:
root |-- COD_1: decimal(10,0) (nullable = true) |-- COD_2: decimal(11,2) (nullable = true) |-- COD_3: decimal(30,2) (nullable = true) |-- COD_4: decimal(7,2) (nullable = true)
我尝试将所有decimal类型转为Decimal(38,原小数位数),代码如下,但执行extractDF.collect()仍报相同错误:
decimalCols = [it for it in list(filter(lambda i: 'decimal' in i[1], extractDF.dtypes))] for c in decimalCols: colScale = re.sub(r"decimal\(\d+,(\d+)\)", r"\1", c[1]) extractDF = extractDF.withColumn(c[0], col(c[0]).cast(DecimalType(38, int(colScale))))
另外发现奇怪现象:rownum<42k时执行正常,rownum<44k时报错,而rownum在42k到44k之间时执行正常。环境为Spark 3.3.2、Scala 2.12、ojdbc8 23.2.0.0。
排查方案
1. 检查分区列的类型与精度冲突
你使用ORIGIN_REF_COLUMN作为分区列,且代码中对其做了int()转换。如果该列实际是Oracle的NUMBER类型且精度超过11(比如NUMBER(31,0)),Spark在分区计算时会尝试将其转为默认精度11的Decimal类型,直接触发溢出。
- 验证分区列的Oracle原生类型:执行SQL查询
SELECT DATA_TYPE, DATA_PRECISION, DATA_SCALE FROM ALL_TAB_COLUMNS WHERE TABLE_NAME='{ORIGIN_TABLE}' AND COLUMN_NAME='{ORIGIN_REF_COLUMN}' - 调整分区列处理逻辑:要么在JDBC查询中显式转换分区列精度(
CAST({ORIGIN_REF_COLUMN} AS DECIMAL(38,0)) AS {ORIGIN_REF_COLUMN}),要么设置Spark参数spark.sql.decimalOperations.allowPrecisionLoss=true允许精度损失。
2. 提前指定列类型,避免Spark自动推断错误
当前的类型转换是在DataFrame加载后执行,但报错发生在collect()阶段,说明数据在JDBC读取时就已被错误解析为低精度Decimal。需要在读取阶段直接定义列类型:
- 使用
customSchema选项显式定义所有列类型:custom_schema = "COD_1 DECIMAL(10,0), COD_2 DECIMAL(11,2), COD_3 DECIMAL(38,2), ..." # 补全所有列定义 extractDF = spark.read \ .format("jdbc") \ .option("dbTable", "...") \ .option("customSchema", custom_schema) \ # 保留其他JDBC选项 .load() - 或者在Oracle查询中直接转换Decimal列:
SELECT CAST(COD_3 AS NUMBER(38,2)) COD_3, ... FROM ...
3. 定位特定行的异常数据
根据rownum的奇怪表现,说明42k到44k之间存在特殊数据(比如超出列定义精度的数值、科学计数法表示的值),导致Spark解析时临时提升精度触发报错。
- 定位异常行:执行Oracle查询
检查所有Decimal列的实际值,尤其是SELECT * FROM (SELECT t.*, ROWNUM rn FROM {ORIGIN_SCHEMA}.{ORIGIN_TABLE} t) WHERE rn BETWEEN 42000 AND 44000COD_3这类高精度列是否存在超出DECIMAL(30,2)的数值(比如超过9999999999999999999999999999.99的数值)。 - 确认Oracle列的真实精度:执行
SELECT COLUMN_NAME, DATA_PRECISION, DATA_SCALE FROM ALL_TAB_COLUMNS WHERE TABLE_NAME='{ORIGIN_TABLE}',排查列定义是否被修改过。
4. 调整JDBC驱动与Spark兼容性参数
ojdbc8 23.2.0.0属于较新版本,可能与Spark 3.3.2存在兼容性问题:
- 添加JDBC选项
oracle.jdbc.mapDateToTimestamp=false,避免日期类型转换间接影响Decimal解析。 - 降低ojdbc版本至21.11.0.0(Spark 3.3.x推荐的稳定兼容版本),验证是否解决精度解析问题。
- 显式设置
spark.sql.decimal.maxPrecision=38,确保Spark启用最高精度的Decimal支持(默认值为38,显式配置可排除参数异常)。
5. 避免用collect()触发全量数据加载
collect()会将所有数据拉到Driver端,容易放大内存和类型解析问题,建议先通过采样或分区遍历验证:
# 采样部分数据验证类型转换效果 sampleDF = extractDF.sample(fraction=0.001).cache() sampleDF.printSchema() sampleDF.show() # 按分区遍历处理,定位异常分区 for i in range(numPartitions): partitionDF = extractDF.filter(spark_partition_id() == i) try: partitionDF.write.mode("append").parquet("/tmp/test_partition") except Exception as e: print(f"Partition {i} error: {e}")
内容的提问来源于stack exchange,提问作者Barenko
相关产品推荐
相关产品推荐

