Django ORM – 多表实例(聚合与分组查询)(完整教程)

Django ORM – 多表实例(聚合与分组查询):从零掌握复杂数据操作

在实际开发中,我们很少会遇到单一数据表的场景。一个完整的系统,往往涉及多个数据表之间的关联。比如在电商系统中,订单表、用户表、商品表、分类表之间存在复杂的关联关系。这时候,如何高效地从多个表中提取所需数据,就成为了开发者的必修课。

Django ORM 提供了强大的多表查询能力,尤其在聚合与分组查询方面,它让我们能够用 Python 代码写出媲美 SQL 的复杂逻辑,而无需直接操作数据库。今天我们就来深入探讨 Django ORM – 多表实例(聚合与分组查询),通过真实案例带你一步步掌握这些核心技能。


理解多表关系:模型设计是基础

在开始查询之前,先让我们建立一个清晰的模型结构。假设我们要开发一个博客系统,包含用户、文章、评论、标签等多个实体。它们之间的关系如下:

  • 一个用户可以发布多篇文章
  • 一篇文章可以有多个评论
  • 一篇文章可以关联多个标签
  • 评论属于某篇文章,也属于某位用户

以下是对应的 Django 模型定义(简化版):

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=50, unique=True)
    email = models.EmailField()

    def __str__(self):
        return self.username

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

class Comment(models.Model):
    content = models.TextField()
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments')
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"Comment by {self.user.username} on {self.post.title}"

class Tag(models.Model):
    name = models.CharField(max_length=30, unique=True)

    def __str__(self):
        return self.name

class PostTag(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)

    class Meta:
        unique_together = ('post', 'tag')

    def __str__(self):
        return f"{self.post.title} - {self.tag.name}"

小贴士related_name 是关键!它让我们可以从反向关联中轻松访问数据,比如 user.posts 就能拿到该用户的所有文章。


当我们要查询文章及其作者信息时,如果直接写 Post.objects.all(),会触发 N+1 查询问题——每查一篇文章,就去数据库查一次作者信息。

Django 提供了两个优化方法:

  • select_related:用于一对一或外键关系,使用 SQL JOIN 一次性获取关联数据
  • prefetch_related:用于多对多或反向外键关系,先查主表,再查关联表,避免 N+1
posts = Post.objects.select_related('author').all()

for post in posts:
    print(f"文章: {post.title}, 作者: {post.author.username}")

📌 select_related 会生成一条 SQL JOIN 查询,性能极高,适合外键场景。
📌 prefetch_related 适合处理 many-to-many 或反向 ForeignKey,比如 post.comments


聚合查询:让数据“汇总”起来

有时候我们不需要每条记录,而是关心整体统计。比如:

  • 有多少篇文章?
  • 所有文章的平均字数?
  • 最早和最晚发布的文章时间?

Django ORM 提供了 aggregate() 方法,配合 Count, Avg, Sum, Max, Min 等函数来实现聚合。

使用聚合函数计算总数与平均值

from django.db.models import Count, Avg, Sum, Max, Min

total_posts = Post.objects.aggregate(Count('id'))
print(f"文章总数: {total_posts['id__count']}")

avg_length = Post.objects.aggregate(Avg('content__length'))
print(f"平均字数: {avg_length['content__length__avg']:.2f}")

user_post_count = User.objects.annotate(post_count=Count('posts'))
for user in user_post_count:
    print(f"{user.username} 发布了 {user.post_count} 篇文章")

🔍 重点annotate()aggregate() 的区别

  • aggregate():返回一个字典,代表整体结果
  • annotate():为每个主对象附加一个新字段,用于分组统计

分组查询:按条件分类统计

现在我们要分析“每个用户发布了多少篇文章”——这正是分组查询的典型场景。

使用 annotate() 配合 Count(),可以轻松实现分组。

按用户分组统计文章数量

user_stats = User.objects.annotate(
    post_count=Count('posts')
).order_by('-post_count')

print("用户文章数量排行:")
for user in user_stats:
    print(f"{user.username}: {user.post_count} 篇")

输出示例:

用户文章数量排行:
alice: 15 篇
bob: 8 篇
charlie: 3 篇

