【问题标题】:How to model complex relational data in Firestore while limiting composite indexes?如何在限制复合索引的同时在 Firestore 中对复杂的关系数据进行建模?
【发布时间】:2020-11-06 15:29:30
【问题描述】:

首先感谢您阅读本文并提供任何建议和帮助的任何人。非常感谢。

我正在为我父亲的企业(专业承包商)开发一个小型定制 CRM(哎哟),我正在为我的数据库使用 Firestore。它应该非常精简,没有太多“金光闪闪”,但与他的专业承包业务密切相关,这很难让任何其他定制 CRM 应用到他的流程中。我已经取得了相当大的进展,并且实现了相当大的规模,但是随着一切都在扩展,我现在遇到了一些非常基本的问题。

我承认,在正确设置我的数据库结构时,我只有在关系数据库方面的经验(而且也没有太多经验)让我摸不着头脑,并且在使用 Firestore 时遇到了一些问题。我也是一个相当新手的开发人员,我觉得我正在处理一些超出我能力范围的东西。 (但现在这个旅程已经一年了,并没有太多转机)

到目前为止,我正在使用顶级集合来展示我在这里展示的内容。我最近开始将子集合用于其他一些次要功能,并开始质疑是否应该将其应用于所有内容。 我预见到的一个大问题是因为我想以多种方式查询,此时我已经消耗了近 100 个复合索引。还有很多东西要添加,所以我需要减少我当前和未来数据结构所需的复合索引的数量。

所以我有点确定,我的数据模型可能存在严重缺陷,需要改进/优化/更改。 (如果需要的话,我不介意这样做,但我对“如何”感到迷茫)我不需要特定的解决方案,但一般来说,可能只是一些关于可用方法的指针。我想我可能缺少一个“啊哈”的时刻。如果我理解一种模式,我通常可以将其进一步应用到其他领域。

我将把我的“销售线索集合”作为这篇文章的核心关注点,因为它有最多的查询变化。

所以我有一个像这样的主要顶级集合结构,但也想添加前缀,除了将 ID 写入其他文档之外,我将“存储”整个“客户”或“销售代表”对象/文档与其他文档和我有云函数,当有更新等时将遍历某些文档(为了避免额外的读取,即当我阅读 SalesLead 时,我不需要阅读 SalesRep 和客户文档,因为它们也被隐藏了/与 SalesLead 嵌套)

| /sales_reps        //SalesReps Collection
|   /docId          //Document ID
|   + salesRepId (document id)
|   + firstName
|   + lastName
|   + other employee/salesRep related info etc.

| /customers            //Customers Collection
|   /docId              //Document ID
|   + customerId (document id)
|   + firstName
|   + lastName
|   + address + other customer specific related info such as contact info (phone, email) etc.

从逻辑上讲,销售线索当然与客户相关联(一对多,一个客户可以有多个线索)。 下面提到的所有字段我都需要能够“查询”和“过滤”

