在 Appengine 数据存储上查询 N 条随机记录
Posted
技术标签:
【中文标题】在 Appengine 数据存储上查询 N 条随机记录【英文标题】:Querying for N random records on Appengine datastore 【发布时间】:2010-11-09 10:52:08 【问题描述】:我正在尝试编写返回 N 个特定类型随机记录的 GQL 查询。我当前的实现有效,但需要对数据存储进行 N 次调用。如果可能的话,我想对数据存储进行 1 次调用。
我目前为放入数据存储区的每种类型分配一个随机数。当我查询随机记录时,我生成另一个随机数并查询记录 > rand ORDER BY asc LIMIT 1。
这可行,但是,它只返回 1 条记录,所以我需要进行 N 次查询。关于如何进行这一查询的任何想法?谢谢。
【问题讨论】:
我为此创建了一个问题,您可以为它加注星标以帮助修复它:code.google.com/p/googleappengine/issues/detail?id=9044 【参考方案1】:“幕后”单个搜索查询调用只能从某个索引返回一组连续的行。这就是为什么某些 GQL 查询(包括对 != 的任何使用)扩展到多个数据存储调用的原因。
N 个独立的均匀随机选择(通常)在任何索引中都不连续。
QED。
您可能可以使用 memcache 来存储实体,并降低抓取 N 个实体的成本。或者,如果您不介意索引中的“随机”选择靠近在一起,请在一个查询中选择一个(例如)100 个随机选择的块,然后从中随机选择 N 个。由于您有一个已经随机化的字段,因此外人不会立即看出这 N 个项目是相关的。至少,直到他们查看大量样本并注意到项目 A 和 Z 从未出现在同一组中,因为它们在随机索引中相距超过 100。如果性能允许,您可以不时重新随机化您的实体。
【讨论】:
谢谢 - 我确实需要随机结果,所以我想我将不得不使用多个数据存储调用。我会尽量减少 N。 这不是真的。 batch operations 和IN
查询运算符都可以返回不连续的实体。
@ryan:与!=
相同。那和IN
都被实现为有限数量的子查询。批处理操作与该问题并不真正相关,但是是的,某些操作确实作用于在任何索引中都不连续的实体。只是不搜索。
好点!抱歉,我错过了您答案的“查询”部分。总的来说,你绝对是对的。有一个例外:merge join queries 确实返回非连续实体。它们在发布时仅用于一小类查询,但最近随着 Alfred 在 article 中描述的高级查询功能,它们发展迅速。
@ryan:实际上我编辑了我的答案以回应您的评论,所以我认为您第一次没有错过任何内容。之前,我不清楚我所指的“数据存储调用”是查询。有趣的是 zigzag 合并连接——我想我可能可以用一些引理来挽救我的“证明”,即它执行的查询等同于 在可能已经构建但实际上的索引中连续的查询不是。但我不确定,所以我不会尝试。每次我尝试否定回答 GAE 问题时,我都曾评论过,你们添加了功能:-)【参考方案2】:
您在寻找什么样的权衡?如果您愿意忍受插入这些实体时对性能的小幅影响,您可以创建一个解决方案来快速获取其中的 N 个。
这是你需要做的:
当您插入实体时,请指定密钥。您想按顺序为您的实体提供密钥,从 1 开始并从那里向上。 (这需要一些努力,因为应用引擎没有 autoincrement() 所以你需要跟踪你在其他实体中使用的最后一个 id,我们称之为 IdGenerator)
现在,当您需要 N 个随机实体时,生成 N 个介于 1 和您生成的最后一个 id 之间的随机数(您的 IdGenerator 会知道这一点)。然后,您可以使用 N 个键按键进行批量获取,这只需要一次访问数据存储区,并且也比查询更快,因为键获取通常比查询快,AFAIK。
这种方法确实需要处理一些烦人的细节:
-
如果您动态插入大量此类项目(超过几秒钟),您的 IdGenerator 可能会成为瓶颈,这需要某种分片 IdGenerator 实现。如果所有这些数据都是预加载的,或者不是大量数据,那么您就很容易了。
您可能会发现某些 Id 实际上不再具有与之关联的实体,因为您将其删除或因为 put() 在某处失败。如果发生这种情况,您将不得不抓取另一个随机实体。 (如果您想花哨并降低这种可能性,您可以将此 Id 提供给 IdGenerator 以重复使用以“填补漏洞”)
所以问题归结为您需要这 N 个项目的速度与添加和删除它们的频率,以及增加一点复杂性是否值得提高性能。
【讨论】:
您可以或多或少地使用 App Engine 的内置 ID 编号来实现这一点 - 如果您知道最大 ID,您可以随机选择一些统一的。有些不存在,所以用新的随机 id 重试它们,等等。如果您的 ID 空间很密集,这将正常工作。 甜蜜。我不知道我们可以依靠内置编号从 1 开始,然后从那里 1 个 1 上升。 您不能 - 但它会以块的形式分配,并且只要这些块大部分被使用,您的重试次数应该足够小,以便于管理。【参考方案3】:看起来唯一的方法是将随机整数值存储在每个实体的特殊属性中并对其进行查询。如果您只添加一个自动初始化的属性,这可以完全自动完成。
不幸的是,如果您的数据存储区已经填满,这将需要对所有实体进行一次处理。
这很奇怪,我知道。
【讨论】:
我认为这是一个很好的方法,并且符合 NoSQL 模型,即通过写入而不是读取来完成工作。当然,这不会是完全随机的——如果你总是得到 N 个连续的条目,那么用户偶尔会看到相同的记录彼此相邻。但这对于 OP 来说可能是随机的。 (您还可以创建几个 - 甚至数百个 - 具有不同随机数的属性,并轮换您从中绘制的索引。)【参考方案4】:我同意史蒂夫的回答,没有办法在一个查询中检索 N 个随机行。
但是,即使是检索单个实体的方法通常也无法正常工作,因此返回结果的概率是均匀分布的。返回给定实体的概率取决于其随机分配的数字与下一个更高的随机数的差距。例如。如果分配了随机数 1,2 和 10(并且没有分配任何数字 3-9),则算法返回“2”的频率是“1”的 8 倍。
我已经以稍微昂贵的方式解决了这个问题。如果有人感兴趣,我很乐意分享
【讨论】:
【参考方案5】:我也遇到了同样的问题。我决定不将 ID 分配给数据存储中已经存在的条目并这样做,因为我已经从分片计数器中获得了总数。
这会从“totalcount”条目中选择“count”个条目,按key排序。
# select $count from the complete set
numberlist = random.sample(range(0,totalcount),count)
numberlist.sort()
pagesize=1000
#initbuckets
buckets = [ [] for i in xrange(int(max(numberlist)/pagesize)+1) ]
for k in numberlist:
thisb = int(k/pagesize)
buckets[thisb].append(k-(thisb*pagesize))
logging.debug("Numbers: %s. Buckets %s",numberlist,buckets)
#page through results.
result = []
baseq = db.Query(MyEntries,keys_only=True).order("__key__")
for b,l in enumerate(buckets):
if len(l) > 0:
result += [ wq.fetch(limit=1,offset=e)[0] for e in l ]
if b < len(buckets)-1: # not the last bucket
lastkey = wq.fetch(1,pagesize-1)[0]
wq = baseq.filter("__key__ >",lastkey)
请注意,这对我来说有些复杂,而且我仍然不相信我没有错误或错误。
请注意,如果 count 接近 totalcount 这可能会非常昂贵。 请注意,在数百万行上,可能无法在 appengine 时间范围内完成。
【讨论】:
【参考方案6】:如果我理解正确,你需要检索 N 个随机实例。
这很容易。只需使用键进行查询。并对键的列表结果执行 random.choice N 次。然后通过获取键来获取结果。
keys = MyModel.all(keys_only=True)
n = 5 # 5 random instance
all_keys = list(keys)
result_keys = []
for _ in range(0,n)
key = random.choice(all_keys)
all_keys.remove(key)
result_keys.append(key)
# result_keys now contain 5 random keys.
【讨论】:
如果您的数据存储中有一百万个实体?从数据存储中加载所有密钥 - 看起来很糟糕...... @aloo 如果你有这么多实例,你可以在数据存储和内存缓存中跟踪它们的总数,然后你只需在 0 和总数之间的数字范围内执行random.choice
。在您使用索引迭代键之后,您生成的。或者只使用限制和偏移量。以上是关于在 Appengine 数据存储上查询 N 条随机记录的主要内容,如果未能解决你的问题,请参考以下文章
是否可以为 appengine 数据存储实体获取 Google 电子表格的数据源 URL?