克服 SharePoint CAML 查询中的列表视图阈值





5.00/5 (8投票s)
如何编写可扩展的 CAML 查询,使其在大型 SharePoint 列表中不会返回列表视图阈值错误。
引言
当您的 CAML 查询开始触及列表视图阈值时,您会觉得它永远不会成功。它确实可以成功,但过程艰难。本文汇集了我过去一年左右收集到的构建 CAML 查询的技巧和窍门。
在 SharePoint 中使用大型列表时,您无疑会遇到列表视图阈值。这是单个视图中可以返回的 5000 行的固定限制。现在,这过于简化了——实际上有办法避免看到这个限制。在本文中,我将重点介绍在您的 CAML 查询代码中处理此限制的方法。具体来说,我将使用 C# 和客户端对象模型 (CSOM),尽管 JavaScript 对象模型将完全相同,并且大多数问题也与服务器对象模型相关。
不要混淆列表视图阈值 (5000) 和列表容量限制,后者大约为 5000 万个项目,或者具有唯一权限的 50,000 个项目。
历史
首先,快速回顾一下 5000 个项目限制的历史。这是一个硬性限制,存在于 SharePoint 2010、2013 和 2016 以及 SharePoint Online (Office 365) 中。您可以在本地环境中更改此限制,但由于不推荐这样做,我甚至不会说明如何更改。例如,您可以将限制从 5,000 更改为 20,000,但当您的列表增长到 20,000 个项目时会发生什么?使用本文中的技术,通过更改架构和编写查询来解决此限制,将更好地为您服务。
SharePoint 列表的底层是一个 SQL Server 表。当您执行 CAML 查询时,查询结果会对 SQL Server 执行 SQL 查询。现在,在 SQL Server 中,查询执行期间锁定项目会带来一点性能损失。当您锁定足够多的项目时,锁定会升级到*整个*表——正如您可以想象的那样,这会导致对该表的其他查询产生普遍的性能损失。因此,SharePoint 通过强制执行单个查询返回 5000 个项目的阈值来防止这种情况发生。这样,作为开发人员,我们被迫改进我们的架构和查询技能以避免这种情况。
在 SharePoint 2016 中,通过以下几种方式略微缓解了此问题
- 列表视图自动索引
如果 SharePoint 检测到某列会带来性能提升,它会自动为该列创建索引。 - 允许追溯创建索引
在 SP2013 中,您不能为包含超过 5000 个项目的列表的列添加索引。在 SP2016 中,这将允许。 - 更智能的列表视图阈值违规检测
它将更可靠地检测何时应该限制查询。 - 改进默认文档库视图
开箱即用的文档库视图将不再首先排序文件夹,从而避免潜在的列表视图阈值错误。
从以上几点我们可以看出,在管理大型列表方面已经取得了一些进展。然而,列表视图阈值仍然存在——因此从查询的角度来看,没有任何变化。
有关更多信息,请参阅 Bill Baer 关于此主题的博客文章:http://blogs.technet.com/b/wbaer/archive/2015/08/27/navigating-list-view-thresholds-in-sharepoint-server-2016-it-preview.aspx
SharePoint UI
如果列表中有超过 5000 个项目,您将在列表设置中收到警告 - “此列表中的项目数量超过列表视图阈值”。这意味着许多 UI 功能将不再起作用,您的自定义视图也可能不再起作用。
上面的列表包含大约七十五万个项目,是 Repstor custodian 的测试列表——这证明了是的,您可以通过一些智能查询来使用大型列表!
列索引
除了索引列外,排序将不再起作用。不幸的是,当列表包含超过 5000 个项目时,您甚至无法为列添加索引,因此如果您的列表可能会增长到此大小,则需要提前准备。不过,这将在 SharePoint 2016 中得到改进。
ID 列是自动索引的,因此默认情况下,您可以在存在 5k+ 项目的情况下对 ID 列进行排序。您最多可以索引 20 列。如上所述,在 SharePoint 2016 中,列索引可以自动管理 - 但是,如果您计划进行一些查询,那么您将需要明确指定您的索引。
筛选视图
即使所有相关列都已索引,当视图将显示超过 5000 个项目时,您也无法呈现筛选视图,即使它已分页。不幸的是,在解决此问题时,分页并没有真正帮助,因为您仍然强制对超过 5000 个项目进行底层扫描。最难理解的一点是,*不包括分页*的查询在某些微不足道的情况下绝不能超过 5000 个结果。
CAML
在这些示例中,我将使用一些约定。假设我的表有一百万个项目。它有以下列:ID、IndexedCol 和 NonIndexedCol,这应该很容易理解;IndexedCol 已索引,NonIndexedCol 未索引。以下所有内容都是完全有效的 CAML,并且如果您有少于 5k 个项目,它们将始终有效。
这个简单的 CAML 查询**将起作用**
<Query>
<View>
<RowLimit>10</RowLimit>
</View>
</Query>
现在,即使它不包含过滤器,也只扫描表的开头:只选取了前 10 个项目。但是,如果我们不将其限制为 10 个项目,我们将收到错误 - 此查询**将不起作用:**
<Query>
<View>
</View>
</Query>
假设只有 1000 行的 IndexedCol 等于“match1k”。此查询**将起作用**,即使我们不包含
<Query>
<Where>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match1k</Value>
</Eq>
</Where>
</Query>
这很合理——在 SQL 中,WHERE 子句只匹配 1000 行。现在假设有 6000 行的 IndexedCol 等于“match6k”。此查询**将不起作用**
<Query>
<Where>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match6k</Value>
</Eq>
</Where>
</Query>
但是,在这种情况下,使用 AND 运算符组合查询**将起作用**
<Query>
<Where>
<And>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match1k</Value>
</Eq>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match6k</Value>
</Eq>
</And>
</Where>
</Query>
看起来很明显,不是吗?然而,令人困惑的是,以下查询**将不起作用**,尽管它看起来与上面的查询相同
<Query>
<Where>
<And>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match6k</Value>
</Eq>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match1k</Value>
</Eq>
</And>
</Where>
</Query>
为什么不起作用?因为从查询的第一部分(`IndexedCol = 'match6k'`)扫描了 6000 个匹配项,并且在命中 WHERE 子句的第二个条件之前发生了阈值错误。这里的教训是
现在,我们将尝试查询未索引的列。此查询**将永远不起作用**,即使它不匹配任何项目
<Query>
<Where>
<Eq>
<FieldRef Name='NonIndexedCol' />
<Value Type='Text'>matchNone</Value>
</Eq>
</Where>
</Query>
这是因为
或者
现在我们转到“或”的使用。不幸的是,我们在这里陷入了困境。对超过 5000 个项目的列表使用“或”将*始终*导致列表视图阈值错误!因此,“或”部分非常简短......不要使用“或”!您唯一的选择是运行多个查询。
排序
只要您满足两个要求,您就可以对结果进行排序
- 您的查询符合上述规则,并且没有超出列表视图阈值(显然),
- 您正在筛选的字段已编制索引。
因此,这个非常简单的查询**将起作用**
<Query>
<View>
<OrderBy>
<FieldRef Name='IndexedCol' Ascending='False' />
</OrderBy>
<RowLimit>10</RowLimit>
</View>
</Query>
这个非常简单的查询**将不起作用**,因为它在一个未索引的列上
<Query>
<View>
<OrderBy>
<FieldRef Name='NonIndexedCol' Ascending='False' />
</OrderBy>
<RowLimit>10</RowLimit>
</View>
</Query>
请记住,如果您包含带有上述内容的 WHERE 子句,则无论您使用 RowLimit 元素如何,您的 WHERE 都应最多匹配 5000 个结果。因此,这**将起作用**
<Query>
<Where>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match1k</Value>
</Eq>
</Where>
<View>
<OrderBy>
<FieldRef Name='IndexedCol' Ascending='False' />
</OrderBy>
</View>
</Query>
分页
如果您有大型列表,那么您几乎总是想利用分页。当您没有过滤器或过滤器返回少于 5000 个项目时,分页效果极佳。因此,您可以使用一个简单的查询来查询最新项目的第一个“页面”,如下所示,该查询**将起作用**
<Query>
<View>
<OrderBy>
<FieldRef Name='IndexedCol' Ascending='False' />
</OrderBy>
<RowLimit>10</RowLimit>
</View>
</Query>
此查询,不带 RowLimit,不会突破视图阈值。
要检索第 10 个项目之后的下一页,您需要在 `CamlQuery` 对象的 `ListItemCollectionPosition` 字段中指定要继续的值。
CamlQuery camlQuery = new CamlQuery();
camlQuery.ListItemCollectionPosition = "Paged=TRUE&p_ID=1034";
camlQuery.ViewXml = "..."; //Query View element
ListItemCollection listItems = list.GetItems(camlQuery);
clientContext.Load(listItems);
clientContext.ExecuteQuery();
//Note listItems.ListItemCollectionPosition for the next page
ListItemCollectionPosition
属性的值来自上一页的 ListItemCollection.ListItemCollectionPosition
。
同样,如果不存在过滤器,或者存在返回少于 5000 个项目的过滤器,则此方法有效。
高级分页技术
上面的分页查询之所以有效,是因为查询中没有 WHERE 子句可能导致列表视图阈值错误。例如,无法检索我们之前看到的此查询的所有项目(这**将不起作用**)
<Query>
<Where>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match6k</Value>
</Eq>
</Where>
</Query>
如果您确实需要执行此类可能超出列表视图阈值的查询,那么您可以通过添加额外的 WHERE 子句来精心设计查询以实现分页效果。例如,通过添加 ID 上的过滤器,这**将起作用**
<Query>
<Where>
<And>
<And>
<Gt><FieldRef Name='ID'></FieldRef><Value Type='Number'>0</Value></Gt>
<Lt><FieldRef Name='ID'></FieldRef><Value Type='Number'>5000</Value></Lt>
</And>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match6k</Value>
</Eq>
</And>
</Where>
<View>
<OrderBy>
<FieldRef Name='ID' Ascending='True' />
</OrderBy>
<RowLimit>60</RowLimit>
</View>
</Query>
在上面的示例中,查询的第一部分将结果集缩小到 ID 介于 0 到 5000 之间的项目。这可以防止超出列表视图阈值的任何可能性。然后,它将这些项目过滤到 `IndexedCol = match6k` 的项目中。最后,RowLimit 确保只返回 60 个项目。
这种技术有几个含义
- 您无法预测返回多少结果,只能预测它小于或等于 RowLimit(在此示例中为 60)。
- 您可能需要重复运行查询以获得足够的结果
要检索下一页结果,您必须获取最后返回项目的 ID(最高 ID,假设我们按升序排序)。使用该 ID,形成一个新查询 - ID 大于该值,且小于该值加 5000。
例如,如果之前返回的最高 ID 是 2074,则要执行的下一个查询如下所示
<Query>
<Where>
<And>
<And>
<Gt><FieldRef Name='ID'></FieldRef><Value Type='Number'>2074</Value></Gt>
<Lt><FieldRef Name='ID'></FieldRef><Value Type='Number'>7074</Value></Lt>
</And>
<Eq>
<FieldRef Name='IndexedCol' />
<Value Type='Text'>match6k</Value>
</Eq>
</And>
</Where>
<View>
<OrderBy>
<FieldRef Name='ID' Ascending='True' />
</OrderBy>
<RowLimit>60</RowLimit>
</View>
</Query>
上述查询将可靠地返回项目,而不会超出列表视图阈值。只需重复此操作,直到您达到列表的最大 ID(您必须单独检索)。
不过,这种方法存在一个潜在问题。考虑以下场景
- 列表中有 100 万个项目
- 第一个匹配 `IndexedCol = 'match6'` 子句的项目是第 900,000 个项目
在这种情况下,查询将不得不运行 900,000 / 5000 = **180** 次,才能返回一个项目!
对这项技术有一个非常有效的改进,那就是智能地调整最小和最大 ID 以跨越大于 5000 的范围。您可以遵循以下规则
- 如果没有返回任何项目,则对于下一个查询,将 ID 跨度加倍(例如,将 5000 个项目增加到 10000 个)
- 如果超出列表视图阈值,则重复相同的查询,但将 ID 跨度减半(例如,将 10000 减少到 5000)
通过这种方式,查询只需运行 8 次即可开始检索项目。在第 8 次迭代时,当它命中项目时,它将尝试检索 635k 到 1.2m 的项目(大数字!)。如果此时超出列表视图阈值,那没关系——上述算法将确保范围随后缩小,直到成功运行。
不要忘记缓存结果,这样就不*需要*经常发生这种情况。
如果您希望调整性能,可以从小于 5000 的数字开始。同样,如果对您的数据集更有意义,您可以将“放大”因子增加三倍而不是两倍。
小于 & 大于
在涉及大量项目的 CAML 查询中,您不能使用**小于或等于 (Lte)** 或**大于或等于 (Gte)**。我不知道为什么,但它不起作用。请坚持使用**小于或大于 (Lt/Gt)**。
可索引字段类型
并非所有字段类型都可索引。例如,用户字段可以被索引并用于涉及大量项目的查询。但是,多用户字段不能。请参阅以下链接以获取更多信息:创建 SharePoint 索引列。
其他列表阈值
列表视图阈值并不是您唯一需要注意的限制!另一个列表限制是项目级权限限制,即 50,000 个项目。通常,权限是在列表级别设置的。但是,如果您选择为每个单独的列表项目设置唯一权限,那么您只能在任何给定列表中为 50k 个项目执行此操作。这是 SharePoint 中的一个硬性和绝对列表,如果您需要超过它,那么您需要将数据拆分到多个列表中。
有关 SharePoint 2013 中的边界和限制 的更多信息,请参阅以下页面
搜索 API
如果所有这些都难以处理,您可能需要考虑使用 搜索 API。我绝对会随时推荐它,而不是费力地处理 CAML 的细微差别!
但是,如果您选择坚持使用 CAML,希望本指南能有所帮助。请在评论中告诉我任何其他提示或技巧、错误或遗漏。同时,我将去角落里哭泣,并尝试接受所有这些 CAML 的可怕之处......