【问题标题】:O(1) Django ORM strategy to query related objects of related objectsO(1) Django ORM策略查询相关对象的相关对象
【发布时间】:2015-03-07 05:37:04
【问题描述】:

Foo和Bar通过Baz的关系如下:

class Foo(Model):
   # stuff

class Bar(Model)
   # stuff

class Baz(Model):
   foos = ManyToManyField("Foo")
   bar = ForeignKey("Bar")

我基本上需要生成以下表示Bars 的字典,这些字典与每个FooBaz 相关(在字典理解伪代码中):

{ foo.id: [通过任何 baz 与 foo 相关的唯一条的列表] for foo in all foos}

我目前可以使用 O(N) 查询(每个 Foo 1 个查询)生成我的数据结构,但是对于大量数据,这是一个瓶颈,我需要将其优化到 O(1)(不是单个查询本身,但无论任何模型的数据大小如何,查询的数量都是固定的),同时还最小化了 python 中数据的迭代。

【问题讨论】:

  • 你将如何用 SQL 编写它?
  • 如果我知道的话,我可以把它翻译成 Django。我精通基本 SQL,但我不是 OUTER JOIN 忍者。 :)

标签: django


【解决方案1】:

如果您可以使用 SQL,则可以使用单个查询(应用程序名称应作为所有表名称的前缀):

select distinct foo.id, bar.id
from baz_foos
join baz on baz_foos.baz_id = baz.id
join foo on baz_foos.foo_id = foo.id
join bar on baz.bar_id = bar.id

baz_foos 是 Django 创建的多对多表。

@Alasdair 的解决方案可能/可能更具可读性(尽管如果您出于性能原因这样做可能不是最重要的)。他的解决方案恰好使用了两个查询(几乎没有区别)。我看到的唯一问题是如果你有大量的 Baz 对象,因为生成的 sql 看起来像这样:

SELECT "foobar_baz"."id", "foobar_baz"."bar_id", "foobar_bar"."id" 
FROM "foobar_baz" 
INNER JOIN "foobar_bar" ON ("foobar_baz"."bar_id" = "foobar_bar"."id")

SELECT
    ("foobar_baz_foos"."baz_id") AS "_prefetch_related_val", 
    "foobar_foo"."id" 
FROM "foobar_foo" 
INNER JOIN "foobar_baz_foos" ON ("foobar_foo"."id" = "foobar_baz_foos"."foo_id") 
WHERE "foobar_baz_foos"."baz_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 
    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 
    55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 
    75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 
    95, 96, 97, 98, 99, 100, 101)

如果你只有几个 Bar 和几百个 Foo,我会这样做:

from django.db import connection
from collections import defaultdict

# foos = {f.id: f for f in Foo.objects.all()}
bars = {b.id: b for b in Bar.objects.all()}

c = connection.cursor()
c.execute(sql)  # from above
d = defaultdict(set)
for f_id, b_id in c.fetchall():
    d[f_id].add(bars[b_id])

【讨论】:

  • 是的,不幸的是,在数据集中,baz 通常是最大且限制最少的(通常不超过几个 bar,可能 30-100 foos)所以我可以继续编写 sql如果我能弄清楚如何让它与 python 一起玩。
  • 我已经更新了答案(解决方案基本上是在Bar 上手动执行“prefetch_related”)。
  • 我确实喜欢 Alasdair 方法的直接性和简洁性,但这对于优化是关键且 Baz 拥有大数据集的情况非常有用。在这里学到了一些好东西。谢谢,比约恩。
【解决方案2】:

使用select_relatedprefetch_related,我认为您可以通过2 个查询构建所需的数据结构:

out = {}
bazes = Baz.objects.select_related('bar').prefetch_related('foos')
for baz in bazes:
    for foo in baz.foos.all():
        out.setdefault(foo.id, set()).add(baz.bar)

输出字典的值是集合,而不是您问题中的列表,以确保唯一性。

【讨论】:

  • 为什么不使用collections.defaultdict(set)
  • using a default dict in a Django template 时有个问题。没有其他原因。使用默认字典就可以了,如果需要,您可以在之后转换为常规字典。
  • 1.7 文档中关于大型数据集的有趣部分:“在大多数情况下,prefetch_related 将使用使用“IN”运算符的 SQL 查询来实现。这意味着对于大型 QuerySet,大型“IN”可能会生成子句,这取决于数据库,在解析或执行 SQL 查询时可能会出现性能问题。始终针对您的用例进行分析!"
  • 是的,我注意到 prefetch_related 基本上使用带有 ID 列表的 IN 子句。我会在实现它时对其进行概要分析,看看我们在看什么。
  • 因为它非常简单,所以我认为这是所有问题的正确答案,除了需要保证@thebjorn 响应的复杂性的特殊情况(优化至关重要且 baz 拥有庞大的数据集)。
猜你喜欢
  • 2020-07-16
  • 2018-11-18
  • 1970-01-01
  • 1970-01-01
  • 2018-10-14
  • 1970-01-01
  • 2013-05-19
  • 2011-02-19
相关资源
最近更新 更多