实战Django之Model操作之select_related 和 prefetch_related最佳实践(三)

这是本系列的最后一篇,主要是select_related() 和 prefetch_related() 的最佳实践。

主篇这里 

第一篇在这里 讲例子和select_related()

第二篇在这里 讲prefetch_related()


4. 一些实例


选择哪个函数

如果我们想要获得所有家乡是湖北的人,最无脑的做法是先获得 湖北省,再获得湖北的所有城市,最后获得故乡是这个城市的人。就像这样:


    
    
    
    
  1. >>> hb = Province.objects.get(name__iexact= u"湖北省")
  2. >>> people = []
  3. >>> for city in hb.city_set.all():
  4. ... people.extend(city.birth.all())
  5. ...
显然这不是一个明智的选择,因为这样做会导致1+(湖北省城市数)次SQL查询。反正是个反例,导致的查询和获得掉结果就不列出来了。

prefetch_related() 或许是一个好的解决方法,让我们来看看。

    
    
    
    
  1. >>> hb = Province.objects.prefetch_related( "city_set__birth").objects.get(name__iexact= u"湖北省")
  2. >>> people = []
  3. >>> for city in hb.city_set.all():
  4. ... people.extend(city.birth.all())
  5. ...
因为是一个深度为2的prefetch,所以会导致3次SQL查询:

    
    
    
    
  1. SELECT `QSOptimize_province`. `id`, `QSOptimize_province`. `name`
  2. FROM `QSOptimize_province`
  3. WHERE `QSOptimize_province`. `name` LIKE '湖北省' ;
  4. SELECT `QSOptimize_city`. `id`, `QSOptimize_city`. `name`, `QSOptimize_city`. `province_id`
  5. FROM `QSOptimize_city`
  6. WHERE `QSOptimize_city`. `province_id` IN ( 1);
  7. SELECT `QSOptimize_person`. `id`, `QSOptimize_person`. `firstname`, `QSOptimize_person`. `lastname`,
  8. `QSOptimize_person`. `hometown_id`, `QSOptimize_person`. `living_id`
  9. FROM `QSOptimize_person`
  10. WHERE `QSOptimize_person`. `hometown_id` IN ( 1, 3);

嗯...看上去不错,但是3次查询么?倒过来查询可能会更简单?
>>> people = list(Person.objects.select_related("hometown__province").filter(hometown__province__name__iexact=u"湖北省"))
    
    
    
    

    
    
    
    
  1. SELECT `QSOptimize_person`. `id`, `QSOptimize_person`. `firstname`, `QSOptimize_person`. `lastname`,
  2. `QSOptimize_person`. `hometown_id`, `QSOptimize_person`. `living_id`, `QSOptimize_city`. `id`,
  3. `QSOptimize_city`. `name`, `QSOptimize_city`. `province_id`, `QSOptimize_province`. `id`, `QSOptimize_province`. `name`
  4. FROM `QSOptimize_person`
  5. INNER JOIN `QSOptimize_city` ON ( `QSOptimize_person`. `hometown_id` = `QSOptimize_city`. `id`)
  6. INNER JOIN `QSOptimize_province` ON ( `QSOptimize_city`. `province_id` = `QSOptimize_province`. `id`)
  7. WHERE `QSOptimize_province`. `name` LIKE '湖北省';
+----+-----------+----------+-------------+-----------+----+--------+-------------+----+--------+
| id | firstname | lastname | hometown_id | living_id | id | name | province_id | id | name   | +----+-----------+----------+-------------+-----------+----+--------+-------------+----+--------+ |  1 | 张 || 3 |         1 | 3 | 十堰市 | 1 |  1 | 湖北省 |
| 2 || 四 |           1 | 3 |  1 | 武汉市 |           1 | 1 | 湖北省 | |  3 | 王 | 麻子     | 3 |         2 | 3 | 十堰市 | 1 |  1 | 湖北省 |
+----+-----------+----------+-------------+-----------+----+--------+-------------+----+--------+
3 rows in set (0.00 sec)
完全没问题。不仅SQL查询的数量减少了,python程序上也精简了。

