如何编写带有子查询的 Django 查询作为 WHERE 子句的一部分?

Posted

技术标签:

【中文标题】如何编写带有子查询的 Django 查询作为 WHERE 子句的一部分?【英文标题】:How do I write a Django query with a subquery as part of the WHERE clause? 【发布时间】:2019-09-02 10:39:08 【问题描述】:

我正在使用 Django 和 Python 3.7。我无法弄清楚如何编写一个 Django 查询,其中有一个子查询作为 where 子句的一部分。这是模型...

class Article(models.Model):
    objects = ArticleManager()
    title = models.TextField(default='', null=False)
    created_on = models.DateTimeField(auto_now_add=True)


class ArticleStat(models.Model):
    objects = ArticleStatManager()
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
    elapsed_time_in_seconds = models.IntegerField(default=0, null=False)
    votes = models.FloatField(default=0, null=False)


class StatByHour(models.Model):
    index = models.FloatField(default=0)
    # this tracks the hour when the article came out
    hour_of_day = IntegerField(
        null=False,
        validators=[
            MaxValueValidator(23),
            MinValueValidator(0)
        ]
    )

在 PostGres 中,查询类似于

SELECT *
FROM article a,
     articlestat ast
WHERE a.id = ast.article_id
  AND ast.votes > 100 * (
    SELECT "index" 
    FROM statbyhour 
    WHERE hour_of_day = extract(hour from (a.created_on + 1000 * interval '1 second')))

注意子查询是 WHERE 子句的一部分

ast.votes > 100 * (select index from statbyhour where hour_of_day = extract(hour from (a.created_on + 1000 * interval '1 second'))) 

所以我想我可以做这样的事情......

hour_filter = Func(
    Func(
        (F("article__created_on") + avg_fp_time_in_seconds * "interval '1 second'"),
        function='HOUR FROM'),
    function='EXTRACT')
...
votes_criterion2 = Q(votes__gte=F("article__website__stats__total_score") / F(
    "article__website__stats__num_articles") * settings.TRENDING_PCT_FLOOR *
                                StatByHour.objects.get(hour_of_day=hour_filter) * day_of_week_index)
qset = ArticleStat.objects.filter(votes_criterion1 & votes_criterion2,
                                  comments__lte=25)

但这会导致“无法将关键字 'article' 解析为字段。选项有:hour_of_day、id、index、num_articles、total_score”错误。我认为这是因为 Django 在运行其中的较大查询之前正在评估我的“StatByHour.objects”查询,但我不知道如何重写以使子查询同时运行。

编辑: K,将我的子查询移动到一个实际的“子查询”函数中,并引用了我使用 OuterRef 创建的过滤器...

hour_filter = Func(
    Func(
        (F("article__created_on") + avg_fp_time_in_seconds * "interval '1 second'"),
        function='HOUR FROM'),
    function='EXTRACT')
query = StatByHour.objects.get(hour_of_day=OuterRef(hour_filter))


...
votes_criterion2 = Q(votes__gte=F("article__website__stats__total_score") / F(
    "article__website__stats__num_articles") * settings.TRENDING_PCT_FLOOR *
                                Subquery(query) * 
                 day_of_week_index)
qset = ArticleStat.objects.filter(votes_criterion1 & votes_criterion2,
                                  comments__lte=25)

这会导致

This queryset contains a reference to an outer query and may only be used in a subquery.

这很奇怪,因为我在子查询中使用它。

编辑#2:即使在根据给出的答案更改查询之后......

hour_filter = Func(
    Func(
        (F("article__created_on") + avg_fp_time_in_seconds * "interval '1 second'"),
        function='HOUR FROM'),
    function='EXTRACT')
query = StatByHour.objects.filter(hour_of_day=OuterRef(hour_filter))[:1]

