使用 Django ORM (CROSS JOIN) 计算组合

Posted

技术标签:

【中文标题】使用 Django ORM (CROSS JOIN) 计算组合【英文标题】:Calculating Combinations using Django ORM (CROSS JOIN) 【发布时间】:2016-04-29 05:56:22 【问题描述】:

我有三个相关模型:ProcessFactorLevelProcessFactors 具有多对多关系,Factor 将具有一个或多个Levels。我正在尝试计算与Process 相关的Levels 的所有组合。使用 Python 的 itertools 作为模型方法,这很容易实现,但执行速度有点慢,所以我试图弄清楚如何使用 Django ORM 在 SQL 中执行此计算。

型号:

class Process(models.Model):
    factors = models.ManyToManyField(Factor, blank = True)

class Factor(models.Model):
    ...

class Level(models.Model):
    factor = models.ForeignKey(Factor, on_delete=models.CASCADE)

示例:一个进程'Running'涉及三个Factors('Distance''Climb''Surface'),每个进程由若干个Levels('Long'/'Short'、@987654341)组成@/'Hilly''Road'/'Mixed'/'Trail')。计算 SQL 中的组合将涉及通过首先确定涉及多少 Factors(本例中为 3 个)并多次执行所有级别的 CROSS JOIN 来构建查询。

在 SQL 中,这可以这样完成:

WITH foo AS
    (SELECT * FROM Level
     WHERE Level.factor_id IN
        (SELECT ProcessFactors.factor_id FROM ProcessFactors WHERE process_id = 1)
    )
SELECT a1.*, a2.*, a3.*
    FROM foo a1
    CROSS JOIN foo a2
    CROSS JOIN foo a3
WHERE (a1.factor_id < a2.factor_id) AND (a2.factor_id < a3.factor_id)

结果:

a1.name | a2.name | a3.name
--------------------------
Long    | Flat    | Road
Long    | Flat    | Mixed
Long    | Flat    | Trail
Long    | Hilly   | Road
Long    | Hilly   | Mixed
Long    | Hilly   | Trail
Short   | Flat    | Road
Short   | Flat    | Mixed
Short   | Flat    | Trail
Short   | Hilly   | Road
Short   | Hilly   | Mixed
Short   | Hilly   | Trail

目前,我已将此作为Process 模型上的方法实现为:

def level_combinations(self):
    levels = []
    for factor in self.factors.all():
        levels.append(Level.objects.filter(factor = factor))
    
    combinations = []
    for levels in itertools.product(*levels):
        combination = 
        
        combination["levels"] = levels
        
        combinations.append(combination)
    
    return combinations

这是否可以使用 Django ORM 实现,或者它是否足够复杂以至于应该将其实现为原始查询以提高 Python 代码实现的速度?

几年前有一个关于 performing CROSS JOIN in Django ORM 的类似问题(看起来像 Django v1.3)并没有引起太多关注(作者强调只使用 Python itertools)。

【问题讨论】:

【参考方案1】:

几年后,此解决方法实际上使用CROSS JOIN,但它确实单个中产生所需的结果查询。

第 1 步:将 cross 字段添加到您的 Factor 模型

class Factor(models.Model):
    cross = models.ForeignKey(
        to='self', on_delete=models.CASCADE, null=True, blank=True)
    ...

第 2 步:将 'Climb' 链接到 'Surface',并将 'Distance' 链接到 'Climb',使用新的 Factor.cross 字段

第三步:查询如下

Level.objects.filter(factor__name='Distance').values_list(
    'name', 'factor__cross__level__name', 'factor__cross__cross__level__name')

结果:

('Long', 'Flat', 'Road')
('Long', 'Flat', 'Mixed')
('Long', 'Flat', 'Trail')
('Long', 'Hilly', 'Road')
('Long', 'Hilly', 'Mixed')
('Long', 'Hilly', 'Trail')
('Short', 'Flat', 'Road')
('Short', 'Flat', 'Mixed')
('Short', 'Flat', 'Trail')
('Short', 'Hilly', 'Road')
('Short', 'Hilly', 'Mixed')
('Short', 'Hilly', 'Trail')

这是一个简化的示例。为了使其更通用,您可以添加一个带有两个外键的新 CrossedFactors 模型,而不是添加 Factor.cross 字段到 Factor。然后可以使用该模型来定义各种实验设计。

【讨论】:

【参考方案2】:
from itertools import groupby, product
    
def level_combinations(self):
    # We need order by factor_id for proper grouping
    levels = Level.objects.filter(factor__process=self).order_by('factor_id')
    # ['name': 'Long', 'factor_id': 1, ...,
    #  'name': 'Short', 'factor_id': 1, ...,
    #  'name': 'Flat', 'factor_id': 2, ...,
    #  'name': 'Hilly', 'factor_id': 2, ...]

    groups = [list(group) for _, group in groupby(levels, lambda l: l.factor_id)]
    # [['name': 'Long', 'factor_id': 1, ...,
    #   'name': 'Short', 'factor_id': 1, ...],
    #  ['name': 'Flat', 'factor_id': 2, ...,
    #   'name': 'Hilly', 'factor_id': 2, ...]]

    # Note: don't forget, that product is iterator/generator, not list
    return product(*groups)

如果顺序无关紧要,那么:

def level_combinations(self):
    levels = Level.objects.filter(factor__process=self)
    groups = 
    for level in levels:
        groups.setdefault(level.factor_id, []).append(level)
    return product(*groups.values())

【讨论】:

虽然这不是我希望的纯 Django ORM 解决方案(从 Django 1.9 开始似乎不存在),但它比我之前使用的代码快得多(timeit速度提高了大约 60%)。对于我的生产数据集,无序算法比有序方法快大约 10%。【参考方案3】:

如果我理解正确,你可以试试:

for process in Process.objects.all():
    # get all levels for current process
    levels = Level.objects.filter(factor__in=process.factors.all())

【讨论】:

以上是关于使用 Django ORM (CROSS JOIN) 计算组合的主要内容,如果未能解决你的问题,请参考以下文章

django orm中的INNER JOIN

Django ORM 在 SQL Join 中创建幻影别名

Django ORM queryset object 解释(子查询和join连表查询的结果)

将非 FK 条件添加到 Django ORM 中的 LEFT OUTER JOIN 以返回不连接的行

django外使用django ORM

Django ORM 数据库设置和读写分离