0
0
0
0
专栏/.../

关于 decimal 精度问题

 weiyinghua  发表于  2025-09-10

一、问题说明

1.1 问题现象

当使用 JAVA 在 TiDB 执行超出 decimal 小数位范围的 prepare 语句时,会返回错误:Cause: java.sql.SQLException: Data truncated for column '%s' at row %d:

把 SQL 手工改成 prepare 语句在 mysql 客户端执行不报错,问题无法复现,必须在应用程序中才能复现。

1.2 环境信息

  • JDK 版本:1.8.0_251
  • TiDB 版本:v8.5.2

1.3 表结构

CREATE TABLE `test_pp` (
  `r_qty` decimal(13,4) DEFAULT NULL
  ... ...
) ;

1.4 报错日志

Data truncated for column '%s' at row %d:

[2025/07/17 20:00:43.701 +08:00] [INFO] [conn.go:1160] ["command dispatched failed"] [conn=2631925850] [session_alias=] [connInfo="id:2631925850, addr:xx.xx.xx.xx:xxxxx status:10, collation:utf8mb4_0900_ai_ci, user:test"] [command=Execute] [status="inTxn:0, autocommit:1"] [sql="insert into test_pp(...] [txn_mode=PESSIMISTIC] [timestamp=0] [err="[types:1265]Data truncated for column '%s' at row %d\ninsert into test_pp(...\ngithub.com/pingcap/tidb/pkg/server.(*clientConn).executePreparedStmtAndWriteResult\n\t/workspace/source/tidb/pkg/server/conn_stmt.go:317\ngithub.com/pingcap/tidb/pkg/server.(*clientConn).executePlanCacheStmt\n\t/workspace/source/tidb/pkg/server/conn_stmt.go:234\ngithub.com/pingcap/tidb/pkg/server.(*clientConn).handleStmtExecute\n\t/workspace/source/tidb/pkg/server/conn_stmt.go:225\ngithub.com/pingcap/tidb/pkg/server.(*clientConn).dispatch\n\t/workspace/source/tidb/pkg/server/conn.go:1384\ngithub.com/pingcap/tidb/pkg/server.(*clientConn).Run\n\t/workspace/source/tidb/pkg/server/conn.go:1127\ngithub.com/pingcap/tidb/pkg/server.(*Server).onConn\n\t/workspace/source/tidb/pkg/server/server.go:740\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1650"]

1.5 原始 SQL

以下 SQL 是复现报错时抓取的完整 SQL。以下 SQL 在 mysql 客户端,无论常规执行或 prepare 方式执行都不会报错;在 JAVA 代码中关闭 prepare 执行不报错,开启 prepare 执行报错,说明问题和 prepare 语句有关。

INSERT INTO test_pp ( r_qty )
VALUES
  (
    0.0001258850097656250000000000000000000000000000000000000000000000000000001,
    ... ....
  )

经过分析我们发现,decimal 类型字段小数位为72位时,JAVA 代码不会报错,比如:

0.000125885009765625000000000000000000000000000000000000000000000000000000

当 decimal 类型字段小数位超72位时,JAVA 代码立刻报错,说明问题和小数位长度有关:

0.0001258850097656250000000000000000000000000000000000000000000000000000001

二、问题根因

进一步分析发现,JDBC 配置了 SQL 以 prepare 方式执行,TiDB prepare 语句处理 decimal 小数位超长72位时,TiDB 内部未正确捕获异常,导致把 error 抛给应用程序引起报错,相关 issuse :https://github.com/pingcap/tidb/issues/62602 。非 prepare 路径处理 decimal 小数位超长时 TiDB 也会报错,但该报错会在 TiDB 内部正常捕获,所以该问题只会在 prepare 路径下复现:

  1. 使用 decimal(M,D) 类型,M:范围是 1 到 65,D:范围是 0 到 M
  2. 只有在 D > 72 时会返回报错 Date truncated,TiDB 代码逻辑是把整数和小数部分除以 9 向上取整,当取正后整数和小数加起来值超过 9 时,在当前有问题的 prepare 处理逻辑下会触发 Data truncated。例如:

0.0001258850097656250000000000000000000000000000000000000000000000000000001

小数位为 73 位,整数位只有 0,算作 1,小数位 Ceil(73 / 9) = 9,所以 1 + 9 > 9 触发 TiDB 产生 Data truncated ... 报错。源码依据:https://github.com/pingcap/tidb/blob/d0ac8e61518286b3f1477c322a834385ee1e0227/pkg/types/mydecimal.go#L395

  1. 该问题通过 mysql 客户端执行 prepare 语句无法复现,变量赋值会自动截断转化为合法的 decimal:
-- 有73位小数
mysql> set @id=0.0001258850097656250000000000000000000000000000000000000000000000000000001;
Query OK, 0 rows affected (0.00 sec)

-- mysql 客户端会截断超过72位的小数,jdbc 并不会截断
mysql> select @id;
+----------------------------------------------------------------------------+
| @id                                                                        |
+----------------------------------------------------------------------------+
| 0.000125885009765625000000000000000000000000000000000000000000000000000000 |
+----------------------------------------------------------------------------+
1 row in set (0.00 sec)

经测试 MySQL 8.0 不存在此问题,证实了 TiDB v8.5.3 在处理 decimal 精度超长时确实有问题。

三、绕过方法

临时方法是代码在处理高精度数据时,避免确保小数位数超72位,例如通过函数确保小数位不超长:

cast( 字段 as decimal(13, 4) );

四、问题修复

反馈问题后,得到了快速响应,预计在 v8.5.4 版本修复。

0
0
0
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论