django group by

概要

如何用 django 自带的 ORM 框架来完成 SQL 中的 group by 操作呢?

group-by


环境介绍

foo 这个应用定义了如下模型。

class PersonModel(models.Model):
    name = models.CharField('姓名', max_length=64)
    age = models.PositiveIntegerField('年龄')

    def __str__(self):
        return f"{self.name} - {self.age}"

数据库层面看到的表结构如下。

CREATE TABLE `foo_personmodel` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
  `age` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

表中的数据如下。

mysql> select * from foo_personmodel;
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | tim   |  16 |
|  2 | jerry |  27 |
|  3 | marry |  18 |
|  4 | tim   |  20 |
|  5 | alis  |  17 |
+----+-------+-----+
5 rows in set (0.00 sec)

全表聚合

一个常见的需求,查询一下表中有多少行数据。

SQL 写法。

mysql> select count(id) as counts from foo_personmodel;
+--------+
| counts |
+--------+
|      5 |
+--------+
1 row in set (0.00 sec)

ORM 实现。

from django.db.models import Count
from apps.foo.models import PersonModel

PersonModel.objects.aggregate(counts = Count(id))

{'counts': 5}

注意 aggregate 返回的不再是 queryset 而是一个字典。


values 方法

要查询出每一行 name 列的取值。

SQL 写法。

mysql> select name from foo_personmodel;
+-------+
| name  |
+-------+
| tim   |
| jerry |
| marry |
| tim   |
| alis  |
+-------+
5 rows in set (0.00 sec)

ORM 实现。

PersonModel.objects.values("name")
<QuerySet [{'name': 'tim'}, {'name': 'jerry'}, {'name': 'marry'}, {'name': 'tim'}, {'name': 'alis'}]>

django 会返回一个 queryset 并且不会去重。


分组聚合

查询同一个名字在表中出现了多少次。

SQL 写法。

mysql> select name,count(id) as counts from foo_personmodel group by name;
+-------+--------+
| name  | counts |
+-------+--------+
| tim   |      2 |
| jerry |      1 |
| marry |      1 |
| alis  |      1 |
+-------+--------+
4 rows in set (0.00 sec)

ORM 写法。

PersonModel.objects.values("name").annotate(counts=Count(id))
<QuerySet [{'name': 'tim', 'counts': 2}, {'name': 'jerry', 'counts': 1}, {'name': 'marry', 'counts': 1}, {'name': 'alis', 'counts': 1}]>

神奇的事情发生了,当 values 和 annotate 一起使用的时候,values 就承担起了 group by 的角色。并且自动去掉了重项!


结论

django 中并没有为 group by 设置单独的方法,而是通过 values + annotate 的组合来实现的。