...
votes_criterion2 = Q(votes__gte=F("article__website__stats__total_score") / F(
    "article__website__stats__num_articles") * settings.TRENDING_PCT_FLOOR *
                                Subquery(query) *
                                day_of_week_index)
qset = ArticleStat.objects.filter(et_criterion1 & et_criterion2 & et_criterion3,
                                  votes_criterion1 & votes_criterion2,
                                  article__front_page_first_appeared_date__isnull=True,
                                  comments__lte=25)

我仍然得到错误

'Func' object has no attribute 'split'

【问题讨论】:

查看Subquery。带有OuterRef 的子查询在执行时解析对外部查询的引用,从而解决了您的问题。如果它不起作用,请用您的尝试更新问题。 我用更新编辑了我的问题。因此,我使用我认为正确的 OuterRef 将内容放入子查询中,但得到了令人困惑的错误,“此查询集包含对外部查询的引用,并且只能在子查询中使用。”。 【参考方案1】:

Subqueries 需要是不立即评估的查询,以便可以推迟评估,直到运行外部查询。 get() 不符合要求,因为它会立即执行并返回一个对象实例而不是 Queryset

但是,将filter 替换为get,然后采用[:1] 切片应该可以:

StatByHour.objects.filter(hour_of_day=OuterRef('hour_filter')).values('hour_of_day')[:1]

注意OuterRef 中的字段引用是字符串文字而不是变量。

此外,子查询需要返回单列和单行(因为它们被分配给单个字段),因此values() 和上面的切片。

另外,我还没有在Q 对象中使用子查询;我不确定它会起作用。您可能必须先将子查询输出保存在注释中,然后将其用于过滤器计算。

【讨论】:

我认为你的意思是让 hour_filter 不带单引号来镜像我创建的过滤器变量(使用单引号错误“无法将关键字 'hour_filter' 解析为字段。选择有:article、article_id、elapsed_time_in_seconds , id, votes”出现)。但即使没有单引号,我也会收到错误消息,“'Func' 对象没有属性 'split'”,即使我的过滤器中的任何地方都没有“split”一词。 'hour_filter' 引用外部查询中的(带注释的)字段(您需要创建)。 OuterRef 需要一个字段名称,而不是 Func 表达式。您提到的错误是语法错误,例如右括号在错误的位置。【参考方案2】:

我发现尽可能多地进入注释以澄清发生了什么是有帮助的。

您可以使用Extract 函数来获取小时。如果你想合并更复杂的avg_fp_time_in_seconds 东西,你需要定义自己的Func,我没有尝试复制它,因为它应该有自己的帖子(这可能是'Func' object has no attribute 'split' 错误来自)。

# First, add a field for the hour 
articles_with_hour = Article.objects.annotate(created_on_hour=ExtractHour('created_on'))

# Set up the subquery, referencing the annotated field
for_this_hour = StatByHour.objects.filter(hour_of_day=OuterRef('created_on_hour'))

# Add the subquery, making sure to slice down to one value
articles_with_hour_index = articles_with_hour.annotate(
    index_for_this_hour=Subquery(for_this_hour.values('index')[:1]),
)

# Add the website averages for later calculations 
#  (note if total_score and num_articles are different field types
#  you may need an ExpressionWrapper)
articles_with_avg_website_score = articles_with_hour_index.annotate(
    average_article_score_for_website=(
        F("website__stats__total_score") / F("website__stats__num_articles")
    )
)

# Use the averages to calculate the trending floor for each article
articles_with_trending_floor = articles_with_avg_website_score.annotate(
    trending_floor=F('average_article_score_for_website') * settings.TRENDING_PCT_FLOOR,
)

# Set up the criteria, referencing fields that are already annotated on the qs
# ...
votes_gte_trending_floor_for_this_hour_criterion = Q(articlestats__votes__gte=(
    F('trending_floor')
    * F('index_for_this_hour')
    * day_of_week_index  # not sure where this comes from?
))
# ...

