排序、分页与聚合
本章介绍 orderBy、limit、page、withTotal、聚合函数、groupBy 和 having。
排序、分页与聚合
Kronos 在查询构建器上提供排序、分页、分组、聚合函数和聚合过滤。这些 API 可用于普通 select()、生成投影和 join 查询。
投影 alias 和生成结果形态见 📚投影。函数细节见 📚内置函数。
使用 orderBy 排序
使用 orderBy { ... } 配合 asc() 或 desc() 排序。多个排序项使用 []。
kotlin
val users = User()
.select { [it.id, it.name, it.age] }
.where { it.age >= 18 }
.orderBy { [it.age.desc(), it.name.asc()] }
.toList()Mysql
SELECT `id`, `name`, `age`
FROM `user`
WHERE `user`.`age` >= :ageMin
ORDER BY `age` DESC, `name` ASC字段没有调用方向方法时,按升序排序。
default asc
User()
.select()
.orderBy { it.id }
.toList()按选中 alias 排序
orderBy { ... } 可以读取当前查询 context,包括已经选中的 alias。
kotlin
val rows = User()
.select { [it.id, f.length(it.name).alias("nameLength")] }
.orderBy { it.nameLength.desc() }
.toList()Mysql
SELECT `id`, LENGTH(`name`) AS `nameLength`
FROM `user`
ORDER BY `nameLength` DESC谓词需要读取 selected alias 时,使用 filter 建立派生查询,见 📚子查询。
按窗口函数 alias 排序
窗口函数是 selected 表达式。先为窗口结果设置 alias,再在同一层按这个 alias 排序。
kotlin
val rows = Order()
.select {
[
it.id,
it.userId,
f.rowNumber()
.over {
partitionBy(it.userId)
orderBy(it.status.desc())
}
.alias("rn")
]
}
.orderBy { it.rn.asc() }
.toList()Mysql
SELECT `id`,
`user_id` AS `userId`,
ROW_NUMBER() OVER (PARTITION BY `user_id` ORDER BY `status` DESC) AS rn
FROM `order`
ORDER BY `rn` ASC过滤窗口 alias 时使用 filter 建立派生查询,见 📚子查询。函数入口见 📚内置函数。
限制返回行数
只需要前几行时,使用 limit(count)。
kotlin
val users = User()
.select()
.orderBy { it.id.asc() }
.limit(10)
.toList()Mysql
SELECT `id`, `name`, `age`
FROM `user`
ORDER BY `id` ASC
LIMIT 10first() 和 toMap() 也会为单行结果应用单行限制。
Offset 分页
使用 page(pageIndex, pageSize) 创建 offset page,页码从 1 开始。返回的 OffsetPageQuery 只执行一条 SELECT,toList() 直接返回记录。
kotlin
val query = User()
.select()
.where { it.age >= 18 }
.orderBy { it.id.asc() }
val rows: List<User> = query
.page(pageIndex = 2, pageSize = 20)
.toList()Mysql page
SELECT `id`, `name`, `age`
FROM `user`
WHERE `user`.`age` >= :ageMin
ORDER BY `id` ASC
LIMIT 20 OFFSET 20需要总数时,在 offset page 上调用 withTotal()。toList() 返回命名的 PageResult,包含 total、records、totalPages、pageIndex 和 pageSize。
kotlin
val page: PageResult<User> = query
.page(pageIndex = 2, pageSize = 20)
.withTotal()
.toList()
val total = page.total
val records = page.records
val totalPages = page.totalPagesMysql total
SELECT COUNT(*)
FROM (
SELECT 1
FROM `user`
WHERE `user`.`age` >= :ageMin
) AS total_countoffset page 仍是有限的 KSelectable,可以保留内层 LIMIT/OFFSET 并作为派生 source。带总数分页是执行阶段,不能再次 select、join、page 或 cursor。
derived page
val finiteSource = query.page(pageIndex = 2, pageSize = 20)
val outerRows = finiteSource
.select { [it.id, it.name] }
.where { it.name != null }
.toList()游标分页
游标分页使用 cursor(pageSize, after)。下一次查询把返回的 nextCursor 作为 after 传入。toList() 返回命名的 CursorResult,包含 hasNext、nextCursor 和 records。
kotlin
val query = User()
.select { [it.id, it.name, it.age] }
.where { it.age >= 18 }
.orderBy { it.id.asc() }
val firstPage: CursorResult<User> = query
.cursor(pageSize = 20)
.toList<User>()
val nextPage: CursorResult<User> = query
.cursor(pageSize = 20, after = firstPage.nextCursor)
.toList<User>()Mysql next page
SELECT `id`, `name`, `age`
FROM `user`
WHERE `user`.`age` >= :ageMin AND `id` > :cursor_id
ORDER BY `id` ASC
LIMIT 21游标排序必须使用已选字段。如果排序本身不唯一,选中输出还必须保留完整的主键或唯一键,Kronos 才能追加稳定 tie-breaker。游标分页是执行阶段,不能调用 page、withTotal 或作为派生 source。创建 page/cursor view 不会修改可复用的基础查询。
选择聚合值
在 select { ... } 中使用聚合函数。返回多个值或需要命名属性时,为聚合表达式添加 alias。
kotlin
val rows = User()
.select {
[
f.count(1).alias("count"),
f.sum(it.score).alias("scoreSum"),
f.avg(it.score).alias("scoreAvg"),
f.min(it.score).alias("scoreMin"),
f.max(it.score).alias("scoreMax")
]
}
.toList()Mysql
SELECT COUNT(1) AS count,
SUM(`score`) AS `scoreSum`,
AVG(`score`) AS `scoreAvg`,
MIN(`score`) AS `scoreMin`,
MAX(`score`) AS `scoreMax`
FROM `user`result shape
data class UserScoreStats(
val count: Number?,
val scoreSum: Number?,
val scoreAvg: Number?,
val scoreMin: Number?,
val scoreMax: Number?
)分组并过滤聚合结果
使用 groupBy { ... } 指定分组键,使用 having { ... } 过滤聚合结果。having 使用和 where 相同的条件 DSL。
kotlin
val rows = User()
.select {
[
it.gender,
f.count(1).alias("count"),
f.avg(it.score).alias("scoreAvg")
]
}
.groupBy { it.gender }
.having { f.count(1) > 5 && f.avg(it.score) > 50 }
.orderBy { it.scoreAvg.desc() }
.toList()Mysql
SELECT `gender`,
COUNT(1) AS count,
AVG(`score`) AS `scoreAvg`
FROM `user`
GROUP BY `gender`
HAVING COUNT(1) > :countMin AND AVG(`score`) > :avgMin
ORDER BY `scoreAvg` DESC返回行包含分组键和聚合 alias。
result shape
listOf(
mapOf("gender" to 1, "count" to 12, "scoreAvg" to 86.5),
mapOf("gender" to 0, "count" to 9, "scoreAvg" to 79.0)
)下一步谓词需要读取聚合 alias,而不是在同层 having 中重复聚合表达式时,使用 filter。
kotlin
val scoredGroups = User()
.select {
[
it.gender,
f.count(1).alias("count"),
f.avg(it.score).alias("scoreAvg")
]
}
.groupBy { it.gender }
.having { f.count(1) > 5 }
val rows = scoredGroups
.filter { it.scoreAvg > 80 }
.toList()Mysql
SELECT `q`.`gender`, `q`.`count`, `q`.`scoreAvg`
FROM (
SELECT `gender`,
COUNT(1) AS count,
AVG(`score`) AS `scoreAvg`
FROM `user`
GROUP BY `gender`
HAVING COUNT(1) > :countMin
) AS `q`
WHERE `q`.`scoreAvg` > :scoreAvgMinhaving 在内层聚合查询中筛选分组,filter 始终建立外层派生查询。它的 receiver 只暴露 scoredGroups 的 Selected 字段:gender、count 和 scoreAvg。与 query-by-example 的 where() 不同,filter 没有无参形式,必须传入 predicate lambda。
多个分组键使用 []。
multiple keys
User()
.select { [it.gender, it.age, f.count(1).alias("count")] }
.groupBy { [it.gender, it.age] }
.toMapList()在 join 后使用相同 API
JOIN 调用 select 后,可以使用同样的 orderBy、page、withTotal、groupBy 和 having API。
kotlin
val rows = User().join(Order()) { user, order ->
leftJoin { user.id == order.userId }
.select {
[
user.id.alias("userId"),
f.count(order.id).alias("orderCount")
]
}
.groupBy { user.id }
.having { f.count(order.id) > 0 }
.orderBy { it.userId.asc() }
}.page(1, 20).withTotal().toMapList().recordsMysql
SELECT `user`.`id` AS `userId`,
COUNT(`order`.`id`) AS `orderCount`
FROM `user`
LEFT JOIN `order`
ON `user`.`id` = `order`.`user_id`
GROUP BY `user`.`id`
HAVING COUNT(`order`.`id`) > :countMin
ORDER BY `user`.`id` ASC
LIMIT 20 OFFSET 0