【mysql】BIGINT UNSIGNED字段被表示为float科学计数法 丢失精度问题
1. 问题
我有如下 sql 查询:
rows, err := db.Query("SELECT COALESCE(creator, ?) FROM table1 LIMIT 1;", "")
if err != nil {return err
}
defer rows.Close()for rows.Next() {var dest stringif err = rows.Scan(&dest); err != nil {return err}fmt.Println("dest: ", dest) // 值为 6.743046165522305e+16 ?
}
发现数据库存储的 creator 内容是 67430461655223049,但是查询结果却被变成了一个科学计数法的 string: 6.743046165522305e+16
creator 的类型明明是一个 BIGINT UNSIGNED 无符号整型:
`creator` BIGINT UNSIGNED NOT NULL DEFAULT 0
为什么会被扫描成一个 float 格式的字符串的呢?
2. sql 分析
增加一段打印,查看 go-sql-driver 驱动解析出的类型:
colTypes, _ := rows.ColumnTypes()
for _, ct := range colTypes {fmt.Printf("DB Type: %s, Scan Type: %v\n", ct.DatabaseTypeName(), ct.ScanType())
}
发现这个字段真的被驱动解析成了 float 类型:
DB Type: DOUBLE, Scan Type: float64
(1) 使用原本字段
尝试修改 sql 语句:
# sql
SELECT creator FROM table1 LIMIT 1;# 打印
DB Type: UNSIGNED BIGINT, Scan Type: uint64
原始 creator 类型是 uint 没错,那问题就出在了 COALESCE 上
(2) 使用 CAST(COALESCE(creator, ?) AS UNSIGNED)
尝试使用 CAST 函数,将类型手动转换为 uint:
# sql
SELECT CAST(COALESCE(creator, ?) AS UNSIGNED) FROM table1 LIMIT 1;# 打印
DB Type: UNSIGNED BIGINT, Scan Type: uint64# 查询结果
67430461655223048
类型对了,但是发现读出的数据不对了!
应该是 67430461655223049,查询出来变成了 67430461655223048,牙白!丢精度了!
因为:驱动想用 float64 去接收的,但我强制将它转成了 uint,在某些环境下可能会丢失精度。
float64 无法精确表示所有 uint64 范围内的整数,特别是超过 2^53 的数字,就会发生舍入误差。
(3) 使用 COALESCE(CAST(creator AS CHAR), ?)
也就是说,它想用 float,我不能强制转为 uint,那我可以强制转为 string 啊!
# sql
SELECT COALESCE(CAST(creator AS CHAR), ?) FROM table1 LIMIT 1;# 打印
DB Type: VARCHAR, Scan Type: sql.NullString# 查询结果
67430461655223049
ok 结果正确
(4) 使用 COALESCE(creator, '')
在尝试不同的 sql 语句过程中我发现,当我使用 COALESCE(creator, ?) 并传入空字符串作为参数,和直接写死 COALESCE(creator, ‘’) 的行为是不同的,会直接影响字段类型的解析结果:
# sql
SELECT COALESCE(creator, ?) FROM ..# 被解析为float
DB Type: DOUBLE, Scan Type: float64# sql
SELECT COALESCE(creator, '') FROM ..# 被解析为string
DB Type: VARCHAR, Scan Type: string
这是由于: MySQL 在执行查询时会为每个字段生成元信息(metadata),包括字段名、字段类型、是否可能为 NULL、是否是数字类型等。当使用 COALESCE(creator, ?) 并传参 "" 时,驱动不知道 ? 的实际类型是什么,它会尝试根据 creator(BIGINT UNSIGNED)和参数的默认类型进行推断。go-sql-driver 就会把参数当作 float64 来处理(因为它是数值型),整个 COALESCE(...) 表达式被推断为 DOUBLE 类型。
也就是说,使用这种写法 COALESCE(creator, '') 也可以达到我们的目标。
BUT!这种写法存在隐患:
- 类型推导不稳定:MySQL可能在某些版本/配置下将其推导为DOUBLE,导致科学计数法(6.743046165522305e+16)
- 依赖驱动行为:不同版本的go-sql-driver/mysql可能解析出不同Go类型(string/float64)
(5) 使用 IFNULL(CAST(creator AS CHAR), ?)
还有一种写法,用 IFNULL 函数:
# sql
SELECT IFNULL(CAST(creator AS CHAR), ?) FROM table1 LIMIT 1;# 打印
DB Type: VARCHAR, Scan Type: sql.NullString# 查询结果
67430461655223049
也是可以的。
但是 IFNULL 不是标准 SQL,仅限于 MySQL 环境下使用。如果你希望代码兼容其他数据库(如 PostgreSQL、达梦),应该优先使用 COALESCE。
3. 解决
最佳写法:
rows, err := db.Query("SELECT COALESCE(CAST(creator AS CHAR), ?) FROM table1 LIMIT 1;", "", "89448245134135417")
- CAST(creator AS CHAR):将字段 creator 强制转换为字符串类型(CHAR),驱动会用 sql.NullString 类型来接收
- COALESCE(..., ?):如果第一个参数不为 NULL,就返回;否则返回第二个参数(即空字符串)
4. 几种 sql 写法对比
方案 | 类型安全 | 精度保证 | 推荐 |
SELECT creator | ✅uint64 | ✅ | ❌不能处理NULL值 |
COALESCE( creator , ?) | ❌ 变为float64 | ❌可能出现科学计数法表示 | |
COALESCE(creator, '') | ❌ 可能string也可能变为float64 | ❌依赖驱动版本 | |
CAST(COALESCE(creator, ?) AS UNSIGNED) | ✅uint64 | ❌ 会将float转为uint丢失精度 | ❌丢失精度 |
COALESCE(CAST(creator AS CHAR), ?) | ✅sql.NullString | ✅ | ✅ |
IFNULL(CAST(creator AS CHAR), ?) | ✅sql.NullString | ✅ | ❌不是标准SQL不够通用 |