使用有序相关模型

时间:2018-01-04 02:47:33

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

我有一些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功能来实现这一点。

2 个答案:

答案 0 :(得分:4)

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

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

另一个声称向Django注入Window功能的模块< 2.0是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类代码。

答案 1 :(得分:3)

你明显的问题是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服务器的。