# Then just filter down (note this is an Article QuerySet, not ArticleStat)
qset = articles_with_trending_floor.filter(
    votes_gte_trending_floor_for_this_hour_criterion,
    # other criteria
    front_page_first_appeared_date__isnull=True,
    articlestats__comments__lte=25,
)

这些计算中的许多可以被压缩,甚至可以使用多个 kwargs 在单个 annotate 调用中完成所有操作,但我认为将它们全部展开更容易理解。

【讨论】:

谢谢你,虽然我无法从你的回答中弄清楚你在哪里写了与“Subquery(query)”子句等效的子句,这似乎给我带来了很多问题。 子查询包含在注释index_for_this_hour=Subquery(for_this_hour.values('index')[:1]) 中,它为您提供了一个新字段“index_for_this_hour”,从那时起(在votes_gte_trending_floor_for_this_hour_criterion 内)可以像普通字段一样使用。 docs.djangoproject.com/en/2.2/ref/models/expressions/… 的所有子查询示例都使用此构造;我不确定是否支持在 Q 对象中像 F 对象一样使用它(正如 Endre Both 建议的那样)。【参考方案3】:

这确实是一个Subquery 解决方案。

Django >= 1.11

作为警告,我确实测试了代码,但仅使用模型,我没有任何数据,所以,这个答案只是为了指出你正确的方向

# Query that references an outer field from another model, in this case created_on.
# On wich we are performing a lookup in order to "extract" the hour (assuming here)
# a DateTimeField or a TimeField.
stat_by_hour = StatByHour.objects.filter(hour_of_day=OuterRef('created_on__hour'))


# Then filter articles, that have articlestats.votes 
# greater than 100 * stat_by_hour.index
result = Article.objects.filter(
    articlestats__votes__gt=100 * Subquery(stat_by_hour.values('index')[:1], output_field=FloatField())
)

乍一看,您可能需要在子查询中执行order_by('index')order_by('-index'),这样切片[:1] 将获得最小值或最大值(取决于您的需要)。

我相信你可以使用这个(或非常相似的东西)来实现你想要的。

【讨论】:

我不断收到同样的错误——“'Func' 对象在该行没有属性 'split'”“articlestats__votes__gt=100 * Subquery(stat_by_hour.values('index')[:1] , output_field=FloatField())" .希望 Django 更清楚它想要什么。【参考方案4】:

看看Django queries。我认为您可以通过将 SQL 基础查询更改为 Django 提供的查询来解决问题。

如果不起作用,您可以perform raw SQL queries。

【讨论】:

【参考方案5】:

使用通过hour_of_day=ExtractHour(OuterRef('article__created_on') + timedelta(seconds=avg_fp_time_in_seconds)) 过滤的子查询进行过滤。真正的代码需要一个额外的ExpressionWrapper,并且仅适用于Django >= 2.1.0

import datetime

from django.db import models
from django.db.models import F, OuterRef, Subquery, Value
from django.db.models.functions import ExtractHour, Coalesce
from django.db.models.expressions import ExpressionWrapper


relevant_hour_stats = (
    StatByHour.objects
    .filter(
        hour_of_day=ExtractHour(ExpressionWrapper(
            OuterRef('article__created_on')  # NOTE: `OuterRef()+Expression` works only on Django >= 2.1.0
            +
            datetime.timedelta(seconds=avg_fp_time_in_seconds),
            output_field=models.DateTimeField()
        )),
    )
    .annotate(
        votes_threshold=Coalesce(
            100.0 * F('index'),
            0.0,
            output_field=models.FloatField(),
        ),
    )
    .order_by('-votes_threshold')
    # NOTE: your StatByHour model does not have unique=True on hour_of_day
    # field, so there may be several stat for same hour.
    # And from your SQL example it's unclear how should they be handled. So I
    # assume that "greatest" threshold is needed.
)

