本文中使用Python 2.7.15Django 1.8.19MariaDB 5.5.62,本文设计大量Django生成的SQL,为方便阅读,对SQL进行了部分简化。

最近开始使用Django,Django的Model功能性非常简单,可以完全不关心SQL,与另一个流行的Python ORM工具SQLAlchyma形成强烈的对比。但是如果只使用Django Model的简单特性,其SQL性能是惨不忍睹的。所幸的是,Django还是提供了一些进阶工具给大家使用。

如果想优化Django的SQL性能,第一步是打开SQL日志,打开SQL日志需要在serrings.LOGGING.loggers字典中加如如下字典项:

1
2
3
4
5
'django.db.backends': {
'handlers': ['console'],
'propagate': True,
'level':'DEBUG',
},

在本例中,创建两个简单的模型如下:

1
2
3
4
5
6
class City(models.Model):
name = models.CharField(max_length=100)

class Person(models.Model):
name = models.CharField(max_length=100)
city = models.ForeignKey(City, related_name="person")

并在MySQL中写入如下测试数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
demo_city表
+----+----------+
| id | name |
+----+----------+
| 1 | ShenZhen |
| 2 | BeiJing |
+----+----------+
demo_person表
+----+--------+---------+
| id | name | city_id |
+----+--------+---------+
| 1 | Tom | 2 |
| 2 | Jerry | 2 |
| 3 | Snow | 2 |
| 4 | Frank | 2 |
| 5 | Bluice | 2 |
| 6 | Dave | 1 |
| 7 | Alen | 1 |
| 8 | Iris | 1 |
| 9 | Ella | 1 |
| 10 | Eva | 1 |
+----+--------+---------+

如果我们要获取id=1的用户的城市信息,我们一般很容易写出如下的代码。

1
2
person = Person.objects.filter(id=1).first()
print person.city.name

虽然我们看起来只做了一次查询,此时请求了两次数据库:

1
2
SELECT * FROM `demo_person` WHERE `demo_person`.`id` = 1 LIMIT 1;
SELECT * FROM `demo_city` WHERE `demo_city`.`id` = 2 LIMIT 1

如果我们要打印所有的人和城市的列表,用一个循环可以很好的解决:

1
2
3
persons = Person.objects.all()
for person in persons:
print person.name, person.city.name

我们不幸的请求了11次数据库:

1
2
3
4
5
6
7
8
9
10
11
SELECT * FROM `demo_person`;
SELECT * WHERE `demo_city`.`id` = 2;
SELECT * WHERE `demo_city`.`id` = 2;
SELECT * WHERE `demo_city`.`id` = 2;
SELECT * WHERE `demo_city`.`id` = 2;
SELECT * WHERE `demo_city`.`id` = 2;
SELECT * WHERE `demo_city`.`id` = 1;
SELECT * WHERE `demo_city`.`id` = 1;
SELECT * WHERE `demo_city`.`id` = 1;
SELECT * WHERE `demo_city`.`id` = 1;
SELECT * WHERE `demo_city`.`id` = 1;

而且其中有相当多的完全一样的SQL,列表查询中这是一个不可忽视的性能杀手。熟悉SQL应该了解,对多个关联表进行查询可以使用join关键字,在Django中我们可以使用.select_related()方法:

1
2
3
persons = Person.objects.select_related("city").all()
for person in persons:
print person.name, person.city.name

几乎和原来一模一样的代码,这次只生成了一个SQL:

1
2
SELECT `* FROM `demo_person` 
INNER JOIN `demo_city` ON ( `demo_person`.`city_id` = `demo_city`.`id` );

.select_related()通常情况下可以不加参数,Django的“懒查询”机制会自动会自动处理关联的表,但是有时(尤其是多表递归关联和Django多对多关系)会无效,需要手动处理。点击查看详情

上面的的还有一种预取查询的方案,写法如下:

1
2
3
person = Person.objects.prefetch_related('city').all()
for person in persons:
print person.name, person.city.name

生成的SQL如下:

1
2
SELECT * FROM `demo_person`;
SELECT * FROM `demo_city` WHERE `demo_city`.`id` IN (1, 2);

prefetch_related可以控制“子查询”的行为,点击查看详情

和子查询的语法非常类似,性能应该也类似于子查询,也大大减少了查询的数量。组合这两种方法,可以获得数倍的性能提升。尤其是在查询

虽然joinin在复杂查询时也是性能杀手,但比起循环查询数据库带来的IO开销,就是小巫见大巫了。