【问题标题】:Annotate QuerySet with first value of ordered related model使用有序相关模型的第一个值注释 QuerySet
【发布时间】:2018-06-13 18:05:06
【问题描述】:

我有一些对象的QuerySet。对于每一个,我希望用相关模型的最小值进行注释(在几个条件下加入,按日期排序)。我可以用 SQL 简洁地表达我想要的结果,但很好奇如何翻译成 Django 的 ORM。

背景

假设我有两个相关模型:BookBlogPost,每个模型都有一个指向 Author 的外键:

class Book(models.Model):
    title = models.CharField(max_length=255)
    genre = models.CharField(max_length=63)
    author = models.ForeignKey(Author)
    date_published = models.DateField()

class BlogPost(models.Model):
    author = models.ForeignKey(Author)
    date_published = models.DateField()

我正试图找到给定作者在他们撰写的每篇博文之后发表的第一本神秘书。在 SQL 中,这可以通过窗口很好地实现。

PostgreSQL 9.6 中的工作解决方案

WITH ordered AS (
  SELECT blog_post.id,
         book.title,
         ROW_NUMBER() OVER (
            PARTITION BY blog_post.id ORDER BY book.date_published
         ) AS rn
    FROM blog_post
         LEFT JOIN book ON book.author_id = blog_post.author_id
                       AND book.genre = 'mystery'
                       AND book.date_published >= blog_post.date_published
)
SELECT id,
       title
  FROM ordered
 WHERE rn = 1;

翻译成 Django 的 ORM

虽然上述 SQL 非常适合我的需求(如果需要,我可以使用原始 SQL),但我很好奇如何在 QuerySet 中执行此操作。我有一个现有的 QuerySet,我想进一步注释它

books = models.Book.objects.filter(...).select_related(...).prefetch_related(...)
annotated_books = books.annotate(
    most_recent_title=...
)

我知道 Django 2.0 支持窗口函数,但我现在使用 Django 1.10。

尝试的解决方案

我首先构建了一个 Q 对象,以过滤在博文之后发布的神秘书籍。

published_after = Q(
    author__book__date_published__gte=F('date_published'),
    author__book__genre='mystery'
)

从这里开始,我尝试将 django.db.models.Min 和其他 F 对象拼凑在一起以达到我想要的结果,但没有成功。

注意:Django 2.0 引入了窗口表达式,但我目前使用的是 Django 1.10,并且很好奇如何使用那里提供的 QuerySet 功能来做到这一点。

【问题讨论】:

  • 老实说(而且根本不是专家),我不认为您可以使用 Django 的 1.10 ORM 在单个查询中执行此操作... Django 1.11 和(如您所说)窗口表达式在 2.0 :-( 中引入
  • 你试过annotate(first=Min(<related_model__id>)).filter(<conditions with timestamps>)吗?

标签: python django postgresql django-queryset django-1.10


【解决方案1】:

也许使用.raw 并不是一个坏主意。检查Window class 的代码,我们可以看到它本质上是组成一个SQL 查询来实现“窗口化”。

一个简单的出路可能是使用architect模块,它可以根据the documentation为PostgreSQL添加分区功能。

另一个声称向 Django django-query-builder,它添加了一个 partition_by() 查询集方法,可以与 order_by 一起使用:

query = Query().from_table(
    Order,
    ['*', RowNumberField(
              'revenue', 
              over=QueryWindow().order_by('margin')
                                .partition_by('account_id')
          )
    ]
)
query.get_sql()
# SELECT tests_order.*, ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY margin ASC) AS revenue_row_number
# FROM tests_order

最后,您可以随时在项目中复制Window 类源代码或使用this alternate Window 类代码。

【讨论】:

    【解决方案2】:

    您的明显问题是 Django 1.10 太旧而无法正确处理window functions(已经存在了很长时间了非常)。

    如果您在没有窗口函数的情况下重写查询,这个问题就会消失。

    3 个等效查询

    哪一个最快取决于可用的索引和数据分布。但它们中的每一个都应该比你原来的更快。

    1. 使用 DISTINCT ON

    SELECT DISTINCT ON (p.id)
           p.id, b.title
    FROM   blog_post p
    LEFT   JOIN book b ON b.author_id = p.author_id
                      AND b.genre = 'mystery'
                      AND b.date_published >= p.date_published
    ORDER  BY p.id, b.date_published;
    

    相关,有详细说明:

    2. 使用 LATERAL 子查询(需要 Postgres 9.3 或更高版本):

    SELECT p.id, b.title
    FROM   blog_post p
    LEFT   JOIN LATERAL (
       SELECT title
       FROM   book 
       WHERE  author_id = p.author_id
       AND    genre = 'mystery'
       AND    date_published >= p.date_published
       ORDER  BY date_published
       LIMIT  1
       ) b ON true;
    -- ORDER BY p.id  -- optional
    

    相关,有详细说明:

    3. 或者更简单,但使用相关子查询

    SELECT p.id
         ,(SELECT title
           FROM   book 
           WHERE  author_id = p.author_id
           AND    genre = 'mystery'
           AND    date_published >= p.date_published
           ORDER  BY date_published
           LIMIT  1)
    FROM   blog_post p;
    -- ORDER BY p.id  -- optional
    

    每个都应该很容易翻译成 Django 语法。您也可以只使用原始 SQL,这就是发送到 Postgres 服务器的内容。

    【讨论】:

      猜你喜欢
      • 2011-04-18
      • 1970-01-01
      • 2011-11-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-14
      • 1970-01-01
      • 2014-07-01
      相关资源
      最近更新 更多