Khi lọc queryset qua quan hệ ManyToMany hoặc quan hệ ngược, Django có thể trả về cùng một bản ghi nhiều lần — danh sách hiển thị trùng lặp, phân trang lệch số lượng và count() báo sai. Nguyên nhân là câu lệnh JOIN sinh ra một dòng cho mỗi bản ghi liên quan khớp điều kiện. Cách sửa gọn nhất là thêm .distinct() vào cuối queryset.
Vấn đề
Tình huống quen thuộc: một cuốn sách có nhiều tác giả, bạn muốn liệt kê mọi cuốn sách của An hoặc Bình.
# Lấy mọi cuốn sách của An hoặc Bìnhbooks = Book.objects.filter(authors__name__in=["An", "Binh"])print(books.count()) # 3 — sai, chỉ có 2 cuốn sáchfor book in books: print(book.title)# Django cơ bản# Django cơ bản <- trùng!# Django nâng cao
Kết quả trả về 3 dòng dù chỉ có 2 cuốn sách: “Django cơ bản” xuất hiện hai lần vì nó khớp cả hai tác giả trong điều kiện lọc. Lỗi này âm thầm lan ra nhiều chỗ: len() và count() đều báo 3, phân trang tính thừa một trang, và giao diện hiển thị cùng một cuốn sách hai lần. Nguy hiểm hơn khi bạn dùng kết quả này để tính tổng tiền hay thống kê — con số sai mà không báo lỗi nào.
Giải pháp
Chỉ cần thêm .distinct() vào cuối queryset. Django dịch nó thành SELECT DISTINCT nên cơ sở dữ liệu tự loại bỏ các dòng trùng trước khi trả kết quả về, và bạn nhận đúng 2 cuốn sách.
books = Book.objects.filter( authors__name__in=["An", "Binh"]).distinct()print(books.count()) # 2 — đúng
distinct() còn hữu ích khi bạn chỉ cần danh sách giá trị duy nhất, ví dụ lấy tên mọi tác giả từng xuất bản sách. Kết hợp values_list() với distinct() để cơ sở dữ liệu làm việc này, thay vì tải toàn bộ về rồi lọc bằng set() trong Python.
# Danh sách tên tác giả duy nhấtAuthor.objects.values_list("name", flat=True).distinct()# <QuerySet ['An', 'Binh']>
Một lưu ý quan trọng: distinct() loại bỏ dòng trùng hoàn toàn giống nhau, không phải bản ghi “gần giống”. Nếu hai cuốn sách trùng tiêu đề nhưng khác ID, cả hai vẫn xuất hiện — đó là dữ liệu trùng thật, cần xử lý bằng unique constraint chứ không phải distinct().
Áp dụng vào dự án của bạn
Rà soát những queryset lọc qua tags, categories, authors hay bất kỳ quan hệ nhiều-nhiều nào trong dự án và thêm .distinct() vào cuối. Cách kiểm tra nhanh nhất là in count() trước và sau khi thêm: nếu con số giảm, bạn vừa sửa một lỗi trùng lặp thật. Đặt quy tắc cho team: mọi queryset filter qua quan hệ nhiều-nhiều đều kèm distinct().
Bài viết liên quan
- Django: sửa lỗi N+1 query bằng select_related trong 5 phút
- Django: sửa lỗi N+1 query với prefetch_related cho quan hệ ManyToMany và quan hệ ngược
- Django: tính toán trên queryset bằng annotate() thay vì vòng lặp Python
Câu hỏi thường gặp
distinct() có làm chậm truy vấn không?
Không đáng kể: cơ sở dữ liệu xử lý DISTINCT ngay trong câu lệnh SQL, thường nhanh hơn nhiều so với việc tải toàn bộ bản ghi trùng về Python rồi lọc bằng set(). Chỉ khi bảng có hàng triệu dòng và thiếu index phù hợp, bạn mới cần đo lại bằng django-debug-toolbar.
distinct(‘tên_cột’) dùng được trên MySQL và SQLite không?
Không. Dạng distinct() kèm tên cột (DISTINCT ON) chỉ có trên PostgreSQL; chạy trên MySQL hoặc SQLite sẽ báo lỗi NotSupportedError. Nếu dự án dùng nhiều database, hãy giữ distinct() không tham số để code chạy giống nhau ở mọi nơi.
distinct() khác gì dùng set() trong Python?
distinct() lọc trùng ngay tại cơ sở dữ liệu, chỉ trả về những dòng cần thiết. Dùng set() nghĩa là bạn đã tải cả bản ghi trùng về bộ nhớ rồi mới lọc — tốn RAM và chậm hơn khi dữ liệu lớn. Quy tắc chung: lọc càng sớm càng tốt, tốt nhất là ở tầng database.
Đã thêm distinct() mà kết quả vẫn trùng thì sao?
Hãy kiểm tra xem bạn có dùng annotate() hay values() kèm nhiều cột không — khi đó mỗi dòng đã khác nhau ở cột tính toán nên DISTINCT không loại được. Cách xử lý là gọi distinct() trước khi annotate, hoặc chỉ distinct() trên đúng cột cần lấy.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
Nguồn tham khảo
- Django documentation — QuerySet API reference: distinct() — https://docs.djangoproject.com/en/6.1/ref/models/querysets/#distinct
- Django documentation — Making queries: Spanning multi-valued relationships — https://docs.djangoproject.com/en/6.1/topics/db/queries/#spanning-multi-valued-relationships