article_stats = (
    ArticleStat.objects
    .all()
    .filter(
        votes__gt=Coalesce(
            Subquery(relevant_hour_stats.values('votes_threshold')[:1]),
            Value(0.0),
            output_field=models.FloatField(),
        ),
    )
)

附:如果你在 github 上设置一些“演示项目”,这样任何人都可以克隆它并在本地检查他们的想法,那会容易得多。

附言此代码经过测试可以正常工作,但适用于不同的模型/字段:

In [15]: relevant_something = (ModelOne.objects.filter(index=ExtractHour(ExpressionWrapper(OuterRef('due_date') + datetime.timedelta(seconds=1000), output_field=models.DateTimeField()))).annotate(votes_threshold=100*F('indent')).order_by('-votes_threshold'))

In [16]: ts = ModelTwo.objects.all().filter(votes__gt=Subquery(relevant_notes.values('votes_threshold')[:1], output_field=models.IntegerField()))

In [17]: print(ts.query)
SELECT 
    ...
FROM 
    "some_app_model_two" 
WHERE 
    "some_app_model_two"."votes" > (
        SELECT 
            (100 * U0."indent") AS "votes_threshold" 
        FROM 
            "some_app_model_one" U0 
        WHERE 
            U0."index" = (
                EXTRACT(
                    'hour' 
                    FROM ("some_app_model_two"."due_date" + 0:16:40) 
                    AT TIME ZONE 'America/Los_Angeles'
                )
            ) 
        ORDER BY "votes_threshold" DESC 
        LIMIT 1
    )
ORDER BY 
    "some_app_model_two"."due_date" ASC, 
    "some_app_model_two"."priority" ASC, 
    "some_app_model_two"."updated_at" DESC

因此,如果您遇到任何错误,请显示您正在运行的实际代码

【讨论】:

谢谢——一个问题。您正在使用 ExtractHour("created_on"),这与我正在做的事情很接近,但并不完全一样。我想从表达式 'F("article__created_on") + avg_fp_time_in_seconds * "interval '1 second'"' 中提取小时,其中 "avg_fp_time_in_seconds" 只是一个数字,为​​简单起见,我们可以将其称为 1000。如何将所有其他内容合并到 ExtractHour 函数中? django 表达式可以理解 timedelta 对象...所以它可以理解这个:ExtractHour(ExpressionWrapper(F('article__created_on') + datetime.timedelta(seconds=avg_fp_time_in_seconds)))。更新了我的答案 @Dave,我知道为什么您的尝试使用object has no attribute 'split' 失败了:OuterRef 仅支持字段名称(不支持 Func(...) 之类的表达式)。您的示例代码的其他问题:1000* "interval '1 second'" 将导致字符串重复 1000 次。并且字符串也被认为是字段名称,并将转换为“table_name.VALUE”。即使您使用Value("interval '1second'"),它也会在 SQL 中作为字符串转义。所以正确的方法是直接使用datetime.timedelta,如我的回答所示 好的,取得一些进展。我现在得到一个“表达式包含混合类型。您必须设置 output_field。” “votes_above_threshold__gt=0”行出错。 “votes”是一个 FloatField,所以我不知道它在抱怨什么。 使用ExpressionWrapper(..., output_field=models.SomeField()) 围绕任何“操作数连接”值。例如:foo=ExtractHour(ExpressionWrapper(x+y+z, output_field=models.DateTimeField()))bar=ExpressionWrapper(F('votes')-Subquery(y), output_field=models.FloatField())。注意:您可以使用Coalesce 获得相同的效果

以上是关于如何编写带有子查询的 Django 查询作为 WHERE 子句的一部分?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 django ORM 的 From 子句中编写子查询

如何重新排序 Django-Graphene 子查询?

带有子查询的Mysql删除[重复]

带有子查询的 MySQL UPDATE 查询永远存在

在 Django 的 ORM 中使用带有 UPDATE 的子查询

如何使用带有字符串作为输入的where子句编写select查询?