簡體   English   中英

使用有序相關模型的第一個值來注釋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()

我試圖找到一個給定作者在他們寫的每篇博文之后發表的第一本神秘書。 在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功能。

也許使用.raw並不是一個壞主意。 檢查Window的代碼,我們可以看到它實際上組成了一個SQL查詢來實現“窗口化”。

一個簡單的方法可能是使用架構師模塊,它可以根據文檔為PostgreSQL添加分區功能。

另一個聲稱向Django <2.0注入Window功能的模塊是django-query-builder ,它添加了partition_by() queryset方法,可以與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類源代碼或使用此備用 Window類代碼。

你明顯的問題是Django 1.10太舊了,無法正常處理窗口函數 (已經存在了長時間)。

如果您在沒有窗口函數的情況下重寫查詢,那么問題就會消失。

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服務器的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM