DEBUG.WIKI
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →
雨云;给项目一台云服务器
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →

Go MySQL 升级后 Prepare 卡在 skipColumns,先查 ORDER BY 参数

go-sql-driver/mysql 1.10.0 配旧 MySQL 8.0.32,ORDER BY ? 可能暴露 DEPRECATE_EOF 元数据问题。占位符也不能代替列名;使用固定白名单语句,分别核对协议等待、服务器版本与排序结果。

升级 go-sql-driver/mysql 到 1.10.0 后,db.Query 一直不返回,goroutine 堆栈停在 (*mysqlConn).Prepare、skipColumns 和网络读取;MySQL 的连接列表却显示 Sleep。若服务器仍是旧版 MySQL 8.0,并且 SQL 中有 ORDER BY ?,要同时检查协议与占位符的真实含义。

这条故障有 MySQL 8.0.32、驱动 1.10.0、Go 1.26.5 的具体组合。不能把任何升级后的查询等待都归到同一原因;首先确认堆栈已经进入驱动读取列定义,而不是还在连接池里等可用连接。

ORDER BY ? 没有选择名为 name 的列

下面的调用并不表示按 name 列排序:

rows, err := db.QueryContext(ctx,
    "SELECT id, name FROM items WHERE status = ? ORDER BY ?",
    0, "name",
)

问号绑定的是数据值,第二个参数是字符串常量 "name"。MySQL 预处理标记不能用来代替表名、列名或 SQL 关键字。因此,即使它在别的版本里很快返回,排序含义也不是应用原本期望的列排序。

1.10.0 增加了 DEPRECATE_EOF 支持;这条旧服务器组合在准备特定查询时,服务端返回的列定义包数量少于声明数量。客户端继续等待缺少的包,就可能停在 skipColumns。对应观察中,8.0.37 及以前可见此现象,8.0.38 不再复现;这里不据此声称某条已确认的官方修复首次进入了 8.0.38,也不把故障范围扩大到所有 MySQL、MariaDB 或所有预处理语句。

Sleep 只是服务器此刻没有执行查询的状态,不能单独证明网络健康,更不能据此推断客户端已拿齐响应。应把完整堆栈、实际 SQL、绑定参数类型以及双方版本放在一起判断。

把可排序列收敛为完整白名单语句

动态选择排序列时,可以把外部选项映射为固定 SQL,而不是直接把输入拼进语句:

queries := map[string]string{
    "name": "SELECT id, name FROM items WHERE status = ? ORDER BY name, id",
    "id":   "SELECT id, name FROM items WHERE status = ? ORDER BY id",
}
query, ok := queries[sortName]
if !ok {
    return fmt.Errorf("unsupported sort option %q", sortName)
}
rows, err := db.QueryContext(ctx, query, status)
if err != nil {
    return err
}
defer rows.Close()

这是已有查询函数中的片段,db、ctx、sortName、status 和返回错误的函数签名由应用提供,错误构造来自 fmt。白名单中的表和列须对应真实模型;id 在第一个选项中作为稳定的次级排序,也应符合业务原有的分页规则。实际函数仍需处理每次 Scan 的错误和最终的 rows.Err()。

数据条件继续使用问号,只有列选择来自固定语句集合。不要把最后一行替换成不受限制的 fmt.Sprintf(..., sortName),否则会把一个排序错误变成任意 SQL 片段入口。排序方向也需要自己的固定选项,不能让外部输入直接成为 ASC/DESC 后面的文本。

排除错位置的修复

如果堆栈停在 database/sql 连接获取阶段,应按事务与 DB 混用导致的池等待检查。这里的 Prepare→skipColumns 已进入另一条路径;单纯扩大池上限不会补回服务器少发的列定义。

可维护服务器时,应按供应方支持范围安排服务端更新,并复查真实驱动组合;不能把一次旧版本对照当成长期停留在不受支持版本的理由。暂时依赖原版本时,先修正并验证 SQL 的标识符选择,不要通过编辑模块缓存、删除读包操作或吞掉 I/O 错误来强行返回。

操作超时和驱动读取超时可为等待提供退出边界,但不是协议修正,也不应承诺任意驱动阻塞都能即时取消。失败后继续遵循驱动的连接处置与资源释放规则,不能把尚未读完响应的底层连接交给下一次业务调用。

验收既要看原调用能否结束,也要看返回行是否真正按所选列排序。保留有、无排序以及不同数据参数的对照,并观察 Prepare 阶段与连接使用情况。只恢复“几毫秒返回”而没有检查排序结果,仍可能留下原先的业务错误。