用有序相关模型的第一个值注释 QuerySet

Annotate QuerySet with first value of ordered related model

我有一些对象的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()

我试图找到给定作者在他们写的每个博客 post 之后出版的第一本神秘书。在 SQL 中,这可以通过 windowing 很好地实现。

Postgre 中的工作解决方案SQL 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 支持 window 函数,但我现在使用的是 Django 1.10。

尝试的解决方案

我首先构建了一个 Q 对象来过滤博客 post 之后出版的神秘书籍。

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

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

注意:Django 2.0 引入了 window 表达式,但我目前使用的是 Django 1.10,并且很好奇如何使用其中可用的 QuerySet 功能来实现这一点。

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

一个简单的方法可能是使用 architect module which can add partition functionality for PostgreSQL according to the documentation

另一个声称向 Django < 2.0 注入 Window 功能的模块是 django-query-builder which adds a 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 class 源代码或使用this alternate Window class 代码。

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

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

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;

相关,有详细解释:

  • Select first row in each GROUP BY group?

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 服务器的内容。