高性能Web开发:减少数据库往返

开发 前端
Web程序的后端主要有两个东西:渲染(生成HTML,或数据序列化)和IO(数据库操作,或内部服务调用)。今天要讲的是后面那个,关注一下如何减少数据库往返这个问题。最快的查询是不存在的,没有最快,只有更快!

背景

Web程序的后端主要有两个东西:渲染(生成HTML,或数据序列化)和IO(数据库操作,或内部服务调用)。今天要讲的是后面那个,关注一下如何减少数据库往返这个问题。最快的查询是不存在的,没有最快,只有更快!

开始讲之前我得提一下Schema的重要性,但不会在这花太多时间。单独一个因素不会影响程序的整体响应速度,有调数据的能力,比有一个好的数据(库)Schema要强得多。这些东西以后会细讲,但Schema问题常会限制你的选择,所以现在提一下。

我也会提一下缓存。在理想情况下,我要讨论的东西能有效减少返回不能缓存或缓存丢失的数据的时间,但跟通过优化查询减少数据库往返次数一样,避免将全部东西扔进缓存里是个极大的进步。

最后得提一下的是,文中我用的是Python(Django),但原理在其他语言或ORM框架里也适用。我以前搞过Java(Hibernate),不太顺手,后来搞Perl(DBIX::Class)、Ruby(Rails)以及其他几种东西去了。

N+1 Selects问题

关于数据库往返最常见又让人吃惊的问题是n+1 selects问题。这个问题最简单的形式包括一个有子对象的实体,和一对多的关系。下面是一个小例子。

  1. from django.db import models  
  2.  
  3.  
  4. class State(models.Model):  
  5.     name = models.CharField(max_length=64)  
  6.     country = models.ForeignKey(Country, related_name='states')  
  7.  
  8.     class Meta:  
  9.         ordering = ('name',)  
  10.  
  11.  
  12. class City(models.Model):  
  13.     name = models.CharField(max_length=64)  
  14.     state = models.ForeignKey(State, related_name='cities')  
  15.  
  16.     class Meta:  
  17.         ordering = ('name',) 

上面定义了州跟市,一个州有0或多个市,这个例子程序用来打印一个州跟市的内联列表。

  1. Alaska  
  2.     Anchorage  
  3.     Fairbanks  
  4.     Willow  
  5. California  
  6.     Berkeley  
  7.     Monterey  
  8.     Palo Alto  
  9.     San Diego  
  10.     San Francisco  
  11.     Santa Cruz  
  12. Kentucky  
  13.     Albany  
  14.     Monticello  
  15.     Lexington  
  16.     Louisville  
  17.     Somerset  
  18.     Stamping Ground 

要完成这个功能的代码如下:

  1. from django.shortcuts import render_to_response  
  2. from django.template.context import RequestContext  
  3. from locations.models import State  
  4.  
  5. def list_locations(request):  
  6.     data = {'states': State.objects.all()}  
  7.     return render_to_response('list_locations.html', data,  
  8.                               RequestContext(request)) 
  1. ...  
  2. <ul>  
  3. {% for state in states %}  
  4. <li>{{ state.name }}  
  5.     <ul>  
  6.         {% for city in state.cities.all %}  
  7.         <li>{{ city.name }}</li>  
  8.         {% endfor %}  
  9.     </ul>  
  10. </li>  
  11. {% endfor %}  
  12. </ul>  
  13. ... 

如果将上面的代码跑起来,生成相应的HTML,通过django-debug-toolbar就会看到有一个用于列出全部的州查询,然后对应每个州有一个查询,用于列出这个州下面的市。如果只有3个州,这不是很多,但如果是50个,“+1”部分还是一个查询,为了得到全部对应的市,“N"则变成了50。

2N+1 (不,这不算个事)

在开始搞这个N+1问题之前,我要给每个加一个属性,就是它所属的国家。这就引入另一个一对多关系。每个州只能属于一个国家。

  1. Alaska (United States)  
  2. ... 
  1. ...  
  2.  
  3. class Country(models.Model):  
  4.     name = models.CharField(max_length=64)  
  5.  
  6. class State(models.Model):  
  7.     name = models.CharField(max_length=64)  
  8.     country = models.ForeignKey(Country, related_name='states')  
  9.  
  10. ... 
  1. ...  
  2. <li>{{ state.name }} ({{ state.country.name }})  
  3. ... 

在django-debug-toolbar的SQL窗口里,能看到现在处理每个州时都得查询一下它所属的国家。注意,这里只能不停的检索同一个州,因为这些州都是同一个国家的。

2N+1 queries, not good 

现在就有两个有趣的问题了,这是每个Django ORM方案都要面对的问题。

#p#

select_related

  1. states = State.objects.select_related('country').all() 

select_related通过在查询主要对象(这里是州state)和其他对象(这里是国家country)之间的SQL做手脚起作用。这样就可以省去为每个州都查一次国家。假如一次数据库往返(网络中转->运行->返回)用时20ms,加起来的话共有N*20ms。如果N足够大,这样做挺费时的。

下面是新的检索州的查询:

  1. SELECT ... FROM "locations_state" 
  2.     INNER JOIN "locations_country" ON  
  3.         ("locations_state"."country_id" = "locations_country"."id")  
  4.     ORDER BY "locations_state"."name" ASC  
  5. ... 

