
2026/01/24 1:43
**「Django のクエリセットにおける重複オブジェクトの除外」**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Django で関連モデルをクエリすると、JOIN が各一致する本ごとに行を作成するため、同じ著者が重複して返されることがあります。
.distinct() を追加するとこれらの重複が除外されますが、データベースは選択されたすべての列を比較せざるを得ず、大きなフィールド(JSONField や TextField など)が含まれる場合には遅くなる可能性があります。PostgreSQL はより高速な代替手段として distinct("id") を提供しています。これはユニークな主キーだけをチェックするため、パフォーマンスが向上しますが、この構文は PostgreSQL 固有です。
Django の
distinct(*fields) もまた、ORDER BY 句がそれらの distinct フィールドと一致している必要があります。そうでない場合、ProgrammingError が発生します。一般的な回避策として、サブクエリで一意の ID を最初に取得し(例:Author.objects.filter(id__in=subquery).order_by(...))、その後で並び替える方法があります。この手法は冗長であり、Django がクエリを最適化できても 2 回のデータベースアクセスが発生します。
本文で推奨されている最も効率的な解決策は
Exists サブクエリを使用する方法です。
Author.objects.filter( Exists(Book.objects.filter(author=OuterRef('id'), title__startswith='Book')) ).order_by('name')
このパターンは並び替えの制約を排除し、すべてのデータベースで動作し、各著者について 1 行だけ(最初に一致した行が見つかった時点で評価を停止)検索するため、モデル内に大きなテキストや JSON データが含まれていてもクエリを高速に保ちます。このアプローチを実装すると、開発者は多対多関係のコードをより明確かつ高速に書くことができ、大量のテキストまたは JSON コンテンツを扱うアプリケーションのスケーラビリティとユーザー体験を向上させることができます。
本文
Djangoでリレーションを横断してクエリセットをフィルタリングすると、結果に重複したオブジェクトが含まれることがあります。
これは1対多(1:N)でもN対N(多対多)でも発生しやすい落とし穴です。
問題点
リレーションを辿ってクエリセットをフィルタリングすると、Django は SQL の
JOIN を実行します。親オブジェクトに複数の関連オブジェクトが条件に合致した場合、その親は結果集合に 複数回 出現します。
具体例 – 1対多
class Author(models.Model): name = models.CharField(max_length=255) class Book(models.Model): title = models.CharField(max_length=255) author = models.ForeignKey( Author, on_delete=models.CASCADE, related_name='books', )
| Author | id |
|---|---|
| Charlie | 1 |
| Alice | 2 |
| Zoe | 3 |
| Book | id | title | author_id |
|---|---|---|---|
| Book A | 1 | Book A | 1 |
| Book B | 2 | Book B | 2 |
| Book C | 3 | Book C | 2 |
| Novel D | 4 | Novel D | 3 |
Author.objects.filter(books__title__startswith="Book") # [<Author: Charlie>, <Author: Alice>, <Author: Alice>]
Alice は Book B と Book C の両方を持つため、2 回現れます。
多対多リレーションでも同様に、複数のマッチンググループに属するオブジェクトが重複して出力されます。
よくある解決策(そしてそれぞれの問題点)
1. distinct()
distinct()Author.objects.filter(books__title__startswith="Book").distinct() # [<Author: Charlie>, <Author: Alice>]
- 利点:シンプルで直感的。
- 欠点:大規模テーブルではコストが高く、選択されたすべてのフィールドを比較します。
やJSONField
が大きい場合は遅くなる可能性があります。TextField
2. distinct(*fields)
(PostgreSQL 専用)
distinct(*fields)Author.objects.filter(books__title__startswith="Book").distinct("id") # [<Author: Charlie>, <Author: Alice>]
- 利点:指定したユニークフィールドだけで比較するため、フル
より高速。DISTINCT - 欠点
- PostgreSQL のみ利用可能。
- 他のフィールドで並べ替える際に制約があります。
は最初のSELECT DISTINCT ON
と一致しなければならないためです。ORDER BY
# 失敗する例: Author.objects.filter(books__title__startswith="Book")\ .order_by("name").distinct("id")
- 回避策:サブクエリを使う
sub = Author.objects.filter(books__title__startswith="Book").distinct("id") Author.objects.filter(id__in=sub).order_by("name") # [<Author: Alice>, <Author: Charlie>]
冗長で 2 回のクエリが必要になります(ただし Django は内部的にサブクエリとして最適化します)。
ベストソリューション:Exists
サブクエリ
Exists最もシンプルかつ高速な方法は
Exists サブクエリを使うことです。
from django.db.models import Exists, OuterRef Author.objects.filter( Exists( Book.objects.filter( author=OuterRef("id"), title__startswith="Book", ) ) ).order_by("name") # [<Author: Alice>, <Author: Charlie>]
なぜ優れているのか
- 性能:
はマッチング行が見つかった瞬間に処理を停止するため、非常に効率的。EXISTS - 並べ替え制限なし:任意のフィールドで
が使える。order_by() - 明確な意図:コード自体が「タイトルが 'Book' で始まる本を持つ著者を探す」ことを示している。
- データベース非依存:PostgreSQL に限定せず、サポートされている全ての DB で動作。
これで Django のクエリセットが重複なしに高速に取得できるようになります!