本文中使用Python 2.7.15、Django 1.8.19、MariaDB 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可以控制“子查询”的行为,点击查看详情
和子查询的语法非常类似,性能应该也类似于子查询,也大大减少了查询的数量。组合这两种方法,可以获得数倍的性能提升。尤其是在查询
虽然join、in在复杂查询时也是性能杀手,但比起循环查询数据库带来的IO开销,就是小巫见大巫了。