用上面这个查询取代旧的,能省去用来找国家的二级查询。然而,这种解决有一个潜在的缺点,即反复的返回同一个国家对象,从而不得不一次又一次的将这一行传给ORM代码,生成大量重复的对象。等下我们还会再说说这个。

在继续往下之前得说一下,在Django ORM中,如果关系中的一方有多个对象,select_related是没用的。它能用来为一个州抓取对应的国家,但如果调用时添上“市”,它什么都不干。其他ORM框架(如Hibernate)没有这种限制,但要用类似功能时得特别小心,这类框架会在join的时候为二级对象重复生成一级对象,然后很快就会失控,ORM滞在那里不停的处理大量的数据或结果行。

综上所述,select_related的最好是在取单独一个对象、同时又想抓取到关联的(一个)对象时用。这样只有一次数据库往返,不会引入大量重复数据,这在Django ORM只有一对一关系时都适用。

prefetch_related

  1. states = State.objects.prefetch_related('country''cities').all() 

相反地, prefetch_related 的功能是收集关联对象的全部id值,一次性批量获取到它们,然后透明的附到相应的对象。这种方式最好的一个地方是能用在一对多关系中,比如本例中的州跟市。

下面是这种方式生成的SQL:

  1. SELECT ... FROM "locations_state" ORDER BY "locations_state"."name" ASC  
  2. SELECT ... FROM "locations_country" WHERE "locations_country"."id" IN (1)  
  3. SELECT ... FROM "locations_city" 
  4.     WHERE "locations_city"."state_id" IN (1, 2, 3)  
  5.     ORDER BY "locations_city"."name" ASC 

这样2N+1就变成3了。把N扔掉是个大进步。3 * 20ms总是会比(2 * 50 + 1) * 20ms  小,甚至比用select_related时的 (50 + 1) * 20ms也小。 

上面这个例子对国家跟市都采用了prefetch。前面说过这里的州都属同一国家,用select_related获得州记录时,这意味着要取到并处理这一国家记录N次。相反,用prefetch_related只要取一次。而这样会引入一次额外的数据库往返,有没有可能综合两种方式,你得在你的机器及数据上试试。然而,在本例中同时用select_related 和 prefetch_related可以将时间降到2 * 20ms,这可能会比分3次查询要快,但也有很多潜在因素要考虑。

  1. states = State.objects.select_related('country') \  
  2.     .prefetch_related('cities').all() 

2 queries, pretty good

 能支持多深的关系?

要跨多个级别时怎么办?select_related 和prefetch_related都可以通过双下划线遍历关系对象。用这个功能时,中间对象也会包括在内。这很有用,但在更复杂的对象模型中有点难用。 

  1. # only works when there's a single object at each step  
  2. city = City.objects.select_related('state__country').all()[0]  
  3. # 1 query, no further db queries  
  4. print('{0} - {1} - {2}'.format(city.name, city.state.name,  
  5.                                city.state.country.name)  
  6.  
  7. # works for both single and multiple object relationships  
  8. countries = Country.objects.prefetch_related('states__cities')  
  9. # 3 queries, no further db queries  
  10. for country in countries:  
  11.     for state in country.states:  
  12.         for city in state.cities:  
  13.             print('{0} - {1} - {2}'.format(city.name, city.state.name,  
  14.                                            city.state.country.name) 

prefetch_related用在原生查询

最后一点。上周的 efficiently querying for nearby things 一文中,为了实现查找最近的经度/纬度点,我写了一条复杂的SQL。其实最好的方法是写一条原生的sql查询 。而原生查询不支持prefetch_related,挺可惜的。但有一个变通的方法,即可以直接用Django实现prefetch_related功能的prefetch_related_objects。

  1. from django.db.models.query import prefetch_related_objects  
  2.  
  3. # prefetch_related_objects requires a list, it won't work on a QuerySet so  
  4. # we need to convert with list()  
  5. cities = list(City.objects.raw('<sql-query-for-nearby-cities>'))  
  6. prefetch_related_objects(cities, ('state__country',))  
  7. # 3 queries, no further db queries  
  8. for city in cities:  
  9.     print('{0} - {1} - {2}'.format(city.name, city.state.name,  
  10.                                    city.state.country.name) 

这多牛呀!

英文原文:High Performance Web: Reducing Database Round Trips

译文链接:http://www.oschina.net/translate/high-performance-web-reducing-database-round-trips

责任编辑:林师授 来源: OSCHINA编译
相关推荐

2011-04-21 09:59:48

WEBjavascript

2011-04-21 10:47:29

Webjavascript

2009-01-15 13:52:16

数据库管理开销

2013-09-10 16:16:19

移动网站性能优化移动web

2011-06-14 09:27:43

高性能WEB开发

2011-03-16 17:55:43

数据库管理开销

2019-07-23 11:41:45

数据库SQLDocker

2015-03-13 19:34:41

2011-04-07 13:53:25

Web工具

2011-04-18 10:16:30

WEB高性能

2018-06-01 14:00:00

数据库MySQL分库分表

2019-06-26 07:25:47

NoSQL数据库开发

2023-11-14 08:24:59

性能Scylla系统架构

2010-10-28 15:15:08

oracle内存参数

2022-02-21 10:14:15

数据中心电力

2011-04-19 11:06:03

JavaScriptweb

2018-10-10 14:27:34

数据库连接池MySQL

2011-10-18 13:58:32

高性能web

2011-04-27 10:57:29

高性能web开发

2011-04-07 13:39:24

WebHTTP
点赞
收藏

51CTO技术栈公众号