在 Go 后端开发中,我们通常使用 MySQL 或 PostgreSQL 等关系型数据库,索引设计的好坏直接决定了服务接口的响应速度。
1.索引设计
1.1 选择合适的列作为索引:
选择性高(High Selectivity): 索引列的不重复值越多越好。例如,用户 ID(唯一)比性别(只有两三种值)更适合作为索引。
场景: 在设计用户服务时,user_id、email 等是理想的索引列。
常用作查询条件(Where): 经常出现在 WHERE 子句中的列,或者用于连接(JOIN)的列。
排序/分组(Order By/Group By): 经常用于排序或分组的列。
1.2 考虑联合索引(Composite Index):
最左前缀原则 (Leftmost Prefix Principle): 这是联合索引设计的核心。如果创建了 (A, B, C) 的联合索引,它可以用于查询 WHERE A = ?、WHERE A = ? AND B = ?、WHERE A = ? AND B = ? AND C = ?,但不能单独用于 WHERE B = ? 或 WHERE C = ?。
场景: 在设计订单查询接口时,如果经常查询 WHERE user_id = ? AND order_status = ?,应建立 (user_id, order_status) 的联合索引。
1.3 覆盖索引 (Covering Index):
如果查询的所有字段都包含在索引中,那么数据库不需要回表(查找主键对应的数据行),直接从索引中返回数据即可。这能大幅提升性能。
场景: 当你需要查询某个用户的订单状态和创建时间,只创建 (user_id, status, create_time) 的联合索引。查询语句为 SELECT status, create_time FROM orders WHERE user_id = ?,数据库直接通过索引就能拿到结果。
1.4 索引数量的平衡:
索引不是越多越好。每个索引都会占用磁盘空间,并且在进行 INSERT, UPDATE, DELETE 操作时,数据库需要维护索引,造成写操作性能下降。
场景: 在设计高写入量的日志表或消息表时,应尽量少建索引,只保留用于最核心查询的索引。
2.高效命中策略
使用 Explain : 在 Go 后端进行复杂查询优化时,一定要在测试环境使用 EXPLAIN 命令分析 SQL 语句,确保 type 列不是 ALL(全表扫描),key 列使用了正确的索引。
Go 代码中遵循 最左前缀原则 : 确保在 Go 代码中构造查询条件时,联合索引的最左侧列总是被包含。
// 假设索引为 (user_id, status, created_at)
// 高效命中:查询中包含 user_id
db.Where("user_id = ? AND status = ?", userID, status).Find(&orders)
// 命中索引的前缀 (user_id):虽然没有 status,但仍然高效。
db.Where("user_id = ?", userID).Find(&orders)
// 低效/索引失效:查询中不包含 user_id
db.Where("status = ?", status).Find(&orders) // 仅 status 无法利用联合索引
//
3.导致索引失效
3.1 导致索引失效的常见方式
索引失效意味着查询优化器放弃使用索引,转而进行全表扫描(ALL),这是后端服务产生慢查询的主要原因。
| 失效方式 | 示例 SQL | 为什么失效? | Go 后端实践建议 |
|---|---|---|---|
| 对索引列进行函数操作 | WHERE YEAR(create_time) = 2024 | 数据库需要对每一行的 create_time 执行 YEAR() 函数,无法直接通过索引 B+ Tree 结构查找。 | 应将函数操作放在等号右侧:WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01' |
| LIKE 模糊查询开头 | WHERE username LIKE '%zhangsan' | 百分号 % 在左侧,导致无法利用 B+ Tree 的有序性进行快速查找。 | 尽量使用前缀匹配:WHERE username LIKE 'zhangsan%'。如果必须使用左模糊,考虑使用 Elasticsearch 或其他全文检索方案。 |
| 类型转换(隐式转换) | WHERE phone = 13800001111(phone 字段是 VARCHAR) | 字符串字段和数字进行比较,MySQL 会将字符串隐式转换为数字,相当于对索引列进行了函数操作。 | 在 Go 代码中严格控制数据类型,使用字符串参数进行查询:db.Where("phone = ?", "13800001111")。 |
| 使用 OR 连接条件 | WHERE name = 'A' OR age = 18 | 如果 name 和 age 字段都没有索引或索引类型不匹配,优化器倾向于全表扫描。 | 如果两个列都有索引,有时会采用索引合并;但更推荐将 OR 拆分为两个 UNION 查询。 |
| 违反最左前缀原则 | 联合索引 (A, B, C),查询 WHERE B = ? AND C = ? | 跳过联合索引的最左侧列 A,B+ Tree 无法按顺序查找。 | 检查联合索引的设计和查询语句,确保查询条件涵盖联合索引的左侧列。 |
| 使用负向查询 | WHERE status != 0 或 WHERE col NOT IN (1, 2) | 负向查询覆盖了大部分数据,优化器认为全表扫描更快。 | 尽量使用正向查询:WHERE status IN (1, 2, 3)。 |