GORM复合索引使用指南

GORM复合索引使用指南 #

基础语法 #

单字段索引 #

type User struct {
    Name string `gorm:"index"`
    Email string `gorm:"uniqueIndex"`
}

复合索引 #

type BillRecord struct {
    UserName string `gorm:"index:idx_user_time,priority:1"`
    PayTime  string `gorm:"index:idx_user_time,priority:2"` 
    Amount   string `gorm:"index:idx_user_time,priority:3"`
}

生成的索引:CREATE INDEX idx_user_time ON bill_record(user_name, pay_time, amount)

复合索引核心规则 #

1. 字段顺序很关键 #

选择性原理 #

选择性 = 不同值的数量 / 总记录数

示例 (10万条记录):

  • UserName: 1000个用户 → 选择性 = 0.01 (高)
  • PayTime: 365个日期 → 选择性 = 0.00365 (中)
  • Status: 3个状态 → 选择性 = 0.00003 (低)
// ✅ 正确:高选择性字段在前
UserName string `gorm:"index:idx_user_time,priority:1"`  // 用户ID,高选择性
PayTime  string `gorm:"index:idx_user_time,priority:2"`  // 时间,中等选择性
Status   string `gorm:"index:idx_user_time,priority:3"`  // 状态,低选择性

// ❌ 错误:低选择性字段在前
Status   string `gorm:"index:idx_bad,priority:1"`        // 只有几个值
PayTime  string `gorm:"index:idx_bad,priority:2"`
UserName string `gorm:"index:idx_bad,priority:3"`

性能对比分析 #

查询:WHERE user_name = 'alice' AND pay_time = '2024-01-01'

错误索引 (status, pay_time, user_name)

  1. 扫描status字段 → 找到33,333条记录 (1/3数据)
  2. 在33,333条中筛选pay_time → 剩余100条
  3. 在100条中找user_name → 最终1条

正确索引 (user_name, pay_time, status)

  1. 直接定位user_name → 找到100条记录
  2. 在100条中筛选pay_time → 最终1条

结果:正确索引比错误索引快 330倍

核心原则:让数据库先用"筛选能力最强"的字段过滤,后续处理的数据量就很小。

2. 最左匹配原则 #

对于索引 (user_name, pay_time, amount)

-- ✅ 能使用索引
WHERE user_name = 'alice'
WHERE user_name = 'alice' AND pay_time > '2024-01-01'
WHERE user_name = 'alice' AND pay_time > '2024-01-01' AND amount > 100

-- ❌ 无法使用索引
WHERE pay_time > '2024-01-01'
WHERE amount > 100
WHERE pay_time > '2024-01-01' AND amount > 100

复合索引与单字段查询 #

常见误区:有复合索引就不支持单字段查询?

答案:支持,但仅限最左字段!

type BillRecord struct {
    UserName string `gorm:"index:idx_user_time,priority:1"`
    PayTime  string `gorm:"index:idx_user_time,priority:2"` 
    Amount   string `gorm:"index:idx_user_time,priority:3"`
}
// 生成索引:(user_name, pay_time, amount)
-- ✅ 单字段查询:可以使用复合索引
SELECT * FROM bill_record WHERE user_name = 'john';

-- ❌ 单字段查询:无法使用复合索引
SELECT * FROM bill_record WHERE pay_time = '2024-01-01';
SELECT * FROM bill_record WHERE amount = '100';

实用索引策略 #

策略1:复合索引 + 单字段索引组合

type Order struct {
    UserID    uint      `gorm:"index:idx_user_time,priority:1;index:idx_user"`
    CreatedAt time.Time `gorm:"index:idx_user_time,priority:2;index:idx_time"`
    Status    string    `gorm:"index:idx_status"`
}

支持查询:

-- 使用复合索引
WHERE user_id = 1 AND created_at > '2024-01-01'

-- 使用单字段索引  
WHERE user_id = 1        -- idx_user
WHERE created_at > now() -- idx_time
WHERE status = 'pending' -- idx_status

策略2:根据查询频率设计

type UserLog struct {
    // 90%查询都带user_id,放最左边
    UserID    uint      `gorm:"index:idx_user_time_action,priority:1"`
    CreatedAt time.Time `gorm:"index:idx_user_time_action,priority:2"`
    Action    string    `gorm:"index:idx_user_time_action,priority:3;index:idx_action"`
}

