查不到索引或没走索引是慢查询最常见原因:WHERE字段无索引、函数操作、隐式类型转换会导致索引失效;应使用EXPLAIN分析执行计划,合理设计联合索引,避免计算,结合连接池与索引覆盖优化。

查不到索引或没走索引是慢查询最常见原因
很多 SELECT 语句在数据量上涨后突然变慢,不是代码问题,而是数据库压根没用上索引。Golang 本身不决定是否走索引,但你写的 SQL 和表结构会——比如 WHERE 条件字段没建索引、用了函数(WHERE DATE(created_at) = '2024-01-01')、或隐式类型转换(user_id 是 INT,却传了字符串 "123"),都会让索引失效。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
EXPLAIN或EXPLAIN ANALYZE看执行计划,确认type是否为ref/range,而不是ALL(全表扫描) - 对高频查询的
WHERE、JOIN、ORDER BY字段联合建索引,注意字段顺序:等值条件在前,范围条件在后(如status = ? AND created_at > ?→ 索引应为(status, created_at)) - 避免在索引字段上做计算或函数操作;改写成
created_at >= '2024-01-01' AND created_at 替代DATE(created_at) = ...
sql.DB 配置不当导致连接池拖垮性能
Golang 的 database/sql 默认连接池参数极保守:MaxOpenConns=0(不限制但实际受系统限制)、MaxIdleConns=2、ConnMaxLifetime=0。高并发下容易出现连接争抢、频繁建连、连接泄漏,表现为查询延迟毛刺大或超时。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 显式设置
db.SetMaxOpenConns(50)和db.SetMaxIdleConns(20),数值参考 QPS × 平均查询耗时(秒)× 安全系数 2–3 - 设
db.SetConnMaxLifetime(30 * time.Minute)防止长连接被数据库侧断开后产生 stale connection 错误 - 用
db.Stats()定期检查Idle、InUse、WaitCount,若WaitCount持续上涨,说明连接池太小或有连接未释放(忘调rows.Close()或tx.Commit())
ORM(如 GORM)自动生成 SQL 效率低且难控制
GORM 的链式调用看着方便,但 Preload、Joins、Scopes 容易生成 N+1 查询或冗余字段;更隐蔽的是,它默认开启 PrepareStmt,在短连接场景下反而增加 round-trip 开销。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 禁用自动预处理:
gorm.Open(mysql.Open(dsn), &gorm.Config{PrepareStmt: false})(尤其在连接复用率低时) - 用
SELECT显式指定字段,避免Find(&users)全字段查;对关联数据,优先手写 JOIN 查询,而非依赖Preload - 对复杂报表类查询,直接用
db.Raw().Scan()执行优化过的 SQL,绕过 ORM 抽象层 - 开启 GORM 日志(
logger.Default.LogMode(logger.Info))看它到底发了什么 SQL,别猜
没做读写分离或结果集过大没分页
所有请求打到主库,或一次拉几万行数据再在 Go 里切片分页,这两种情况在业务增长后必然卡死。Golang 无法解决数据库单点压力,但能帮你避开典型反模式。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
db.QueryRowContext()替代db.QueryContext()查单条;查列表必须带LIMIT和合理OFFSET(或更好:用游标分页,如WHERE id > ? ORDER BY id LIMIT 50) - 主从分离需自行实现(GORM 企业版支持,开源版要自己写
Resolver);至少把报表、导出类查询路由到只读副本 - 结果集超过 1000 行,考虑流式处理:
rows, _ := db.QueryContext(ctx, sql); defer rows.Close(); for rows.Next() { ... },避免一次性加载进内存
慢查询背后往往不是 Go 写得不好,而是数据库访问路径没对齐真实负载。最容易被忽略的其实是连接池配置和索引覆盖度——它们不写在业务逻辑里,但决定了 80% 的响应时间分布。上线前跑一次 EXPLAIN,压测时盯住 db.Stats(),比加机器管用得多。











