29

我基本上有经典的多对多模型。一个用户,一个奖项,以及用户和奖项之间的“多对多”表映射。

每个用户有大约 400 个奖励,每个奖励给大约 1/2 的用户。

我想遍历所有用户的奖励并总结他们的积分。在 SQL 中,它将是多对多之间的表连接,然后遍历每一行。在具有 MySQL 实例的体面机器上,400 行根本不是什么大问题。

在应用引擎上,我看到大约需要 10 秒来计算总和。大部分时间都花在 Google 的数据存储中。这是 cProfile 的前几行

   ncalls tottime percall cumtime percall filename:lineno(function)
      462 6.291 0.014 6.868 0.015 {google3.apphosting.runtime._apphosting_runtime___python__apiproxy.Wait}
      913 0.148 0.000 1.437 0.002 数据存储.py:524(_FromPb)
     8212 0.130 0.000 0.502 0.000 datastore_types.py:1345(FromPropertyPb)
      462 0.120 0.000 0.458 0.001 {google3.net.proto._net_proto___parse__python.MergeFromString}

我的数据模型错了吗?我做错了吗?这是我必须处理缓存和批量更新的一个缺点吗(这将是一个非常痛苦的事情)。

4

5 回答 5

20

可能两者兼而有之;-)

如果您在 Awards 表上执行 400 次查询,每个查询返回一个映射表上的查询,那么我预计这会很痛苦。查询有 1000 个结果的限制是因为 BigTable 认为返回 1000 个结果是其在合理时间内运行的能力的限制。根据该架构,我希望 400 个查询比返回 400 个结果的一个查询慢得多(400 log N vs. (log M) + 400)。

好消息是,在 GAE 上,memcache 包含所有奖项及其积分值的单个哈希表非常简单(好吧,当我不久前查看 memcache 文档时看起来非常简单。我不需要这样做然而)。

此外,如果您还不知道,for result in query.fetch(1000)它比 快得多for result in query,而且无论哪种方式,您都被限制为 1000 个结果。后者的优点是(1)如果你提前退出可能会更快,以及(2)如果谷歌将限制增加到超过 1000,它可以在不更改代码的情况下获得好处。

删除用户(或奖励)时,您也可能会遇到问题。我在一项测试中发现,我可以在时间限制内删除 300 个对象。这些对象比您的映射对象更复杂,具有 3 个属性和 5 个索引(包括隐式索引),而您的映射表可能只有 2 个属性和 2 个(隐式)索引。[编辑:刚刚意识到我在知道 db.delete() 可以获取列表之前进行了此测试,这可能要快得多]。

BigTable 不一定能做关系数据库设计好的事情。相反,它将数据很好地分布在许多节点上。但几乎所有网站都在单个数据库服务器上运行良好,存在瓶颈,因此并不严格需要 BigTable 所做的事情。

另一件事:如果您对单个 http 请求执行 400 次数据存储查询,那么您会发现在达到请求固定配额之前就已经达到了数据存储固定配额。当然,如果你在配额之内,或者如果你先打别的东西,那么这可能与你的应用程序无关。但是这两个配额之间的比例大约是 8:1,我将此作为 Google 期望我的数据模型看起来像的暗示。

于 2009-06-05T08:44:42.140 回答
19

我的数据模型错了吗?我做错了吗?

是的,是的,我害怕。

就您的数据模型而言,迄今为止处理此问题的最佳方法是将总和存储在用户记录中,并在用户获得/失去奖励时更新它。在绝大多数情况下,每次都计算他们的分数真的没有意义,它不会改变。如果您将“UserAward”实体类型设为“User”的子实体,您可以在单个原子事务中更新分数并插入或删除 UserAward 条目,确保您的计数始终准确。

onebyone 指出,您可以 memcache 奖项表。这是个好主意,但鉴于数据量有限,更好的办法是将其存储在本地内存中。全局成员在 HTTP 请求之间持续存在,并且由于我认为您不会经常更新奖励表,因此您不必担心缓存无效。只需在第一个请求时加载它(甚至将其硬编码到您的源代码中)。如果您确实更改了奖励列表,部署新的次要更新将重置所有实例,导致它们重新加载。

对于查找,请记住,执行数据存储操作的主要成本是往返时间。一个 get() 操作,按 ID 查找 1 个或多个记录(您可以批处理!)大约需要 20-40 毫秒。但是,查询大约需要 160-200 毫秒。因此,非规范化的力量。

于 2009-06-05T16:24:12.467 回答
2

应用引擎的一个重要习语是存储很便宜,但时间永远不会过剩。似乎在应用引擎中建立多对多关系的最佳方法是简单地存储双方的信息。IE 一个用户有一个奖励列表,每个奖励都有一个用户列表。要查找用户获得的所有奖励,您只需查询某个用户的奖励表。

这个想法在这里得到了很好的证明:Building Scalable Complex Apps

于 2010-10-13T14:22:23.410 回答
0

Google BigTable 在 Google 分布式文件系统上运行。

数据是分布式的。也许 400 行 mysql 仍然更好,但对于更大的数据,谷歌 BigTable 可能会更快。

我认为这就是为什么他们鼓励我们使用 memcache 来加快速度。

于 2009-06-05T08:44:10.470 回答
0

即使您提到 BigTable,我认为您是在云 SQL 上实现关系数据库。

你的模型很好,这是做这样的事情的正确方法。我认为没有充分的理由将聚合反规范化到用户表上。

您是否为快速表连接创建了索引。这很简单。您可能需要所有涉及表连接的字段的 BTree 索引。无需索引聚合字段(您获取 SUM)。基本上 N:N 表的两个外键都应该被索引。如果这些外键引用其他两个表的主键,那就足够了。

超过 100 行,外键上的简单 BTree 索引可以显着提高吞吐量。

我在 CloudSQL 上运行一个数据库,其中一些边缘表有超过 200 万条记录。只有在 250 万条记录之后,我才考虑进行一些非规范化,这也是一些额外的索引,并且仍在为 SUM 聚合。否则,每当添加新记录时,我都会对 SUM 字段进行不必要的更新。

只有当表超过 100 万条记录时,我们才不得不考虑使用只读副本。那时我们可以区分只读取某些表而不写入的进程。

如果你使用的是 Django,当你根据他们的文档实现 LIMIT 时要小心;因为它非常具有误导性。当您在记录集上 [:100](拼接)时,实际发送到 SQL 服务器的 SQL 并不是您所期望的。我很难弄清楚这一点。当您计划做一些会产生非常大规模的事情时,Django 不是一个好的选择。但是大约 1000 条记录就可以了。

于 2017-03-01T19:46:07.710 回答