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 string
if 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, ?)
尝试使用 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 类型。
BUT!这种写法存在隐患:
- 类型推导不稳定:MySQL可能在某些版本/配置下将其推导为DOUBLE,导致科学计数法(6.743046165522305e+16)
- 依赖驱动行为:不同版本的go-sql-driver/mysql可能解析出不同Go类型(string/float64)
(5) 使用 IFNULL(CAST(creator AS CHAR), '')
还有一种写法:
# 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")
# 打印
DB Type: VARCHAR, Scan Type: sql.NullString
CAST 直接将字段按映射成了 string 类型,当其值为 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不够通用
|