在django中访问数据库通常直接用orm,比如,

Post.objects.filter(...)

django的orm操作很方便,但方便的另一面则是这样的数据库访问操作可能发生在任何地方,不像java中会实现dao层来封装数据库访问。这样就不可避免会出现些随意的代码,有些看似简单的查询则是会对系统性能产生影响。因此在写代码时必须注意一些,尽可能避免写出的代码生成一个非常慢的sql查询。

注意外键查询字句

若是查询外键关联对象,能够用id的地方就用id,

Post.objects.filter(category__id=category.id)

值得注意的一点时django 1.3.x版中的一个bug,

Post.objects.filter(category=None)

会生成一条类似,

select * from post join category on post.category_id = category.id where category.id is null

在进行外键字段None值判定时会产生这么一条没有比较的join,对此解决之道是将查询改为,

Post.objects.exclude(category__isnull=False).filter(...)

这样生成的sql会变为,

  select * from post where (not (post.category.id is not null))

慎用select_related

使用select_related可以一次性将外键关联的字段一并取出,

# 生成一条sql,查询posts
posts = Post.objects.filter(...)
# 生成多条sql,访问每一条post上的category
for post in posts:
    print post.category.name

# 生成一条sql,查询posts以及posts关联的所有category
posts = Post.objects.select_related('category').filter(...)
for post in posts:
    print post.category.name

使用select_related可以减少生成的sql条数,不过却也有些问题,

  • 消耗更多内存

若后续不访问外键关联字段则内存就无谓消耗了。默认不加参数select_related会级连获取所有外键字段,内存、性能都成问题。

  • 性能可能变差

select_related减少了sql数,但却引入了join操作。在数据表很大的情况下join的开销很有可能比多次简单的sql查询还要大,后者在有cache的情况下性能还是有所保证的。

因此个人经验就是能不用select_related就不用,但若是查询字句中原本就需要通过外键字段进行判定,那么用select_related就不会有什么再额外的开销。

使用prefetch

相较select_related引入join,prefetch则是会生成两条sql,

# 一条sql用于查询posts、一条用于查询所有posts对应的category
Post.objects.prefetch('category').filter(...)

使用prefetch就绕开了join可能的性能底下以及过多sql这两个问题,不过prefech在1.3.x版本中是不存在的,这就还是略尴尬。

避免意外的复杂查询

queryset是lazy的,在创建queryset对象时并不会去真正访问数据库,访问数据库是在queryset第一次被访问时进行的。因此不少时候看着性能应该还不错的代码实际性能却会很烂,

category_id_list = Category.objects.filter(...).values('id', flat=True)
Post.objects.filter(category__id__in=category_id_list)

上面代码看着应该会产生两条sql,一条获取category id,一条获取posts,然而实际上生成的sql却只有一条,

select * from post where cagegory_id in (select id from category)

子查询的性能在一些情况下会很糟糕,糟糕的性能明显不是我们想要的,这算是lazy带来的一些副作用。因此我们需要强制先执行第一条语句,代码可以进行如下修改,

category_id_list = list(Category.objects.filter(...).values('id', flat=True))
Post.objects.filter(category__id__in=category_id_list)

创建合适的索引

对单条sql性能影响最大的还是数据库索引,默认情况下主键、外键都是有索引的,剩下的则是需要手动创建。索引创建依赖实际使用的查询字句,对于频繁出现的查询字句,就可以考虑对其创建索引,单个字段之外还需要考虑创建联合索引。

索引创建一是在代码实现之处就很明白哪些字段应该放入索引中,二是需要分析那些慢查询,看是否能够通过索引进行优化。对mysql slow sql的监控是非常有必要的,通过各种优化处理,消灭slow sql是持续的目标。