一条查询可能命中数百万行,而屏幕上同时可见的行数通常在几十行量级。若把整个结果集一次取回并全部构造为界面元素,内存与布局开销都会随行数增长。因此数据库客户端通常在两处设限:取数时限制单次传输的行数,渲染时只构造视口附近的元素。
一次性取回与游标式取回
驱动层通常提供两种取数方式:
- 一次性取回:执行语句后把全部结果行读入客户端内存,调用返回时结果已完整可用。
- 游标式取回:执行后返回一个游标,客户端按批读取,游标在服务端或客户端保持语句的执行状态。
一次性取回的实现简单,但在结果集规模不可控时会带来内存压力;游标式取回把内存占用与批大小绑定,代价是需要维持一个打开的游标。需要注意,逐行读取并不等于逐行往返:多数驱动在底层按批预取,只是把批边界隐藏起来。
服务端游标与客户端游标
按状态保存在哪一侧,可以区分两类游标:
| 维度 | 服务端游标 | 客户端游标 |
|---|---|---|
| 状态保存位置 | 数据库会话内 | 客户端内存或临时缓冲 |
| 内存占用 | 服务端按需保留结果 | 客户端持有已读批次 |
| 连接占用 | 通常需保持连接与会话 | 可不依赖会话 |
| 并发修改可见性 | 取决于隔离级别与快照 | 取回时刻的快照 |
| 中途放弃 | 需要显式关闭以释放资源 | 释放本地缓冲即可 |
服务端游标往往要求在事务中打开,并且同一连接上同时只能存在有限数量的活动游标。如果连接池把连接复用给其他请求,未关闭的游标可能被隐式关闭,因此取批逻辑需要显式处理「游标已失效」这一状态,能在失效后重新定位并继续取数。
取批大小与预取
批大小是取回阶段的核心参数。取回量偏小时往返次数增加,每批的固定开销被放大;取回量偏大时内存占用提高,首批数据可见的延迟也随之增加。多数实现采用介于两者之间的固定值,并在滚动接近已加载边界时预取下一批。
// 预取:剩余未消费行数少于阈值时提前请求下一批
const PREFETCH_THRESHOLD = 50;
const remaining = loadedRows.length - cursorIndex;
if (remaining < PREFETCH_THRESHOLD && hasMore) {
fetchNextBatch();
}
如果结果集在取回期间可能被其他会话修改,不同批次可能来自不同时刻的数据,一致性取决于隔离级别;要求一致性读时,通常把整次取回放进一个事务或使用快照。
结果集与虚拟滚动的映射
渲染侧的做法与列表虚拟滚动一致:用行高与滚动偏移换算出可见区间,只构造可见区间与缓冲区内的行元素,其余行用占位高度表示。高度换算、缓冲区与节点回收的具体策略可参见 大目录虚拟滚动实现。
结果集与本地列表的差别在于数据来源的延迟不同:本地条目已在内存中,而行可能尚未取回。因此索引到偏移的换算必须为未取回的行给出占位高度,滚动到这些区间时先渲染占位行,待批次到达后原位替换。
滚动条长度依赖总行数。若无法获得确切总数,可以用已加载行数作为长度,并在取回过程中延长,代价是滚动条位置会随加载推进而调整。使用排序键定位的分页方式,其依据可参见 B+ 树索引结构 中关于范围扫描的说明。
编辑状态与大字段
未提交行与变更表
可编辑的结果集需要额外跟踪每行的修改状态。常见做法是维护一张以行标识为键的变更表,记录原始值与新值,渲染时把变更叠加到取回的数据之上。这样处理的原因是后续批次的取回可能覆盖已修改的行,而变更表与批次解耦后不受影响。
插入行与删除行需要单独处理:尚未提交的插入行没有服务端行标识,通常用临时标识占位;删除行在界面上划去,但在提交前仍占据位置,否则偏移换算得到的行数与实际不一致,滚动位置会出现偏移。
大字段的延迟加载
包含大文本或二进制列的结果集不适合在取批时一并传输。多数实现把这类列从概述查询中排除,只在某行展开或进入编辑时单独请求该列的值,并在客户端缓存结果。常见做法以「行加列」为单位请求,避免因为一列而拉取整行的大字段。
连接占用与游标释放
打开的游标通常绑定一个连接,而连接池容量有限。滚动到结果集中部后长时间停留,连接会被该游标占用;若同时打开多个结果页,占用的连接数随之增加。常见的处理方式是在滚动暂停时关闭游标,只保留已取回的行,再次滚动超出已取回区间时改用键集分页重新定位。
键集分页依赖有序且可比较的排序键,通过条件从上一批的边界值之后继续取数,避免偏移量分页在大偏移时需要先跳过大量行的扫描成本。
-- 键集分页:以上一批末尾的排序键继续取数
SELECT id, name FROM users
WHERE (name, id) > ('alice', 1024)
ORDER BY name, id
LIMIT 100;
该方式要求排序键在结果集内不重复,或与补充列组合后不重复,否则边界处可能重复或漏行。
小结
结果集的懒加载由两侧配合完成:取数侧用游标与批大小控制传输量与内存,渲染侧用虚拟滚动控制元素数量,两者通过「已加载区间」这一状态衔接。编辑状态、大字段与游标释放则要求把变更、值与连接生命周期分别管理,避免它们耦合到具体的批次或元素上。