排序与限制

查询结果的顺序和数量控制,是分页展示和数据分析的基础。

Order 排序

单字段排序:

db.Order("age desc").Find(&users)
db.Order("age asc").Find(&users)
db.Order("age").Find(&users)

多字段排序:

db.Order("age desc, name asc").Find(&users)

链式排序:

db.Order("age desc").Order("name asc").Find(&users)

动态排序

根据用户输入动态排序:

func GetUsers(db *gorm.DB, sortBy string, order string) []User {
    var users []User
    db.Order(sortBy + " " + order).Find(&users)
    return users
}

注意:动态排序要验证输入,防止 SQL 注入:

allowedSort := map[string]bool{
    "age":       true,
    "name":      true,
    "created_at": true,
}
allowedOrder := map[string]bool{
    "asc":  true,
    "desc": true,
}

if !allowedSort[sortBy] {
    sortBy = "id"
}
if !allowedOrder[order] {
    order = "desc"
}

db.Order(sortBy + " " + order).Find(&users)

NULL 值排序

MySQL 中 NULL 值排序:

db.Order("age IS NULL, age asc").Find(&users)

PostgreSQL:

db.Order("age NULLS FIRST").Find(&users)
db.Order("age NULLS LAST").Find(&users)

Limit 限制数量

限制返回条数:

db.Limit(10).Find(&users)

限制 0 条:

db.Limit(0).Find(&users)

返回空结果,某些场景有用。

Offset 跳过

跳过指定条数:

db.Offset(10).Find(&users)

跳过前 10 条,返回后面的记录。

Limit 和 Offset 组合

实现分页:

page := 2
pageSize := 10
db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&users)

随机排序

MySQL:

db.Order("RAND()").Limit(5).Find(&users)

PostgreSQL:

db.Order("RANDOM()").Limit(5).Find(&users)

SQLite:

db.Order("RANDOM()").Limit(5).Find(&users)

取前 N 条

最常见的场景:

db.Order("score desc").Limit(10).Find(&topUsers)
db.Order("created_at desc").Limit(5).Find(&latestPosts)

分组后排序

先分组,再排序:

type Result struct {
    Category string
    Count    int
}

var results []Result
db.Model(&Article{}).
    Select("category, count(*) as count").
    Group("category").
    Order("count desc").
    Find(&results)

去重后排序

db.Distinct("category").Order("category asc").Find(&articles)

分页封装

封装分页函数:

type Pagination struct {
    Page     int
    PageSize int
    Total    int64
    Data     interface{}
}

func Paginate(db *gorm.DB, page, pageSize int, data interface{}) (*Pagination, error) {
    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 10
    }

    var total int64
    db.Count(&total)

    offset := (page - 1) * pageSize
    err := db.Offset(offset).Limit(pageSize).Find(data).Error

    return &Pagination{
        Page:     page,
        PageSize: pageSize,
        Total:    total,
        Data:     data,
    }, err
}

var users []User
result, err := Paginate(db.Model(&User{}), 2, 10, &users)

性能考虑

大偏移量问题

db.Offset(100000).Limit(10).Find(&users)

偏移量大时,数据库要扫描前 100000 条记录再跳过,性能很差。

解决方案:用上次查询的最后一条记录作为起点:

lastID := 100000
db.Where("id > ?", lastID).Order("id asc").Limit(10).Find(&users)

这叫"游标分页"或"键集分页",性能更好。

排序字段没有索引

排序字段没有索引,数据库要全表扫描后排序:

db.Order("created_at desc").Limit(10).Find(&users)

确保 created_at 有索引。

小结

排序和限制是分页的基础。注意大偏移量问题和索引优化,能让查询更高效。