| /sales_leads          //SalesLeads Collection
|   /docId              //Document ID
|   + customerId (document id) <- this is what I would query by to look for leads for a specific customer
|   + salesRepId (document id) <- this is what I would query by to look for leads for a specific sales Rep
|   + status <- (String: "Open", "Sold", "Lost", "On Hold)
|   + progress <- (String: "Started", "Appointment scheduled", "Estimates created", etc. etc., )
|   + type <- (String: New Construction or Service/Repair)
|   + jobTye <- (String: Different Types job Jobs related to what type of structures they are; 8-10 types right now)
|   + reference <- (String: How the lead was referred to the company, i.e. Facebook, Google, etc. etc. );
|   + many other (non queryable) data related to a lead, but not relevant here...

SalesEstimates 以一对多的关系与潜在客户相关。 (一个领导可以有很多估计)但是估计与这个讨论并不完全相关,但无论如何只想包括它。不过,我以与潜在客户非常相似的方式查询和过滤估计值。 (类似领域等)

| /sales_estimates      //SalesEstimates Collection
|   /docId              //Document ID
|   + salesLeadId (document id) <- this is what I would query by to look for estimates for a specific lead
|   + customerId (document id) <- this is what I would query by to look for estimates for a specific customer
|   + salesRepId (document id) <- this is what I would query by to look for estimates for a specific sales Rep
|   + specific sales Lead related data etc....

在客户端的“销售线索列表”中,我有一些下拉框作为过滤器,其中包含值(即销售代表),但也有一个选项/值“全部”来否定任何过滤。

所以我会开始组装一个查询:

Query query = db.collection("sales_leads");

//Rep
if (!salesRepFilter.equals("All")) { //Typically only Managers/Supervisors woujld be able to see "all leads" whereas for a SalesRep this would be set on his own ID by default.
    query = query = query.whereEqualTo("salesRepId", salesRepId);  
}
//Lead Status (Open, Sold, Lost, On Hold)
if (!statusFilter.contains("All")) {
    query = query.whereEqualTo("status", statusFilter);
}
//Lead Progress
if (!progressFilter.contains("All")) {
    query = query.whereEqualTo("progress", progressFilter);
}
//Lead Type
if (!typeFilter.contains("All")) {
    query = query.whereEqualTo("leadType", typeFilter);
}
//Job Type
if (!jobTypeFilter.contains("All")) {
    query = query.whereArrayContains("jobTypes", jobTypeFilter);
}
//Reference
if (!referenceFilter.contains("All")) {
    query = query.whereEqualTo("reference", referenceFilter);
}

此外,我可能希望将整个查询减少到单个客户(这通常意味着跳过所有其他过滤器并“显示 this 客户的所有潜在客户)。如果用户打开客户页面/详细信息,然后单击“显示此客户的潜在客户”之类的内容。

//Filter by Customer (when entering my SalesLead List from a Customer Card/Page where user clicked on "Show Leads for this Customer")
if (filterByCustomer) {
    query = query.whereEqualTo("customerId", customerFilter);
}

//And at last I want to be able to query the date Range (when the lead was created) and also sort by "oldest" or "newest"

//Date Range
query = query.whereGreaterThan("leadCreatedOnDate", filterFromDate); 
        .whereLessThan("leadCreatedOnDate", filterToDate;

//Sort Newest vs Oldest

if (sortByNewest) { //either newest or oldest
    query = query.orderBy("leadCreatedOnDate", Query.Direction.ASCENDING);
} else {
    query = query.orderBy("leadCreatedOnDate", Query.Direction.DESCENDING);
}

这将完成我对销售线索的查询。这一切都很好现在,但我很担心继续前进并最终达到复合索引的限制。我没有确切的数字,但我可能会为我的 sales_leads 集合提供 25-30 个综合索引。 (哎呀!)

不仅有许多字段可供查询,所需的复合索引的数量还要乘以可能的过滤器集的组合。 (呃)

我需要能够查询所有潜在客户,然后按上述字段过滤它们(在描述我的 sales_leads 集合时)。

因此,与其将所有这些集合保留为顶级集合,我猜测我应该以某种方式通过娱乐子集合来重组我的数据库,但我尝试用不同的方法对此进行建模,但似乎总是碰壁。

我想我可以将“sales_leads”作为每个客户对象下的子集合,并且可以使用集合组查询来检索“所有潜在客户”,但是那些需要复合索引,对吗?所以这只是一个可搜索字段的权衡。 (..撞墙..)

抱歉,篇幅较长。我希望它是可读的。我感谢任何帮助、反馈和意见。我处于一个非常焦虑和沮丧的境地。

如果这不起作用,我可能需要考虑专业咨询。

谢谢!

【问题讨论】:

    标签: firebase google-cloud-firestore


    【解决方案1】:

    以下是我认为对您有帮助的几件事。

    首先,在 YouTube 上观看 AWS re:Invent 2018: Amazon DynamoDB Deep Dive。这是关于 DynamoDB,但 DynamoDB 是一个与 Firestore 非常相似的 NoSQL 数据库,并且这些概念普遍适用。在视频的中途,Rick 以一家像您这样的公司为例,您可能会惊讶地发现他仅通过数据建模就能有效地减少查询次数。

    其次,熟悉 Firestore 的 index merging。在像您这样的情况下,最好手动创建复合索引,或者至少手动审核它们,因为 Firestore 的自动索引并不能保证最有效的复合索引菜单。请记住,复合索引是根据您执行查询的顺序自动创建的,如果您稍后执行查询,通过取消先前的索引可以更好地结构化,Firestore 不会返回并为您删除它 - 您必须这样做。

    我对销售线索查询消耗 25-30 个复合索引这一事实高度怀疑;考虑到文档中有多少字段被索引,这个数字对我来说似乎太高了。在你做任何事情之前——当然,在观看视频和研究索引合并之后——我会完全专注于这个集合。您必须完全确定此集合需要使用的最大复合索引数。也许创建一个虚拟集合并尝试索引合并并真正了解它的工作原理,因为仅此一项就可以解决您的所有问题。如果 Firestore 无法处理贵公司的用例,我会感到震惊。

    第三,不要害怕对数据进行非规范化。 NoSQL 的基本前提是真正的非规范化——也就是说,数据存储确实应该是您最不关心的问题,而计算/操作确实应该是您最关心的问题。如果您可以通过在多个集合中的多个文档上复制数据来减少查询计数,那么当替代方案达到 200 个复合索引时,您必须这样做。

    【讨论】:

    • 感谢您的建设性反馈。我不知道索引合并以及它是如何工作的。仅此一项就有助于立即减少我的不良行为。 (但可能需要两个更进一步)这就是为什么有这么多的indeces。我刚刚使用了 Firestore 的“自动索引”(为我正在执行的任何查询创建索引提供的链接)我猜我可以将其减少到不到一半,只是“浏览索引合并”。在接受您的答案之前,我会看看是否有其他 cmets 进来,但这绝对非常有用。非常感激。谢谢
    • 更新:仅仅通过索引合并,我能够将我的综合指数从 95 减少到大约 50,甚至还没有完成这项任务(删除索引合并覆盖的所有不必要的索引)。通过我所有的收藏。我猜我完成后会坐在 35 到 45 之间。您对手动评估这些指数是绝对正确的。我将在稍后或明天观看 DynamoDB 视频,然后深入了解 noSQL 数据结构中的数据非规范化。感谢您的洞察力。
    猜你喜欢
    • 2011-04-26
    • 2020-01-30
    • 2019-08-27
    • 2019-05-16
    • 2013-02-03
    • 1970-01-01
    • 2017-08-04
    • 2013-03-25
    相关资源
    最近更新 更多