实战场景 #

场景1:用户+时间范围查询 #

type BillRecord struct {
    UserName string `gorm:"index:idx_user_time,priority:1"`
    PayTime  string `gorm:"index:idx_user_time,priority:2"`
    Amount   string
}

优化查询:

SELECT * FROM bill_record 
WHERE user_name IN ('alice', 'bob') 
  AND pay_time BETWEEN '2024-01-01' AND '2024-12-31'

场景2:多维度分析索引 #

type BillRecord struct {
    UserName     string `gorm:"index:idx_user_category,priority:1"`
    CategoryName string `gorm:"index:idx_user_category,priority:2"`
    PayTime      string `gorm:"index:idx_time_category,priority:1"`
    CategoryName string `gorm:"index:idx_time_category,priority:2"`
}

支持两种查询模式:

  • 用户+分类:WHERE user_name = ? AND category_name = ?
  • 时间+分类:WHERE pay_time BETWEEN ? AND ? AND category_name = ?

场景3:覆盖索引优化 #

type BillRecord struct {
    UserName string `gorm:"index:idx_cover,priority:1"`
    PayTime  string `gorm:"index:idx_cover,priority:2"`
    Amount   string `gorm:"index:idx_cover,priority:3"`  // 包含查询字段
    Status   string `gorm:"index:idx_cover,priority:4"`  // 包含查询字段
}

无需回表查询:

SELECT amount, status FROM bill_record 
WHERE user_name = 'alice' AND pay_time > '2024-01-01'
-- 所有字段都在索引中,无需访问数据页

何时使用复合索引 #

✅ 适用场景 #

  1. 多字段WHERE条件

    WHERE user_id = ? AND status = ? AND created_at > ?
    
  2. 范围查询+精确匹配

    WHERE category = 'food' AND pay_time BETWEEN ? AND ?
    
  3. GROUP BY + ORDER BY优化

    SELECT user_name, COUNT(*) FROM bills 
    GROUP BY user_name ORDER BY user_name
    
  4. 频繁的联合查询

    WHERE (user_name = ? AND status = ?) OR (user_name = ? AND category = ?)
    

❌ 避免场景 #

  1. 低选择性字段组合

    // 性别+状态,区分度太低
    Gender string `gorm:"index:idx_bad,priority:1"`  // 只有男/女
    Status string `gorm:"index:idx_bad,priority:2"`  // 只有active/inactive
    
  2. 过多字段的复合索引

    // 超过4个字段,维护成本高
    Field1 string `gorm:"index:idx_too_many,priority:1"`
    Field2 string `gorm:"index:idx_too_many,priority:2"`
    Field3 string `gorm:"index:idx_too_many,priority:3"`
    Field4 string `gorm:"index:idx_too_many,priority:4"`
    Field5 string `gorm:"index:idx_too_many,priority:5"`  // 过多
    
  3. 频繁更新的字段

    // 更新频繁的字段不适合做索引
    UpdateCount int `gorm:"index:idx_bad,priority:1"`  // 每次都更新
    

性能验证 #

查看执行计划 #

-- SQLite
EXPLAIN QUERY PLAN SELECT * FROM bill_record 
WHERE user_name = 'alice' AND pay_time > '2024-01-01';

-- MySQL  
EXPLAIN SELECT * FROM bill_record 
WHERE user_name = 'alice' AND pay_time > '2024-01-01';

预期结果 #

-- ✅ 使用了索引
SEARCH TABLE bill_record USING INDEX idx_user_time (user_name=? AND pay_time>?)

-- ❌ 全表扫描
SCAN TABLE bill_record

实际案例对比 #

优化前后性能差异 #

// 原始查询:10万条数据,耗时2000ms
SELECT * FROM bill_record WHERE user_name IN (?,?,?) AND pay_time BETWEEN ? AND ?

// 添加复合索引后:相同查询,耗时20ms
// 性能提升100倍

最佳实践 #

  1. 分析查询模式:先看WHERE条件,再设计索引
  2. 高选择性字段优先:区分度高的字段放前面
  3. 控制索引数量:单表索引不超过5个
  4. 定期监控:检查索引使用率,删除无用索引
  5. 测试验证:用EXPLAIN检查执行计划

复合索引是数据库优化的核心工具,合理使用可以带来10-100倍的性能提升。