💡 形象比喻:想象你有一堆文件夹,每个文件夹代表一个用户。annotate 就是给每个文件夹贴上“里面有多少文件”的标签,方便你快速排序。


多表聚合:跨越多个模型的复杂统计

真正的挑战来了:我们想统计“每个标签下有多少篇文章”,这涉及 PostTag 这个中间表。

按标签分组统计文章数量

tag_stats = Tag.objects.annotate(
    post_count=Count('posttag')
).order_by('-post_count')

print("标签文章数量统计:")
for tag in tag_stats:
    print(f"{tag.name}: {tag.post_count} 篇")

✅ 这里 Tag.objects.annotate(post_count=Count('posttag')) 会自动通过 PostTag 中间表统计文章数。
📌 注意:posttagTag 模型中 PostTag 的反向关系名。


高级用法:条件聚合与复杂筛选

有时候我们只想统计“已发布超过 100 字”的文章,或者“2024 年内发布的文章”。

Django ORM 支持在聚合中加入条件筛选。

条件聚合:只统计特定条件的文章

from django.db.models import Q, Count

long_posts = Post.objects.filter(content__length__gt=100).aggregate(Count('id'))
print(f"长度超过 100 的文章有 {long_posts['id__count']} 篇")

long_post_stats = User.objects.annotate(
    long_post_count=Count('posts', filter=Q(posts__content__length__gt=100))
).order_by('-long_post_count')

print("按用户统计长文章数量:")
for user in long_post_stats:
    print(f"{user.username}: {user.long_post_count} 篇")

filter=Q(...) 是 Django 2.0+ 新特性,允许在聚合中加入条件过滤,非常强大!


实际应用:构建一个数据看板

让我们整合前面的知识,构建一个完整的数据看板函数,用于后台管理:

def generate_dashboard_data():
    """生成后台数据看板统计信息"""
    
    # 1. 总体统计
    total_users = User.objects.count()
    total_posts = Post.objects.count()
    total_comments = Comment.objects.count()
    
    # 2. 按用户分组统计文章数
    user_post_ranking = User.objects.annotate(
        post_count=Count('posts')
    ).order_by('-post_count')[:5]  # 取前 5 名
    
    # 3. 按标签分组统计文章数
    tag_post_stats = Tag.objects.annotate(
        post_count=Count('posttag')
    ).order_by('-post_count')[:5]
    
    # 4. 最近 7 天发布的文章数
    from django.utils import timezone
    import datetime
    
    seven_days_ago = timezone.now() - datetime.timedelta(days=7)
    recent_posts = Post.objects.filter(created_at__gte=seven_days_ago).count()
    
    return {
        'total_users': total_users,
        'total_posts': total_posts,
        'total_comments': total_comments,
        'recent_posts': recent_posts,
        'top_authors': [
            {'username': u.username, 'post_count': u.post_count}
            for u in user_post_ranking
        ],
        'top_tags': [
            {'name': t.name, 'post_count': t.post_count}
            for t in tag_post_stats
        ]
    }

data = generate_dashboard_data()
print("📊 数据看板统计结果:")
print(f"用户总数: {data['total_users']}")
print(f"文章总数: {data['total_posts']}")
print(f"最近 7 天新增文章: {data['recent_posts']}")

✅ 这个函数可以作为 Django 管理后台的统计接口,完全基于 ORM 实现,无需手写 SQL。


总结与建议

通过本文,我们系统地学习了 Django ORM – 多表实例(聚合与分组查询) 的核心能力:

  • 掌握了 select_relatedprefetch_related 的使用场景,避免 N+1 问题
  • 熟练使用 aggregate()annotate() 实现数据汇总
  • 能够处理多对多关系中的分组统计,如标签与文章的关系
  • 学会了条件聚合,实现更精细的数据筛选
  • 最终构建了一个完整的数据看板功能

📌 实践建议

  1. 优先使用 annotate() 而非原生 SQL,提升代码可维护性
  2. 使用 values()values_list() 配合聚合,避免返回完整模型对象
  3. 在复杂查询中,使用 query 属性打印生成的 SQL,调试性能问题

Django ORM 的强大之处,就在于它让我们用 Python 的思维去操作数据库。只要掌握好关联、聚合与分组的逻辑,就能轻松应对绝大多数复杂查询需求。多练习,多写,你会越来越得心应手。