高性能 Web 開發:減少數據庫往返
背景
Web程序的后端主要有兩個東西:渲染(生成HTML,或數據序列化)和IO(數據庫操作,或內部服務調用)。今天要講的是后面那個,關注一下如何減少數據庫往返這個問題。最快的查詢是不存在的,沒有最快,只有更快!
開始講之前我得提一下Schema的重要性,但不會在這花太多時間。單獨一個因素不會影響程序的整體響應速度,有調數據的能力,比有一個好的數據(庫)Schema要強得多。這些東西以后會細講,但Schema問題常會限制你的選擇,所以現在提一下。
我也會提一下緩存。在理想情況下,我要討論的東西能有效減少返回不能緩存或緩存丟失的數據的時間,但跟通過優化查詢減少數據庫往返次數一樣,避免將全部東西扔進緩存里是個極大的進步。
最后得提一下的是,文中我用的是Python(Django),但原理在其他語言或ORM框架里也適用。我以前搞過Java(Hibernate),不太順手,后來搞Perl(DBIX::Class)、Ruby(Rails)以及其他幾種東西去了。
N+1 Selects問題
關于數據庫往返最常見又讓人吃驚的問題是n+1 selects問題。這個問題最簡單的形式包括一個有子對象的實體,和一對多的關系。下面是一個小例子。
from django.db import modelsclass State(models.Model): name = models.CharField(max_length=64) country = models.ForeignKey(Country, related_name='states')
class Meta: ordering = ('name',)
class City(models.Model): name = models.CharField(max_length=64) state = models.ForeignKey(State, related_name='cities')
class Meta: ordering = ('name',)</pre> <p>上面定義了州跟市,一個州有0或多個市,這個例子程序用來打印一個州跟市的內聯列表。 </p>
Alaska Anchorage Fairbanks Willow California Berkeley Monterey Palo Alto San Diego San Francisco Santa Cruz Kentucky Albany Monticello Lexington Louisville Somerset Stamping Ground要完成這個功能的代碼如下:
from django.shortcuts import render_to_response from django.template.context import RequestContext from locations.models import Statedef list_locations(request): data = {'states': State.objects.all()} return render_to_response('list_locations.html', data, RequestContext(request))</pre>
... <ul> {% for state in states %} <li>{{ state.name }} <ul> {% for city in state.cities.all %} <li>{{ city.name }}</li> {% endfor %} </ul> </li> {% endfor %} </ul> ...如果將上面的代碼跑起來,生成相應的HTML,通過django-debug-toolbar就會看到有一個用于列出全部的州查詢,然后對應每個州有一個查詢,用于列出這個州下面的市。如果只有3個州,這不是很多,但如果是50個,“+1”部分還是一個查詢,為了得到全部對應的市,“N"則變成了50。
2N+1 (不,這不算個事)
在開始搞這個N+1問題之前,我要給每個州加一個屬性,就是它所屬的國家。這就引入另一個一對多關系。每個州只能屬于一個國家。
Alaska (United States) ......class Country(models.Model): name = models.CharField(max_length=64)
class State(models.Model): name = models.CharField(max_length=64) country = models.ForeignKey(Country, related_name='states')
...</pre>
... <li>{{ state.name }} ({{ state.country.name }}) ...在django-debug-toolbar的SQL窗口里,能看到現在處理每個州時都得查詢一下它所屬的國家。注意,這里只能不停的檢索同一個州,因為這些州都是同一個國家的。
![]()
現在就有兩個有趣的問題了,這是每個Django ORM方案都要面對的問題。
select_related
states = State.objects.select_related('country').all()select_related通過在查詢主要對象(這里是州state)和其他對象(這里是國家country)之間的SQL做手腳起作用。這樣就可以省去為每個州都查一次國家。假如一次數據庫往返(網絡中轉->運行->返回)用時20ms,加起來的話共有N*20ms。如果N足夠大,這樣做挺費時的。
下面是新的檢索州的查詢:
SELECT ... FROM "locations_state" INNER JOIN "locations_country" ON ("locations_state"."country_id" = "locations_country"."id") ORDER BY "locations_state"."name" ASC ...用上面這個查詢取代舊的,能省去用來找國家的二級查詢。然而,這種解決有一個潛在的缺點,即反復的返回同一個國家對象,從而不得不一次又一次的將這一行傳給ORM代碼,生成大量重復的對象。等下我們還會再說說這個。
在繼續往下之前得說一下,在Django ORM中,如果關系中的一方有多個對象,select_related是沒用的。它能用來為一個州抓取對應的國家,但如果調用時添上“市”,它什么都不干。其他ORM框架(如Hibernate)沒有這種限制,但要用類似功能時得特別小心,這類框架會在join的時候為二級對象重復生成一級對象,然后很快就會失控,ORM滯在那里不停的處理大量的數據或結果行。
綜上所述,select_related的最好是在取單獨一個對象、同時又想抓取到關聯的(一個)對象時用。這樣只有一次數據庫往返,不會引入大量重復數據,這在Django ORM只有一對一關系時都適用。
prefetch_related
states = State.objects.prefetch_related('country', 'cities').all()相反地, prefetch_related 的功能是收集關聯對象的全部id值,一次性批量獲取到它們,然后透明的附到相應的對象。這種方式最好的一個地方是能用在一對多關系中,比如本例中的州跟市。
下面是這種方式生成的SQL:
SELECT ... FROM "locations_state" ORDER BY "locations_state"."name" ASC SELECT ... FROM "locations_country" WHERE "locations_country"."id" IN (1) SELECT ... FROM "locations_city" WHERE "locations_city"."state_id" IN (1, 2, 3) 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次查詢要快,但也有很多潛在因素要考慮。
states = State.objects.select_related('country') \ .prefetch_related('cities').all()
![]()
能支持多深的關系?
要跨多個級別時怎么辦?select_related 和prefetch_related都可以通過雙下劃線遍歷關系對象。用這個功能時,中間對象也會包括在內。這很有用,但在更復雜的對象模型中有點難用。
# only works when there's a single object at each step city = City.objects.select_related('state__country').all()[0]1 query, no further db queries
print('{0} - {1} - {2}'.format(city.name, city.state.name, city.state.country.name)
works for both single and multiple object relationships
countries = Country.objects.prefetch_related('states__cities')
3 queries, no further db queries
for country in countries: for state in country.states: for city in state.cities: print('{0} - {1} - {2}'.format(city.name, city.state.name, city.state.country.name)</pre>
prefetch_related用在原生查詢
最后一點。上周的 efficiently querying for nearby things 一文中,為了實現查找最近的經度/緯度點,我寫了一條復雜的SQL。其實最好的方法是寫一條原生的sql查詢 。而原生查詢不支持prefetch_related,挺可惜的。但有一個變通的方法,即可以直接用Django實現prefetch_related功能的prefetch_related_objects。
from django.db.models.query import prefetch_related_objectsprefetch_related_objects requires a list, it won't work on a QuerySet so
we need to convert with list()
cities = list(City.objects.raw('<sql-query-for-nearby-cities>')) prefetch_related_objects(cities, ('state__country',))
3 queries, no further db queries
for city in cities: print('{0} - {1} - {2}'.format(city.name, city.state.name, city.state.country.name)</pre>
這多牛呀!
本文地址:http://www.oschina.net/translate/high-performance-web-reducing-database-round-trips
原文地址:http://www.xormedia.com/high-performance-web-reducing-database-round-trips/