select_related()的效率要高于prefetch_related()。因此,最好在能用select_related()的地方尽量使用它,也就是说,对于ForeignKey字段,避免使用prefetch_related()。



联用

对于同一个QuerySet,你可以同时使用这两个函数。
在我们一直使用的例子上加一个model:Order (订单)

    
    
    
    
  1. class Order(models.Model):
  2. customer = models.ForeignKey(Person)
  3. orderinfo = models.CharField(max_length= 50)
  4. time = models.DateTimeField(auto_now_add = True)
  5. def __unicode__(self):
  6. return self.orderinfo
如果我们拿到了一个订单的id 我们要知道这个订单的客户去过的省份。因为有ManyToManyField显然必须要用prefetch_related()。如果只用prefetch_related()会怎样呢?

    
    
    
    
  1. >>> plist = Order.objects.prefetch_related( 'customer__visitation__province').get(id= 1)
  2. >>> for city in plist.customer.visitation.all():
  3. ... print city.province.name
  4. ...
显然,关系到了4个表:Order、Person、City、Province,根据prefetch_related()的特性就得有4次SQL查询

    
    
    
    
  1. SELECT `QSOptimize_order`. `id`, `QSOptimize_order`. `customer_id`, `QSOptimize_order`. `orderinfo`, `QSOptimize_order`. `time`
  2. FROM `QSOptimize_order`
  3. WHERE `QSOptimize_order`. `id` = 1 ;
  4. SELECT `QSOptimize_person`. `id`, `QSOptimize_person`. `firstname`, `QSOptimize_person`. `lastname`, `QSOptimize_person`. `hometown_id`, `QSOptimize_person`. `living_id`
  5. FROM `QSOptimize_person`
  6. WHERE `QSOptimize_person`. `id` IN ( 1);
  7. SELECT ( `QSOptimize_person_visitation`. `person_id`) AS `_prefetch_related_val`, `QSOptimize_city`. `id`,
  8. `QSOptimize_city`. `name`, `QSOptimize_city`. `province_id`
  9. FROM `QSOptimize_city`
  10. INNER JOIN `QSOptimize_person_visitation` ON ( `QSOptimize_city`. `id` = `QSOptimize_person_visitation`. `city_id`)
  11. WHERE `QSOptimize_person_visitation`. `person_id` IN ( 1);
  12. SELECT `QSOptimize_province`. `id`, `QSOptimize_province`. `name`
  13. FROM `QSOptimize_province`
  14. WHERE `QSOptimize_province`. `id` IN ( 1, 2);
+----+-------------+---------------+---------------------+
| id | customer_id | orderinfo | time                | +----+-------------+---------------+---------------------+ |  1 | 1 | Info of Order | 2014-08-10 17:05:48 |
+----+-------------+---------------+---------------------+
1 row in set (0.00 sec)

±—±----------±---------±------------±----------+
| id | firstname | lastname | hometown_id | living_id |
±—±----------±---------±------------±----------+
| 1 || 三 | 3 | 1 |
±—±----------±---------±------------±----------+
1 row in set (0.00 sec)

±----------------------±—±-------±------------+
| _prefetch_related_val | id | name | province_id |
±----------------------±—±-------±------------+
|
1 | 1 | 武汉市 | 1 |
| 1 | 2 | 广州市 | 2 |
|
1 | 3 | 十堰市 | 1 |
±----------------------±—±-------±------------+
3 rows in set (0.00 sec)

±—±-------+
| id | name |
±—±-------+
|
1 | 湖北省 |
| 2 | 广东省 |
±—±-------+
2 rows in set (0.00 sec)



