如何滚动和查看数百万条记录





5.00/5 (8投票s)
当您需要允许用户访问大量数据时,一种简单而有用的模式。
引言
最近有人问我,当需要以线性方式向用户显示大量数据时,应该采取何种方法。特别是,如果您有数百万条记录(甚至数百条,概念相同!),并且无论出于何种原因,需要允许用户滚动查看这些记录。我以前使用一些成熟的模式处理过这个问题,所以我认为将其写成一篇短文并分享知识会很有用。
背景
当处理超过一屏的数据时,我们需要考虑如何向用户呈现这些数据。用户将如何查看数据,以及他们如何导航数据?显然,超过一屏的数据量对人类的眼睛来说太大,无法一次性准确地吸收和处理,因此我们需要在幕后实现一个系统来管理事务。这些基本概念在 Web 上与在桌面和移动设备上(以及我们可能尚未遇到的其他界面上)的工作方式相同。本文将探讨这些概念(您可能已经知道但没有过多思考),并提供一些示例和需要注意的事项。
核心概念 1 - 跟踪用户位置
当用户请求数据时,他们会从特定的视角或位置进行请求。所采取的位置取决于数据的视角和上下文。一些示例:
- Facebook 动态
- 当您加载 Facebook 时,根据底层算法,会向您显示朋友的最新帖子,最新内容显示在页面顶部。这里的起始位置可能是您上次登录或查看此特定朋友动态的时间。
- CodeProject 首页 (1)
- 登陆 CodeProject 首页后,您会看到“最新文章”。这些文章以线性方式呈现给您,顶部显示一篇特色文章,然后是十篇最新的文章/技巧。这里的起始位置是您登录前刚发布的最新十篇文章。
- CodeProject 首页 (2)
- 当您来到首页并看到标题栏时,您会注意到,哦,我可以点击一个表示特定技术类型的按钮来过滤这篇文章列表。例如,在撰写本文时,目前可用的有 Android、C#、C++ 等……当您点击其中一个按钮时,会应用一个过滤器,现在,起始位置已经从所有文章变成了特定类型文章的子集。
通过以上示例,我们可以看到用户相对于他们想要查看和我们想要呈现给他们的数据所处的位置视角是可变的。因此,向用户呈现大量数据的关键部分是理解和跟踪他们与我们正在处理的整个数据集相关联的用户位置。
核心概念 2 - 数据门户
当用户请求数据时,我们需要知道几件事:
- 用户对整个数据集的所需视图是什么
- 他们相对于所需视图的位置是什么
- 用户一次能够查看/处理的数据量是多少
我们已经在桌面和网络上多次看到这种模式——这就是数据分页的概念。
在上面(来自出色的 datatables .net)示例中,我们可以看到用户所需的视图是对包含“lo”的任何字段的过滤,他们当前的位置是“第 1 页”,并且他们一次只希望查看 10 条记录。
后端滚动视口管理
从前端的角度来看,正在发生什么,或者更确切地说,我们正在指示什么发生,是清楚和明显的,但是后端是如何处理的呢?
为了解释,让我们看一个包含 15 条记录的数据集的简单示例。假设用户从第一条记录开始,并希望每次以 5 条记录的块/视图/集合移动。在第 1 步,我们得到记录 1..5,第 2 步得到记录 6..10,最后在第 3 步得到记录 11..15。
当用户点击前端的“上一页/下一页”按钮时,他们会向后端发送指令,说明“我在这里的位置 X,请从该位置给我 Y 条记录”。
步骤 1 - 起始位置 = <开始> 记录 0,从这里开始,给我 5 条记录 = 记录 1..5
步骤 2 - 起始位置 = 记录 5,从这里开始,给我 5 条记录 = 记录 6..10
步骤 3 - 起始位置 = 记录 10,从这里开始,给我 5 条记录 = 记录 11..15
这在后端非常简单地转换,取决于您正在使用的后端——这里有两个例子:
SQL (2012): OFFSET
(起始位置)FETCH NEXT
(所需数量)参考 MSDN
例如:
SELECT RecordID, FirstName FROM Contacts
order by RecordID
OFFSET 10 ROWS
FETCH NEXT 5 ROWS ONLY
LINQ: SKIP
(起始位置)TAKE
(所需数量)参考 MSDN
var custQuery2 =
(from cust in db.Customers
orderby cust.ContactName
select cust)
.Skip(50).Take(10);
请查阅您正在使用的任何其他后端的文档——应该有一个等效的方法来允许您实现此功能。
现在,我已经向您指出了从结构化数据集中提取有限数据视图的这种常用方法,我必须提醒您不要期望每次都依赖它。始终质疑用例、所涉数据量、可用的索引字段等等。在非常非常大的数据集中,map/reduce 模式可能效果很好,或者您可能预加载了用于特定数据展示的数据以供提供,或者 offset/limit 可能会随着特定数据量而变慢等等……像往常一样,虽然这是一个通用的“如何做”,但您案例中的具体答案很可能是“视情况而定”。基本概念和模式保持不变,目标是确定如何在您的情况下最好地应用它们。
一篇有趣的文章讲述了如何在较旧的结构化数据库/关系型数据库系统不支持offset
概念的情况下处理问题。
动态分页
在离开简单数据表之前,值得指出如何处理动态分页。当您处理适量数据时,您会期望看到像这样的分页控件:
当您开始向其中添加数据时,会发生什么?
如您所见,中间页码开始填充……这会很快变得非常混乱。一个很好的处理方法是视觉上移动您指示用户在视图中的范围……以下是 Google 的做法,例如……请注意,结果现在从 9 开始到 18,他们告诉我们这是数万亿页中的第 14 页……(他们还为结果的速度自豪,这很好:))
即使是谷歌也不允许你搜索所有内容——有时他们会说——不行,你的查询需要更具体一些……以我点击超过第 1000 页时收到的这条消息为例(我为了研究真是拼了!)。
(顺便说一句,红字是我加的……开玩笑!)
有关分页的更多阅读,请查看UI 模式分页页面,如果您想深入了解,请坐下来阅读Janko at Warp Speed 上的表格 UI 模式(很棒!)。
优化分页显示的四种方法
作为优化分页的最后简短说明,这里总结了 Bill Karwin 在 StackOverflow 上提出的一些想法。
-
在第一次查询时,获取并缓存所有结果(注意:我的备注仅适用于小数据集!)
-
不要显示所有结果。即使是 Google 也不让你看到第一百万个结果。
-
不要显示总数或指向其他页面的中间链接。只显示“下一页”链接。
-
估算有多少结果。同样,Google 也这样做,没人抱怨。
表格和分页都很好,但是这种无限滚动是怎么回事,它与视图门户概念有什么关系?……实际上,在后端可以认为是相同的,前端有一些技巧,主要关注用户当前正在查看的数据的预缓存和后缓存。让我们看看它是如何工作的。
核心概念 2 - 缓存
无限或连续滚动实际上与分页相同,只是用户不必明确点击以移动到下一批数据——我们为他们假设了意图。这可以根据适合您情况的任意数量的触发器来触发……例如,滚动条达到极限,用户滚动到页面最底部,屏幕上可见数据的最后一部分显示在视图中……触发点完全取决于您的设计。
然而,无限滚动唯一真正的问题是如何优化用户体验。我们如何使其感觉无缝且快速?……答案在于缓存。在最基本的层面上,我们预测用户想要做什么,并在他们询问之前完成它。有许多解决方案可以为您在各种平台上的各种演示框架中实现这一点,但了解幕后可能发生的基本概念是很好的。让我们看一个示例方法。
步骤 1 - 默认加载
当我们第一次加载网页/应用程序时,我们有一个现成的默认视图供用户使用。这就是下面显示的“当前视图”。在我们的应用程序中,我们知道用户接下来会做的事情是使用他们的拇指/鼠标将下一段数据滚动到视图中。在这种情况下,我们已经在设备/页面上加载了“下一视图”,并准备好立即显示它,用户体验没有任何延迟或干扰。这个“下一视图”默认缓存,并随时可用。从实践角度来看,这意味着当我们设置页面时,我们不仅加载当前视图,还加载“下一视图”——下一视图不可见,但缓存到屏幕外,并随时按需使用。
步骤 2 - 后缓存和预缓存
当用户滚动“下一视图”使其成为“当前视图”时,当前视图会移开成为“上一视图”。这个新的“上一视图”会以不可见的方式缓存到屏幕外,以防用户决定再次向上滚动。一旦“下一视图”开始滚动到位,我们就会立即去获取下一个“下一视图”来替换它(这与《盗梦空间》不太一样,但确实让我想起了它!)。
步骤 2 - 进一步管理
接下来做什么取决于您这位系统设计者。如果您是从头开始设计,您需要决定接下来的步骤;或者,如果您使用的是预构建系统,您可能会有一些配置选项可供选择,以帮助调整缓存大小等。从简单层面讲,您可能会决定,一旦您从原始的“当前视图”移动了两步/滚动离开,那么用户实际上就不会再滚回去了,您可能会决定删除那个“上一个-上一个-视图”。无论您的策略如何运作,预加载/缓存数据以备不时之需的概念仍然是可靠的。
数据视图
以防有帮助,这里是上述情况的另一个视图,但显示了样本记录数据是如何遍历的……
您可以在UI patterns上阅读更多关于无限/连续滚动模式的信息。
摘要
这就是最基本的概念——显然还有很多可以展开的地方,这只是冰山一角,但希望能给一些人指明正确的方向。
代码示例
我很快会上传一些与本文相关的代码示例。
历史
版本 1 - 2016 年 7 月 7 日。