更好的办法是先调用一次select_related()再调用prefetch_related(),最后再select_related()后面的表

   
   
   
   
  1. >>> plist = Order.objects.select_related( 'customer').prefetch_related( 'customer__visitation__province').get(id= 1)
  2. >>> for city in plist.customer.visitation.all():
  3. ... print city.province.name
  4. ...
这样只会有3次SQL查询,Django会先做select_related,之后prefetch_related的时候会利用之前缓存的数据,从而避免了1次额外的SQL查询:

    
    
    
    
  1. SELECT `QSOptimize_order`. `id`, `QSOptimize_order`. `customer_id`, `QSOptimize_order`. `orderinfo`
  2. `QSOptimize_order`. `time`, `QSOptimize_person`. `id`, `QSOptimize_person`. `firstname`
  3. `QSOptimize_person`. `lastname`, `QSOptimize_person`. `hometown_id`, `QSOptimize_person`. `living_id` 
  4. FROM `QSOptimize_order` 
  5. INNER JOIN `QSOptimize_person` ON ( `QSOptimize_order`. `customer_id` = `QSOptimize_person`. `id`
  6. WHERE `QSOptimize_order`. `id` = 1 ;
  7. SELECT ( `QSOptimize_person_visitation`. `person_id`) AS `_prefetch_related_val`, `QSOptimize_city`. `id`
  8. `QSOptimize_city`. `name`, `QSOptimize_city`. `province_id` 
  9. FROM `QSOptimize_city` 
  10. INNER JOIN `QSOptimize_person_visitation` ON ( `QSOptimize_city`. `id` = `QSOptimize_person_visitation`. `city_id`
  11. WHERE `QSOptimize_person_visitation`. `person_id` IN ( 1);
  12. SELECT `QSOptimize_province`. `id`, `QSOptimize_province`. `name` 
  13. FROM `QSOptimize_province` 
  14. WHERE `QSOptimize_province`. `id` IN ( 1, 2);
+----+-------------+---------------+---------------------+----+-----------+----------+-------------+-----------+
| id | customer_id | orderinfo | time                | id | firstname | lastname | hometown_id | living_id |
+----+-------------+---------------+---------------------+----+-----------+----------+-------------+-----------+
| 1 |           1 | Info of Order | 2014-08-10 17:05:48 | 1 || 三 |           3 | 1 |
+----+-------------+---------------+---------------------+----+-----------+----------+-------------+-----------+
1 row in set (0.00 sec)

±----------------------±—±-------±------------+
| _prefetch_related_val | id | name   | province_id |
±----------------------±—±-------±------------+
|
                    1 |  1 | 武汉市 |           1 |
|                     1 |  2 | 广州市 |           2 |
|
                    1 |  3 | 十堰市 |           1 |
±----------------------±—±-------±------------+
3 rows in set (0.00 sec)

±—±-------+
| id | name |
±—±-------+
|
1 | 湖北省 |
| 2 | 广东省 |
±—±-------+
2 rows in set (0.00 sec)


值得注意的是,可以在调用prefetch_related之前调用select_related,并且Django会按照你想的去做:先select_related,然后利用缓存到的数据prefetch_related。然而一旦prefetch_related已经调用,select_related将不起作用。


小结

  1. 因为select_related()总是在单次SQL查询中解决问题,而prefetch_related()会对每个相关表进行SQL查询,因此select_related()的效率通常比后者高。
  2. 鉴于第一条,尽可能的用select_related()解决问题。只有在select_related()不能解决问题的时候再去想prefetch_related()。
  3. 你可以在一个QuerySet中同时使用select_related()和prefetch_related(),从而减少SQL查询的次数。
  4. 只有prefetch_related()之前的select_related()是有效的,之后的将会被无视掉。



关于这两个函数,我能想到的东西目前只有这么多。不过基于一些个人原因,写第三篇时间比较短,写的有些仓促。如果什么时候又想起了什么,我会在这篇博文中添加。

你可能感兴趣的:(Django,Django之model实战篇,Django实战篇【总】)